diff --git a/AUTHORS.rst b/AUTHORS.rst index 5c0f9bb9882..224dacd9936 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -27,6 +27,7 @@ Contributors * Andi Albrecht -- agogo theme * Antonio Valentino -- qthelp builder, docstring inheritance * Antti Kaihola -- doctest extension (skipif option) +* Barak Katzir -- autodoc improvements * Barry Warsaw -- setup command improvements * Bart Kamphorst -- warning improvements * Ben Egan -- Napoleon improvements & viewcode improvements diff --git a/CHANGES.rst b/CHANGES.rst index 6acb88698e4..caff84fd2a9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -33,6 +33,9 @@ Deprecated Features added -------------- +* #10351, #10359: autodoc: overloaded function or method can now be customized in the + 'autodoc-before-process-signature' and 'autodoc-process-signature' events. + Patch by Barak Katzir. * #13173: Add a new ``duplicate_declaration`` warning type, with ``duplicate_declaration.c`` and ``duplicate_declaration.cpp`` subtypes. Patch by Julien Lecomte and Adam Turner. @@ -115,6 +118,9 @@ Features added Bugs fixed ---------- +* #9813: autodoc: partial bugfix, :confval:`autodoc_type_aliases` is now supported + by overload signatures of functions and methods. + Patch by Barak Katzir. * #12463: autosummary: Respect an empty module ``__all__``. Patch by Valentin Pratz * #13060: HTML Search: use ``Map`` to store per-file term scores. diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 7f32a720391..bfc71d1806a 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -12,7 +12,7 @@ import re import sys from inspect import Parameter, Signature -from typing import TYPE_CHECKING, Any, NewType, TypeVar +from typing import TYPE_CHECKING, Any, NewType, TypeVar, get_overloads from docutils.statemachine import StringList @@ -1473,6 +1473,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ objtype = 'function' member_order = 30 + overload_impl_sig: Signature | None = None @classmethod def can_document_member( @@ -1498,6 +1499,8 @@ def format_args(self, **kwargs: Any) -> str: sig = inspect.signature( self.object, type_aliases=self.config.autodoc_type_aliases ) + if self.overload_impl_sig is not None: + sig = self.merge_default_value(self.overload_impl_sig, sig) args = stringify_signature(sig, **kwargs) except TypeError as exc: logger.warning( @@ -1559,15 +1562,14 @@ def format_signature(self, **kwargs: Any) -> str: actual = inspect.signature( self.object, type_aliases=self.config.autodoc_type_aliases ) - __globals__ = safe_getattr(self.object, '__globals__', {}) - for overload in self.analyzer.overloads['.'.join(self.objpath)]: - overload = self.merge_default_value(actual, overload) - overload = evaluate_signature( - overload, __globals__, self.config.autodoc_type_aliases - ) - - sig = stringify_signature(overload, **kwargs) - sigs.append(sig) + overload_kwargs = kwargs | {'show_annotation': True} + for overload_func in get_overloads(self.object): + documenter = type(self)(self.directive, '') + documenter.object = overload_func + documenter.objpath = [''] + # pass actual implementation signature to merge default values later + documenter.overload_impl_sig = actual + sigs.append(documenter.format_signature(**overload_kwargs)) return '\n'.join(sigs) @@ -1576,7 +1578,7 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu parameters = list(overload.parameters.values()) for i, param in enumerate(parameters): actual_param = actual.parameters.get(param.name) - if actual_param and param.default == '...': + if actual_param and param.default in {'...', ...}: parameters[i] = param.replace(default=actual_param.default) return overload.replace(parameters=parameters) @@ -2390,6 +2392,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: directivetype = 'method' member_order = 50 priority = 1 # must be more than FunctionDocumenter + overload_impl_sig: Signature | None = None @classmethod def can_document_member( @@ -2449,6 +2452,8 @@ def format_args(self, **kwargs: Any) -> str: bound_method=True, type_aliases=self.config.autodoc_type_aliases, ) + if self.overload_impl_sig is not None: + sig = self.merge_default_value(self.overload_impl_sig, sig) args = stringify_signature(sig, **kwargs) except TypeError as exc: logger.warning( @@ -2537,20 +2542,15 @@ def format_signature(self, **kwargs: Any) -> str: type_aliases=self.config.autodoc_type_aliases, ) - __globals__ = safe_getattr(self.object, '__globals__', {}) - for overload in self.analyzer.overloads['.'.join(self.objpath)]: - overload = self.merge_default_value(actual, overload) - overload = evaluate_signature( - overload, __globals__, self.config.autodoc_type_aliases - ) - - if not inspect.isstaticmethod( - self.object, cls=self.parent, name=self.object_name - ): - parameters = list(overload.parameters.values()) - overload = overload.replace(parameters=parameters[1:]) - sig = stringify_signature(overload, **kwargs) - sigs.append(sig) + overload_kwargs = kwargs | {'show_annotation': True} + for overload_func in get_overloads(self.object): + documenter = type(self)(self.directive, '') + documenter.object = overload_func + documenter.objpath = [''] + documenter.parent = self.parent + # pass actual implementation signature to merge default values later + documenter.overload_impl_sig = actual + sigs.append(documenter.format_signature(**overload_kwargs)) return '\n'.join(sigs) @@ -2559,7 +2559,7 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu parameters = list(overload.parameters.values()) for i, param in enumerate(parameters): actual_param = actual.parameters.get(param.name) - if actual_param and param.default == '...': + if actual_param and param.default in {'...', Ellipsis}: parameters[i] = param.replace(default=actual_param.default) return overload.replace(parameters=parameters) diff --git a/tests/roots/test-ext-autodoc/target/autodoc_type_aliases.py b/tests/roots/test-ext-autodoc/target/autodoc_type_aliases.py index 846c48a432c..554c87beeb5 100644 --- a/tests/roots/test-ext-autodoc/target/autodoc_type_aliases.py +++ b/tests/roots/test-ext-autodoc/target/autodoc_type_aliases.py @@ -4,9 +4,11 @@ from typing import TYPE_CHECKING, overload if TYPE_CHECKING: - from typing import Optional + import fractions as frac + from typing import Optional, Union myint = int +myfrac = float #: docstring variable: myint @@ -22,17 +24,17 @@ def read(r: io.BytesIO) -> io.StringIO: """docstring""" -def sum(x: myint, y: myint) -> myint: +def sum(x: myfrac, y: myfrac) -> myfrac: """docstring""" return x + y @overload -def mult(x: myint, y: myint) -> myint: ... +def mult(x: int, y: int) -> int: ... @overload -def mult(x: float, y: float) -> float: ... +def mult(x: myfrac, y: myfrac) -> myfrac: ... def mult(x, y): @@ -44,7 +46,45 @@ class Foo: """docstring""" #: docstring - attr1: myint + attr1: Union[frac.Fraction, myint] # NoQA: UP007 def __init__(self): self.attr2: myint = None #: docstring + + def method1(self, x: Union[frac.Fraction, myfrac]) -> Union[frac.Fraction, myfrac]: # NoQA: UP007 + """docstring""" + return self.attr1 * x + + @overload + def method2(self, x: frac.Fraction) -> frac.Fraction: ... + + @overload + def method2(self, x: myfrac) -> myfrac: ... + + @overload + def method2( + self, + x: Union[frac.Fraction, myfrac], # NoQA: UP007 + ) -> Union[frac.Fraction, myfrac]: ... # NoQA: UP007 + + def method2(self, x): + """docstring""" + return self.attr2 * x + + +@overload +def prod(x: tuple[float, myfrac]) -> float: ... + + +@overload +def prod(x: tuple[frac.Fraction, myfrac]) -> frac.Fraction: ... + + +def prod(x): + """docstring""" + return x[0] * x[1] + + +def print_value(x: Union[frac.Fraction, myfrac]) -> None: # NoQA: UP007 + """docstring""" + print('value:', x) diff --git a/tests/test_extensions/test_ext_autodoc_configs.py b/tests/test_extensions/test_ext_autodoc_configs.py index ab7539190e0..b54f391ae6a 100644 --- a/tests/test_extensions/test_ext_autodoc_configs.py +++ b/tests/test_extensions/test_ext_autodoc_configs.py @@ -1361,7 +1361,7 @@ def test_autodoc_type_aliases(app: SphinxTestApp) -> None: '', ' .. py:attribute:: Foo.attr1', ' :module: target.autodoc_type_aliases', - ' :type: int', + ' :type: ~fractions.Fraction | int', '', ' docstring', '', @@ -1373,6 +1373,20 @@ def test_autodoc_type_aliases(app: SphinxTestApp) -> None: ' docstring', '', '', + ' .. py:method:: Foo.method1(x: ~fractions.Fraction | float) -> ~fractions.Fraction | float', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + ' .. py:method:: Foo.method2(x: ~fractions.Fraction) -> ~fractions.Fraction', + ' Foo.method2(x: float) -> float', + ' Foo.method2(x: ~fractions.Fraction | float) -> ~fractions.Fraction | float', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', '.. py:function:: mult(x: int, y: int) -> int', ' mult(x: float, y: float) -> float', ' :module: target.autodoc_type_aliases', @@ -1380,13 +1394,26 @@ def test_autodoc_type_aliases(app: SphinxTestApp) -> None: ' docstring', '', '', + '.. py:function:: print_value(x: ~fractions.Fraction | float) -> None', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + '.. py:function:: prod(x: tuple[float, float]) -> float', + ' prod(x: tuple[~fractions.Fraction, float]) -> ~fractions.Fraction', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', '.. py:function:: read(r: ~io.BytesIO) -> ~io.StringIO', ' :module: target.autodoc_type_aliases', '', ' docstring', '', '', - '.. py:function:: sum(x: int, y: int) -> int', + '.. py:function:: sum(x: float, y: float) -> float', ' :module: target.autodoc_type_aliases', '', ' docstring', @@ -1418,6 +1445,7 @@ def test_autodoc_type_aliases(app: SphinxTestApp) -> None: # define aliases app.config.autodoc_type_aliases = { 'myint': 'myint', + 'myfrac': 'my.module.myfrac', 'io.StringIO': 'my.module.StringIO', } actual = do_autodoc(app, 'module', 'target.autodoc_type_aliases', options) @@ -1434,7 +1462,7 @@ def test_autodoc_type_aliases(app: SphinxTestApp) -> None: '', ' .. py:attribute:: Foo.attr1', ' :module: target.autodoc_type_aliases', - ' :type: myint', + " :type: ~fractions.Fraction | TypeAliasForwardRef('myint')", '', ' docstring', '', @@ -1446,8 +1474,35 @@ def test_autodoc_type_aliases(app: SphinxTestApp) -> None: ' docstring', '', '', - '.. py:function:: mult(x: myint, y: myint) -> myint', - ' mult(x: float, y: float) -> float', + " .. py:method:: Foo.method1(x: ~fractions.Fraction | TypeAliasForwardRef('my.module.myfrac')) -> ~fractions.Fraction | TypeAliasForwardRef('my.module.myfrac')", + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + ' .. py:method:: Foo.method2(x: ~fractions.Fraction) -> ~fractions.Fraction', + ' Foo.method2(x: my.module.myfrac) -> my.module.myfrac', + " Foo.method2(x: ~fractions.Fraction | TypeAliasForwardRef('my.module.myfrac')) -> ~fractions.Fraction | TypeAliasForwardRef('my.module.myfrac')", + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + '.. py:function:: mult(x: int, y: int) -> int', + ' mult(x: my.module.myfrac, y: my.module.myfrac) -> my.module.myfrac', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + ".. py:function:: print_value(x: ~fractions.Fraction | TypeAliasForwardRef('my.module.myfrac')) -> None", + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + ".. py:function:: prod(x: tuple[float, TypeAliasForwardRef('my.module.myfrac')]) -> float", + " prod(x: tuple[~fractions.Fraction, TypeAliasForwardRef('my.module.myfrac')]) -> ~fractions.Fraction", ' :module: target.autodoc_type_aliases', '', ' docstring', @@ -1459,7 +1514,7 @@ def test_autodoc_type_aliases(app: SphinxTestApp) -> None: ' docstring', '', '', - '.. py:function:: sum(x: myint, y: myint) -> myint', + '.. py:function:: sum(x: my.module.myfrac, y: my.module.myfrac) -> my.module.myfrac', ' :module: target.autodoc_type_aliases', '', ' docstring', @@ -1495,7 +1550,7 @@ def test_autodoc_type_aliases(app: SphinxTestApp) -> None: srcdir='autodoc_typehints_description_and_type_aliases', confoverrides={ 'autodoc_typehints': 'description', - 'autodoc_type_aliases': {'myint': 'myint'}, + 'autodoc_type_aliases': {'myfrac': 'my.module.myfrac'}, }, ) def test_autodoc_typehints_description_and_type_aliases(app: SphinxTestApp) -> None: @@ -1511,12 +1566,12 @@ def test_autodoc_typehints_description_and_type_aliases(app: SphinxTestApp) -> N ' docstring\n' '\n' ' Parameters:\n' - ' * **x** (*myint*)\n' + ' * **x** (*my.module.myfrac*)\n' '\n' - ' * **y** (*myint*)\n' + ' * **y** (*my.module.myfrac*)\n' '\n' ' Return type:\n' - ' myint\n' + ' my.module.myfrac\n' )