Skip to content

Commit

Permalink
Add defaults (#191)
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep authored Jan 5, 2022
1 parent 2f863c2 commit 8dc8d94
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## dev

- Added `document_defaults` config option allowing to automatically annotate parameter defaults.

## 1.13.1

- Fixed ``NewType`` inserts a reference as first argument instead of a string
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ The following configuration options are accepted:
`True`, add stub documentation for undocumented parameters to be able to add type info.
- `typehints_document_rtype` (default: `True`): If `False`, never add an `:rtype:` directive. If `True`, add the
`:rtype:` directive if no existing `:rtype:` is found.
- `document_defaults` (default: `None`): If `None`, defaults are not added. Otherwise adds a default annotation:

- `'comma'` adds it after the type, changing Sphinx’ default look to “**param** (*int*, default: `1`) -- text”.
- `'braces'` adds `(default: ...)` after the type (useful for numpydoc like styles).
- `'braces-after'` adds `(default: ...)` at the end of the parameter documentation text instead.

- `simplify_optional_unions` (default: `True`): If `True`, optional parameters of type \"Union\[\...\]\" are simplified
as being of type Union\[\..., None\] in the resulting documention (e.g. Optional\[Union\[A, B\]\] -\> Union\[A, B,
None\]). If `False`, the \"Optional\"-type is kept. Note: If `False`, **any** Union containing `None` will be
Expand Down
39 changes: 34 additions & 5 deletions src/sphinx_autodoc_typehints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import sys
import textwrap
import typing
from typing import Any, AnyStr, NewType, Tuple, TypeVar, get_type_hints
from typing import Any, AnyStr, Dict, NewType, Optional, Tuple, TypeVar, get_type_hints

from sphinx.application import Sphinx
from sphinx.util import logging
from sphinx.util.inspect import signature as sphinx_signature
from sphinx.util.inspect import stringify_signature
Expand Down Expand Up @@ -425,7 +426,17 @@ def add(val):
return result


def process_docstring(app, what, name, obj, options, lines): # noqa: U100
def format_default(app: Sphinx, default: Any) -> Optional[str]:
if default is inspect.Parameter.empty:
return None
formatted = repr(default).replace("\\", "\\\\")
if app.config.typehints_defaults.startswith("braces"):
return f" (default: ``{formatted}``)"
else:
return f", default: ``{formatted}``"


def process_docstring(app: Sphinx, what, name, obj, options, lines): # noqa: U100
original_obj = obj
if isinstance(obj, property):
obj = obj.fget
Expand All @@ -435,11 +446,13 @@ def process_docstring(app, what, name, obj, options, lines): # noqa: U100
obj = obj.__init__

obj = inspect.unwrap(obj)
signature = sphinx_signature(obj)
type_hints = get_all_type_hints(obj, name)

for arg_name, annotation in type_hints.items():
if arg_name == "return":
continue # this is handled separately later
default = signature.parameters[arg_name].default
if arg_name.endswith("_"):
arg_name = f"{arg_name[:-1]}\\_"

Expand All @@ -462,7 +475,15 @@ def process_docstring(app, what, name, obj, options, lines): # noqa: U100
insert_index = len(lines)

if insert_index is not None:
lines.insert(insert_index, f":type {arg_name}: {formatted_annotation}")
type_annotation = f":type {arg_name}: {formatted_annotation}"
if app.config.typehints_defaults:
formatted_default = format_default(app, default)
if formatted_default:
if app.config.typehints_defaults.endswith("after"):
lines[insert_index] += formatted_default
else: # add to last param doc line
type_annotation += formatted_default
lines.insert(insert_index, type_annotation)

if "return" in type_hints and not inspect.isclass(original_obj):
# This avoids adding a return type for data class __init__ methods
Expand Down Expand Up @@ -493,18 +514,26 @@ def process_docstring(app, what, name, obj, options, lines): # noqa: U100
lines.insert(insert_index, f":rtype: {formatted_annotation}")


def builder_ready(app):
def builder_ready(app: Sphinx) -> None:
if app.config.set_type_checking_flag:
typing.TYPE_CHECKING = True


def setup(app):
def validate_config(app: Sphinx, *args) -> None: # noqa: U100
valid = {None, "comma", "braces", "braces-after"}
if app.config.typehints_defaults not in valid | {False}:
raise ValueError(f"typehints_defaults needs to be one of {valid!r}, not {app.config.typehints_defaults!r}")


def setup(app: Sphinx) -> Dict[str, bool]:
app.add_config_value("set_type_checking_flag", False, "html")
app.add_config_value("always_document_param_types", False, "html")
app.add_config_value("typehints_fully_qualified", False, "env")
app.add_config_value("typehints_document_rtype", True, "env")
app.add_config_value("typehints_defaults", None, "env")
app.add_config_value("simplify_optional_unions", True, "env")
app.connect("builder-inited", builder_ready)
app.connect("env-before-read-docs", validate_config) # config may be changed after “config-inited” event
app.connect("autodoc-process-signature", process_signature)
app.connect("autodoc-process-docstring", process_docstring)
return {"parallel_read_safe": True}
7 changes: 7 additions & 0 deletions tests/roots/test-dummy/dummy_module_simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def function(x: bool, y: int = 1) -> str: # noqa: U100
"""
Function docstring.
:param x: foo
:param y: bar
"""
4 changes: 4 additions & 0 deletions tests/roots/test-dummy/simple.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Simple Module
=============

.. autofunction:: dummy_module_simple.function
53 changes: 51 additions & 2 deletions tests/test_sphinx_autodoc_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,11 @@ def set_python_path():


def maybe_fix_py310(expected_contents):
if sys.version_info[:2] >= (3, 10):
if PY310_PLUS:
for old, new in [
("*str** | **None*", '"Optional"["str"]'),
("(*bool*)", '("bool")'),
("(*int*)", '("int")'),
("(*int*", '("int"'),
(" str", ' "str"'),
('"Optional"["str"]', '"Optional"["str"]'),
('"Optional"["Callable"[["int", "bytes"], "int"]]', '"Optional"["Callable"[["int", "bytes"], "int"]]'),
Expand Down Expand Up @@ -613,6 +613,55 @@ def test_sphinx_output_future_annotations(app, status):
assert text_contents == maybe_fix_py310(expected_contents)


@pytest.mark.parametrize(
("defaults_config_val", "expected"),
[
(None, '("int") -- bar'),
("comma", '("int", default: "1") -- bar'),
("braces", '("int" (default: "1")) -- bar'),
("braces-after", '("int") -- bar (default: "1")'),
("comma-after", Exception("needs to be one of")),
],
)
@pytest.mark.sphinx("text", testroot="dummy")
@patch("sphinx.writers.text.MAXWIDTH", 2000)
def test_sphinx_output_defaults(app, status, defaults_config_val, expected):
set_python_path()

app.config.master_doc = "simple"
app.config.typehints_defaults = defaults_config_val
try:
app.build()
except Exception as e:
if not isinstance(expected, Exception):
raise
assert str(expected) in str(e)
return
assert "build succeeded" in status.getvalue()

text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "simple.txt"
text_contents = text_path.read_text().replace("–", "--")
expected_contents = textwrap.dedent(
f"""\
Simple Module
*************
dummy_module_simple.function(x, y=1)
Function docstring.
Parameters:
* **x** ("bool") -- foo
* **y** {expected}
Return type:
"str"
"""
)
assert text_contents == expected_contents


def test_normalize_source_lines_async_def():
source = textwrap.dedent(
"""
Expand Down
1 change: 1 addition & 0 deletions whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func
getmodule
getsource
idx
inited
inv
isfunction
iterdir
Expand Down

0 comments on commit 8dc8d94

Please sign in to comment.