From 8d86d358a721996bd47c281dcf8dce06b11d988a Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 2 May 2021 21:00:26 +0900 Subject: [PATCH] Fix #8872: autodoc: stacked singledispatches are wrongly rendered When multiple singledispatch decorators are stacked, the first typehints are copied to the subsequent definitions unexpectedly. Now autodoc generates a dummy function not to affect typehints to subsequent functions. --- CHANGES | 2 + sphinx/ext/autodoc/__init__.py | 45 ++++++++++++------- .../test-ext-autodoc/target/singledispatch.py | 1 + .../target/singledispatchmethod.py | 1 + tests/test_ext_autodoc.py | 3 ++ tests/test_ext_autodoc_autofunction.py | 1 + 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/CHANGES b/CHANGES index 7028c9e6860..0ee39d2caa2 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,8 @@ Features added Bugs fixed ---------- +* #8872: autodoc: stacked singledispatches are wrongly rendered + Testing -------- diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index c92709deb93..ade52fef904 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1320,10 +1320,10 @@ def format_signature(self, **kwargs: Any) -> str: if typ is object: pass # default implementation. skipped. else: - self.annotate_to_first_argument(func, typ) + dispatchfunc = self.annotate_to_first_argument(func, typ) documenter = FunctionDocumenter(self.directive, '') - documenter.object = func + documenter.object = dispatchfunc documenter.objpath = [None] sigs.append(documenter.format_signature()) if overloaded: @@ -1350,28 +1350,34 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu return overload.replace(parameters=parameters) - def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: + def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]: """Annotate type hint to the first argument of function if needed.""" try: sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) except TypeError as exc: logger.warning(__("Failed to get a function signature for %s: %s"), self.fullname, exc) - return + return None except ValueError: - return + return None if len(sig.parameters) == 0: - return + return None + + def dummy(): + pass params = list(sig.parameters.values()) if params[0].annotation is Parameter.empty: params[0] = params[0].replace(annotation=typ) try: - func.__signature__ = sig.replace(parameters=params) # type: ignore + dummy.__signature__ = sig.replace(parameters=params) # type: ignore + return dummy except (AttributeError, TypeError): # failed to update signature (ex. built-in or extension types) - return + return None + else: + return None class DecoratorDocumenter(FunctionDocumenter): @@ -2109,11 +2115,11 @@ def format_signature(self, **kwargs: Any) -> str: if typ is object: pass # default implementation. skipped. else: - self.annotate_to_first_argument(func, typ) + dispatchmeth = self.annotate_to_first_argument(func, typ) documenter = MethodDocumenter(self.directive, '') documenter.parent = self.parent - documenter.object = func + documenter.object = dispatchmeth documenter.objpath = [None] sigs.append(documenter.format_signature()) if overloaded: @@ -2149,27 +2155,34 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu return overload.replace(parameters=parameters) - def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: + def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]: """Annotate type hint to the first argument of function if needed.""" try: sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) except TypeError as exc: logger.warning(__("Failed to get a method signature for %s: %s"), self.fullname, exc) - return + return None except ValueError: - return + return None + if len(sig.parameters) == 1: - return + return None + + def dummy(): + pass params = list(sig.parameters.values()) if params[1].annotation is Parameter.empty: params[1] = params[1].replace(annotation=typ) try: - func.__signature__ = sig.replace(parameters=params) # type: ignore + dummy.__signature__ = sig.replace(parameters=params) # type: ignore + return dummy except (AttributeError, TypeError): # failed to update signature (ex. built-in or extension types) - return + return None + else: + return None class NonDataDescriptorMixin(DataDocumenterMixinBase): diff --git a/tests/roots/test-ext-autodoc/target/singledispatch.py b/tests/roots/test-ext-autodoc/target/singledispatch.py index 3fa81dcae4c..fca2b668381 100644 --- a/tests/roots/test-ext-autodoc/target/singledispatch.py +++ b/tests/roots/test-ext-autodoc/target/singledispatch.py @@ -15,6 +15,7 @@ def func(arg, kwarg=None): @func.register(int) +@func.register(float) def _func_int(arg, kwarg=None): """A function for int.""" pass diff --git a/tests/roots/test-ext-autodoc/target/singledispatchmethod.py b/tests/roots/test-ext-autodoc/target/singledispatchmethod.py index b5ccbb2f09d..086c7fe668d 100644 --- a/tests/roots/test-ext-autodoc/target/singledispatchmethod.py +++ b/tests/roots/test-ext-autodoc/target/singledispatchmethod.py @@ -10,6 +10,7 @@ def meth(self, arg, kwarg=None): pass @meth.register(int) + @meth.register(float) def _meth_int(self, arg, kwarg=None): """A method for int.""" pass diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 2becf5de2ad..e7d5c4042ad 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -2080,6 +2080,7 @@ def test_singledispatch(app): '', '', '.. py:function:: func(arg, kwarg=None)', + ' func(arg: float, kwarg=None)', ' func(arg: int, kwarg=None)', ' func(arg: str, kwarg=None)', ' :module: target.singledispatch', @@ -2107,6 +2108,7 @@ def test_singledispatchmethod(app): '', '', ' .. py:method:: Foo.meth(arg, kwarg=None)', + ' Foo.meth(arg: float, kwarg=None)', ' Foo.meth(arg: int, kwarg=None)', ' Foo.meth(arg: str, kwarg=None)', ' :module: target.singledispatchmethod', @@ -2125,6 +2127,7 @@ def test_singledispatchmethod_automethod(app): assert list(actual) == [ '', '.. py:method:: Foo.meth(arg, kwarg=None)', + ' Foo.meth(arg: float, kwarg=None)', ' Foo.meth(arg: int, kwarg=None)', ' Foo.meth(arg: str, kwarg=None)', ' :module: target.singledispatchmethod', diff --git a/tests/test_ext_autodoc_autofunction.py b/tests/test_ext_autodoc_autofunction.py index 6150918894c..ca2429b5e88 100644 --- a/tests/test_ext_autodoc_autofunction.py +++ b/tests/test_ext_autodoc_autofunction.py @@ -119,6 +119,7 @@ def test_singledispatch(app): assert list(actual) == [ '', '.. py:function:: func(arg, kwarg=None)', + ' func(arg: float, kwarg=None)', ' func(arg: int, kwarg=None)', ' func(arg: str, kwarg=None)', ' :module: target.singledispatch',