Skip to content

Commit 8dc8d94

Browse files
authored
Add defaults (#191)
1 parent 2f863c2 commit 8dc8d94

File tree

7 files changed

+107
-7
lines changed

7 files changed

+107
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## dev
4+
5+
- Added `document_defaults` config option allowing to automatically annotate parameter defaults.
6+
37
## 1.13.1
48

59
- Fixed ``NewType`` inserts a reference as first argument instead of a string

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ The following configuration options are accepted:
6161
`True`, add stub documentation for undocumented parameters to be able to add type info.
6262
- `typehints_document_rtype` (default: `True`): If `False`, never add an `:rtype:` directive. If `True`, add the
6363
`:rtype:` directive if no existing `:rtype:` is found.
64+
- `document_defaults` (default: `None`): If `None`, defaults are not added. Otherwise adds a default annotation:
65+
66+
- `'comma'` adds it after the type, changing Sphinx’ default look to “**param** (*int*, default: `1`) -- text”.
67+
- `'braces'` adds `(default: ...)` after the type (useful for numpydoc like styles).
68+
- `'braces-after'` adds `(default: ...)` at the end of the parameter documentation text instead.
69+
6470
- `simplify_optional_unions` (default: `True`): If `True`, optional parameters of type \"Union\[\...\]\" are simplified
6571
as being of type Union\[\..., None\] in the resulting documention (e.g. Optional\[Union\[A, B\]\] -\> Union\[A, B,
6672
None\]). If `False`, the \"Optional\"-type is kept. Note: If `False`, **any** Union containing `None` will be

src/sphinx_autodoc_typehints/__init__.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
import sys
33
import textwrap
44
import typing
5-
from typing import Any, AnyStr, NewType, Tuple, TypeVar, get_type_hints
5+
from typing import Any, AnyStr, Dict, NewType, Optional, Tuple, TypeVar, get_type_hints
66

7+
from sphinx.application import Sphinx
78
from sphinx.util import logging
89
from sphinx.util.inspect import signature as sphinx_signature
910
from sphinx.util.inspect import stringify_signature
@@ -425,7 +426,17 @@ def add(val):
425426
return result
426427

427428

428-
def process_docstring(app, what, name, obj, options, lines): # noqa: U100
429+
def format_default(app: Sphinx, default: Any) -> Optional[str]:
430+
if default is inspect.Parameter.empty:
431+
return None
432+
formatted = repr(default).replace("\\", "\\\\")
433+
if app.config.typehints_defaults.startswith("braces"):
434+
return f" (default: ``{formatted}``)"
435+
else:
436+
return f", default: ``{formatted}``"
437+
438+
439+
def process_docstring(app: Sphinx, what, name, obj, options, lines): # noqa: U100
429440
original_obj = obj
430441
if isinstance(obj, property):
431442
obj = obj.fget
@@ -435,11 +446,13 @@ def process_docstring(app, what, name, obj, options, lines): # noqa: U100
435446
obj = obj.__init__
436447

437448
obj = inspect.unwrap(obj)
449+
signature = sphinx_signature(obj)
438450
type_hints = get_all_type_hints(obj, name)
439451

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

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

464477
if insert_index is not None:
465-
lines.insert(insert_index, f":type {arg_name}: {formatted_annotation}")
478+
type_annotation = f":type {arg_name}: {formatted_annotation}"
479+
if app.config.typehints_defaults:
480+
formatted_default = format_default(app, default)
481+
if formatted_default:
482+
if app.config.typehints_defaults.endswith("after"):
483+
lines[insert_index] += formatted_default
484+
else: # add to last param doc line
485+
type_annotation += formatted_default
486+
lines.insert(insert_index, type_annotation)
466487

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

495516

496-
def builder_ready(app):
517+
def builder_ready(app: Sphinx) -> None:
497518
if app.config.set_type_checking_flag:
498519
typing.TYPE_CHECKING = True
499520

500521

501-
def setup(app):
522+
def validate_config(app: Sphinx, *args) -> None: # noqa: U100
523+
valid = {None, "comma", "braces", "braces-after"}
524+
if app.config.typehints_defaults not in valid | {False}:
525+
raise ValueError(f"typehints_defaults needs to be one of {valid!r}, not {app.config.typehints_defaults!r}")
526+
527+
528+
def setup(app: Sphinx) -> Dict[str, bool]:
502529
app.add_config_value("set_type_checking_flag", False, "html")
503530
app.add_config_value("always_document_param_types", False, "html")
504531
app.add_config_value("typehints_fully_qualified", False, "env")
505532
app.add_config_value("typehints_document_rtype", True, "env")
533+
app.add_config_value("typehints_defaults", None, "env")
506534
app.add_config_value("simplify_optional_unions", True, "env")
507535
app.connect("builder-inited", builder_ready)
536+
app.connect("env-before-read-docs", validate_config) # config may be changed after “config-inited” event
508537
app.connect("autodoc-process-signature", process_signature)
509538
app.connect("autodoc-process-docstring", process_docstring)
510539
return {"parallel_read_safe": True}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def function(x: bool, y: int = 1) -> str: # noqa: U100
2+
"""
3+
Function docstring.
4+
5+
:param x: foo
6+
:param y: bar
7+
"""

tests/roots/test-dummy/simple.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Simple Module
2+
=============
3+
4+
.. autofunction:: dummy_module_simple.function

tests/test_sphinx_autodoc_typehints.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,11 +263,11 @@ def set_python_path():
263263

264264

265265
def maybe_fix_py310(expected_contents):
266-
if sys.version_info[:2] >= (3, 10):
266+
if PY310_PLUS:
267267
for old, new in [
268268
("*str** | **None*", '"Optional"["str"]'),
269269
("(*bool*)", '("bool")'),
270-
("(*int*)", '("int")'),
270+
("(*int*", '("int"'),
271271
(" str", ' "str"'),
272272
('"Optional"["str"]', '"Optional"["str"]'),
273273
('"Optional"["Callable"[["int", "bytes"], "int"]]', '"Optional"["Callable"[["int", "bytes"], "int"]]'),
@@ -613,6 +613,55 @@ def test_sphinx_output_future_annotations(app, status):
613613
assert text_contents == maybe_fix_py310(expected_contents)
614614

615615

616+
@pytest.mark.parametrize(
617+
("defaults_config_val", "expected"),
618+
[
619+
(None, '("int") -- bar'),
620+
("comma", '("int", default: "1") -- bar'),
621+
("braces", '("int" (default: "1")) -- bar'),
622+
("braces-after", '("int") -- bar (default: "1")'),
623+
("comma-after", Exception("needs to be one of")),
624+
],
625+
)
626+
@pytest.mark.sphinx("text", testroot="dummy")
627+
@patch("sphinx.writers.text.MAXWIDTH", 2000)
628+
def test_sphinx_output_defaults(app, status, defaults_config_val, expected):
629+
set_python_path()
630+
631+
app.config.master_doc = "simple"
632+
app.config.typehints_defaults = defaults_config_val
633+
try:
634+
app.build()
635+
except Exception as e:
636+
if not isinstance(expected, Exception):
637+
raise
638+
assert str(expected) in str(e)
639+
return
640+
assert "build succeeded" in status.getvalue()
641+
642+
text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "simple.txt"
643+
text_contents = text_path.read_text().replace("–", "--")
644+
expected_contents = textwrap.dedent(
645+
f"""\
646+
Simple Module
647+
*************
648+
649+
dummy_module_simple.function(x, y=1)
650+
651+
Function docstring.
652+
653+
Parameters:
654+
* **x** ("bool") -- foo
655+
656+
* **y** {expected}
657+
658+
Return type:
659+
"str"
660+
"""
661+
)
662+
assert text_contents == expected_contents
663+
664+
616665
def test_normalize_source_lines_async_def():
617666
source = textwrap.dedent(
618667
"""

whitelist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func
1616
getmodule
1717
getsource
1818
idx
19+
inited
1920
inv
2021
isfunction
2122
iterdir

0 commit comments

Comments
 (0)