diff --git a/CHANGES b/CHANGES index 12d5d8f3da8..53b2b57cae2 100644 --- a/CHANGES +++ b/CHANGES @@ -199,6 +199,8 @@ Dependencies Incompatible changes -------------------- +* #7222: ``sphinx.util.inspect.unwrap()`` is renamed to ``unwrap_all()`` + Deprecated ---------- @@ -209,6 +211,7 @@ Bugs fixed ---------- * #7343: Sphinx builds has been slower since 2.4.0 on debug mode +* #7222: autodoc: ``__wrapped__`` functions are not documented correctly Testing -------- diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 041ebc20b56..ad3e5f00add 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1011,41 +1011,42 @@ def format_args(self, **kwargs: Any) -> str: if self.env.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) - if ((inspect.isbuiltin(self.object) or inspect.ismethoddescriptor(self.object)) and - not inspect.is_cython_function_or_method(self.object)): + unwrapped = inspect.unwrap(self.object) + if ((inspect.isbuiltin(unwrapped) or inspect.ismethoddescriptor(unwrapped)) and + not inspect.is_cython_function_or_method(unwrapped)): # cannot introspect arguments of a C function or method return None try: - if (not inspect.isfunction(self.object) and - not inspect.ismethod(self.object) and - not inspect.isbuiltin(self.object) and - not inspect.is_cython_function_or_method(self.object) and - not inspect.isclass(self.object) and - hasattr(self.object, '__call__')): + if (not inspect.isfunction(unwrapped) and + not inspect.ismethod(unwrapped) and + not inspect.isbuiltin(unwrapped) and + not inspect.is_cython_function_or_method(unwrapped) and + not inspect.isclass(unwrapped) and + hasattr(unwrapped, '__call__')): self.env.app.emit('autodoc-before-process-signature', - self.object.__call__, False) - sig = inspect.signature(self.object.__call__) + unwrapped.__call__, False) + sig = inspect.signature(unwrapped.__call__) else: - self.env.app.emit('autodoc-before-process-signature', self.object, False) - sig = inspect.signature(self.object) + self.env.app.emit('autodoc-before-process-signature', unwrapped, False) + sig = inspect.signature(unwrapped) args = stringify_signature(sig, **kwargs) except TypeError: - if (inspect.is_builtin_class_method(self.object, '__new__') and - inspect.is_builtin_class_method(self.object, '__init__')): - raise TypeError('%r is a builtin class' % self.object) + if (inspect.is_builtin_class_method(unwrapped, '__new__') and + inspect.is_builtin_class_method(unwrapped, '__init__')): + raise TypeError('%r is a builtin class' % unwrapped) # if a class should be documented as function (yay duck # typing) we try to use the constructor signature as function # signature without the first argument. try: self.env.app.emit('autodoc-before-process-signature', - self.object.__new__, True) - sig = inspect.signature(self.object.__new__, bound_method=True) + unwrapped.__new__, True) + sig = inspect.signature(unwrapped.__new__, bound_method=True) args = stringify_signature(sig, show_return_annotation=False, **kwargs) except TypeError: self.env.app.emit('autodoc-before-process-signature', - self.object.__init__, True) - sig = inspect.signature(self.object.__init__, bound_method=True) + unwrapped.__init__, True) + sig = inspect.signature(unwrapped.__init__, bound_method=True) args = stringify_signature(sig, show_return_annotation=False, **kwargs) if self.env.config.strip_signature_backslash: @@ -1432,16 +1433,17 @@ def format_args(self, **kwargs: Any) -> str: if self.env.config.autodoc_typehints == 'none': kwargs.setdefault('show_annotation', False) - if ((inspect.isbuiltin(self.object) or inspect.ismethoddescriptor(self.object)) and - not inspect.is_cython_function_or_method(self.object)): + unwrapped = inspect.unwrap(self.object) + if ((inspect.isbuiltin(unwrapped) or inspect.ismethoddescriptor(unwrapped)) and + not inspect.is_cython_function_or_method(unwrapped)): # can never get arguments of a C function or method return None - 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) + 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) else: - self.env.app.emit('autodoc-before-process-signature', self.object, True) - sig = inspect.signature(self.object, bound_method=True) + self.env.app.emit('autodoc-before-process-signature', unwrapped, True) + sig = inspect.signature(unwrapped, bound_method=True) args = stringify_signature(sig, **kwargs) if self.env.config.strip_signature_backslash: diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 855b11d832e..53229d252b9 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -17,7 +17,7 @@ import warnings from functools import partial, partialmethod from inspect import ( # NOQA - Parameter, isclass, ismethod, ismethoddescriptor, isroutine + Parameter, isclass, ismethod, ismethoddescriptor, isroutine, unwrap ) from io import StringIO from typing import Any, Callable, Mapping, List, Tuple @@ -116,11 +116,16 @@ def getargspec(func: Callable) -> Any: kwonlyargs, kwdefaults, annotations) -def unwrap(obj: Any) -> Any: - """Get an original object from wrapped object.""" +def unwrap_all(obj: Any) -> Any: + """ + Get an original object from wrapped object (unwrapping partials, wrapped + functions, and other decorators). + """ while True: if ispartial(obj): - obj = unpartial(obj) + obj = obj.func + elif inspect.isroutine(obj) and hasattr(obj, '__wrapped__'): + obj = obj.__wrapped__ elif isclassmethod(obj): obj = obj.__func__ elif isstaticmethod(obj): @@ -207,26 +212,27 @@ def is_cython_function_or_method(obj: Any) -> bool: def isattributedescriptor(obj: Any) -> bool: """Check if the object is an attribute like descriptor.""" - if inspect.isdatadescriptor(object): + if inspect.isdatadescriptor(obj): # data descriptor is kind of attribute return True elif isdescriptor(obj): # non data descriptor - if isfunction(obj) or isbuiltin(obj) or inspect.ismethod(obj): + unwrapped = inspect.unwrap(obj) + if isfunction(unwrapped) or isbuiltin(unwrapped) or inspect.ismethod(unwrapped): # attribute must not be either function, builtin and method return False - elif is_cython_function_or_method(obj): + elif is_cython_function_or_method(unwrapped): # attribute must not be either function and method (for cython) return False - elif inspect.isclass(obj): + elif inspect.isclass(unwrapped): # attribute must not be a class return False - elif isinstance(obj, (ClassMethodDescriptorType, - MethodDescriptorType, - WrapperDescriptorType)): + elif isinstance(unwrapped, (ClassMethodDescriptorType, + MethodDescriptorType, + WrapperDescriptorType)): # attribute must not be a method descriptor return False - elif type(obj).__name__ == "instancemethod": + elif type(unwrapped).__name__ == "instancemethod": # attribute must not be an instancemethod (C-API) return False else: @@ -257,17 +263,22 @@ def is_singledispatch_method(obj: Any) -> bool: def isfunction(obj: Any) -> bool: """Check if the object is function.""" - return inspect.isfunction(unwrap(obj)) + return inspect.isfunction(unwrap_all(obj)) def isbuiltin(obj: Any) -> bool: """Check if the object is builtin.""" - return inspect.isbuiltin(unwrap(obj)) + return inspect.isbuiltin(unwrap_all(obj)) + + +def isroutine(obj: Any) -> bool: + """Check is any kind of function or method.""" + return inspect.isroutine(unwrap_all(obj)) def iscoroutinefunction(obj: Any) -> bool: """Check if the object is coroutine-function.""" - obj = unwrap(obj) + obj = unwrap_all(obj) if hasattr(obj, '__code__') and inspect.iscoroutinefunction(obj): # check obj.__code__ because iscoroutinefunction() crashes for custom method-like # objects (see https://github.com/sphinx-doc/sphinx/issues/6605) diff --git a/tests/roots/test-ext-autodoc/target/wrappedfunction.py b/tests/roots/test-ext-autodoc/target/wrappedfunction.py new file mode 100644 index 00000000000..ea872f08682 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/wrappedfunction.py @@ -0,0 +1,8 @@ +# for py32 or above +from functools import lru_cache + + +@lru_cache(maxsize=None) +def slow_function(message, timeout): + """This function is slow.""" + print(message) diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index fc56a7f7237..11c3503bee7 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1413,6 +1413,19 @@ def test_partialmethod(app): assert list(actual) == expected +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_wrappedfunction(app): + actual = do_autodoc(app, 'function', 'target.wrappedfunction.slow_function') + assert list(actual) == [ + '', + '.. py:function:: slow_function(message, timeout)', + ' :module: target.wrappedfunction', + '', + ' This function is slow.', + ' ', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_partialmethod_undoc_members(app): expected = [