From caa6579dbda5fd351b63f0786beffcd8a1cd9425 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 | 63 +++++++++++-------- .../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, 46 insertions(+), 25 deletions(-) diff --git a/CHANGES b/CHANGES index 5b7a840dc..8af4039f3 100644 --- a/CHANGES +++ b/CHANGES @@ -25,6 +25,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 6ccfd9c7f..fab0a3d74 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1328,12 +1328,12 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ if typ is object: pass # default implementation. skipped. else: - self.annotate_to_first_argument(func, typ) - - documenter = FunctionDocumenter(self.directive, '') - documenter.object = func - documenter.objpath = [None] - sigs.append(documenter.format_signature()) + dispatchfunc = self.annotate_to_first_argument(func, typ) + if dispatchfunc: + documenter = FunctionDocumenter(self.directive, '') + documenter.object = dispatchfunc + documenter.objpath = [None] + sigs.append(documenter.format_signature()) if overloaded: actual = inspect.signature(self.object, type_aliases=self.config.autodoc_type_aliases) @@ -1358,28 +1358,34 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ 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): @@ -2118,13 +2124,13 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: if typ is object: pass # default implementation. skipped. else: - self.annotate_to_first_argument(func, typ) - - documenter = MethodDocumenter(self.directive, '') - documenter.parent = self.parent - documenter.object = func - documenter.objpath = [None] - sigs.append(documenter.format_signature()) + dispatchmeth = self.annotate_to_first_argument(func, typ) + if dispatchmeth: + documenter = MethodDocumenter(self.directive, '') + documenter.parent = self.parent + documenter.object = dispatchmeth + documenter.objpath = [None] + sigs.append(documenter.format_signature()) if overloaded: if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): actual = inspect.signature(self.object, bound_method=False, @@ -2158,27 +2164,34 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: 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 3fa81dcae..fca2b6683 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 b5ccbb2f0..086c7fe66 100644 --- a/tests/roots/test-ext-autodoc/target/singledispatchmethod.py +++ b/tests/roots/test-ext-autodoc/target/singledispatchmethod.py @@ -10,6 +10,7 @@ class Foo: 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 2becf5de2..e7d5c4042 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 615091889..ca2429b5e 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',