diff --git a/CHANGES b/CHANGES index 07d56016c98..0fef20d7151 100644 --- a/CHANGES +++ b/CHANGES @@ -88,6 +88,7 @@ Bugs fixed autodoc_typehints='description' mode * #7551: autodoc: failed to import nested class * #7362: autodoc: does not render correct signatures for built-in functions +* #7650: autodoc: undecorated signature is shown for decorated functions * #7551: autosummary: a nested class is indexed as non-nested class * #7535: sphinx-autogen: crashes when custom template uses inheritance * #7536: sphinx-autogen: crashes when template uses i18n feature diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 80bcc0e8d53..952a0e71b4e 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1051,10 +1051,12 @@ def format_args(self, **kwargs: Any) -> str: if self.env.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) - unwrapped = inspect.unwrap(self.object) try: - self.env.app.emit('autodoc-before-process-signature', unwrapped, False) - sig = inspect.signature(unwrapped) + self.env.app.emit('autodoc-before-process-signature', self.object, False) + if inspect.is_singledispatch_function(self.object): + sig = inspect.signature(self.object, follow_wrapped=True) + else: + sig = inspect.signature(self.object) args = stringify_signature(sig, **kwargs) except TypeError: args = '' @@ -1447,7 +1449,6 @@ def format_args(self, **kwargs: Any) -> str: if self.env.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) - unwrapped = inspect.unwrap(self.object) try: if self.object == object.__init__ and self.parent != object: # Classes not having own __init__() method are shown as no arguments. @@ -1456,12 +1457,18 @@ def format_args(self, **kwargs: Any) -> str: # But it makes users confused. args = '()' else: - if inspect.isstaticmethod(unwrapped, cls=self.parent, name=self.object_name): - self.env.app.emit('autodoc-before-process-signature', unwrapped, False) - sig = inspect.signature(unwrapped, bound_method=False) + if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): + self.env.app.emit('autodoc-before-process-signature', self.object, False) + sig = inspect.signature(self.object, bound_method=False) else: - self.env.app.emit('autodoc-before-process-signature', unwrapped, True) - sig = inspect.signature(unwrapped, bound_method=True) + self.env.app.emit('autodoc-before-process-signature', self.object, True) + + meth = self.parent.__dict__.get(self.objpath[-1], None) + if meth and inspect.is_singledispatch_method(meth): + sig = inspect.signature(self.object, bound_method=True, + follow_wrapped=True) + else: + sig = inspect.signature(self.object, bound_method=True) args = stringify_signature(sig, **kwargs) except ValueError: args = '' @@ -1514,7 +1521,9 @@ def add_singledispatch_directive_header(self, sig: str) -> None: self.annotate_to_first_argument(func, typ) documenter = MethodDocumenter(self.directive, '') + documenter.parent = self.parent documenter.object = func + documenter.objpath = self.objpath self.add_line(' %s%s' % (self.format_name(), documenter.format_signature()), sourcename) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 42defa6f8d4..6ba698eed94 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -408,13 +408,20 @@ def is_builtin_class_method(obj: Any, attr_name: str) -> bool: return getattr(builtins, name, None) is cls -def signature(subject: Callable, bound_method: bool = False) -> inspect.Signature: +def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False + ) -> inspect.Signature: """Return a Signature object for the given *subject*. :param bound_method: Specify *subject* is a bound method or not + :param follow_wrapped: Same as ``inspect.signature()``. + Defaults to ``False`` (get a signature of *subject*). """ try: - signature = inspect.signature(subject) + try: + signature = inspect.signature(subject, follow_wrapped=follow_wrapped) + except ValueError: + # follow built-in wrappers up (ex. functools.lru_cache) + signature = inspect.signature(subject) parameters = list(signature.parameters.values()) return_annotation = signature.return_annotation except IndexError: diff --git a/tests/roots/test-ext-autodoc/target/decorator.py b/tests/roots/test-ext-autodoc/target/decorator.py index 4ccfedf28cd..61398b324e3 100644 --- a/tests/roots/test-ext-autodoc/target/decorator.py +++ b/tests/roots/test-ext-autodoc/target/decorator.py @@ -1,5 +1,9 @@ +from functools import wraps + + def deco1(func): """docstring for deco1""" + @wraps(func) def wrapper(): return func() @@ -14,3 +18,14 @@ def wrapper(): return wrapper return decorator + + +@deco1 +def foo(name=None, age=None): + pass + + +class Bar: + @deco1 + def meth(self, name=None, age=None): + pass diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 76b970dbb2f..f56b84f06c6 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -142,6 +142,7 @@ def formatsig(objtype, name, obj, args, retann): inst = app.registry.documenters[objtype](directive, name) inst.fullname = name inst.doc_as_attr = False # for class objtype + inst.parent = object # dummy inst.object = obj inst.objpath = [name] inst.args = args @@ -1243,6 +1244,17 @@ def test_autofunction_for_methoddescriptor(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autofunction_for_decorated(app): + actual = do_autodoc(app, 'function', 'target.decorator.foo') + assert list(actual) == [ + '', + '.. py:function:: foo()', + ' :module: target.decorator', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_automethod_for_builtin(app): actual = do_autodoc(app, 'method', 'builtins.int.__add__') @@ -1256,6 +1268,17 @@ def test_automethod_for_builtin(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_automethod_for_decorated(app): + actual = do_autodoc(app, 'method', 'target.decorator.Bar.meth') + assert list(actual) == [ + '', + '.. py:method:: Bar.meth()', + ' :module: target.decorator', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_abstractmethods(app): options = {"members": None, @@ -1415,7 +1438,7 @@ def test_coroutine(app): actual = do_autodoc(app, 'function', 'target.coroutine.sync_func') assert list(actual) == [ '', - '.. py:function:: sync_func()', + '.. py:function:: sync_func(*args, **kwargs)', ' :module: target.coroutine', '', ] diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 1e8f8bd2472..ff51dc68ef6 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -97,7 +97,7 @@ def wrapped_bound_method(*args, **kwargs): # wrapped bound method sig = inspect.signature(wrapped_bound_method) - assert stringify_signature(sig) == '(arg1, **kwargs)' + assert stringify_signature(sig) == '(*args, **kwargs)' def test_signature_partialmethod():