From 90de551d4cfc206450cbd776cd1a14512ae3f0a4 Mon Sep 17 00:00:00 2001 From: "Terence D. Honles" Date: Thu, 27 Feb 2020 16:27:41 -0800 Subject: [PATCH 1/2] Fix: autodoc: `__wrapped__` functions are not documented correctly Functions that are decorated with `@lru_cache` or other `functools` decorators may not even be detected as a function. This results in the documentation not having the `()` or even trying to render the function signature. This change updates the `sphinx.util.inspect` code to unwrap `__wrapped__` functions before determining if they can be documented. `@lru_cache` and its associated test is an example of a decorated function that is incorrectly identified as an attribute rather than a module level function and when rendering the signature (upon changing `isattributedescriptor`) the decorated function is still incorrectly identified as a C function. This change also renames the newly introduced `unwrap` as `unwrap_all` because it is different than the prexisting Python supplied `inspect.unwrap`. See `update_wrapper` "Changed in version 3.4" for more background: https://docs.python.org/3/library/functools.html#functools.update_wrapper --- sphinx/ext/autodoc/__init__.py | 48 ++++++++++--------- sphinx/util/inspect.py | 39 +++++++++------ .../target/wrappedfunction.py | 8 ++++ tests/test_autodoc.py | 13 +++++ 4 files changed, 71 insertions(+), 37 deletions(-) create mode 100644 tests/roots/test-ext-autodoc/target/wrappedfunction.py diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 08e4e5301ee..e9c492413a8 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -976,39 +976,40 @@ 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): + unwrapped = inspect.unwrap(self.object) + if inspect.isbuiltin(unwrapped) or inspect.ismethoddescriptor(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.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.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) # escape backslashes for reST @@ -1337,15 +1338,16 @@ 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): + unwrapped = inspect.unwrap(self.object) + if inspect.isbuiltin(unwrapped) or inspect.ismethoddescriptor(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) # escape backslashes for reST diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 2bcccdd6051..bce5c33ea34 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 - isclass, ismethod, ismethoddescriptor, isroutine + isclass, ismethod, ismethoddescriptor, unwrap ) from io import StringIO from typing import Any, Callable, Mapping, List, Tuple @@ -111,11 +111,16 @@ def getargspec(func): 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): @@ -194,23 +199,24 @@ def isabstractmethod(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 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: @@ -221,17 +227,22 @@ def isattributedescriptor(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 a86211f18b7..cb93b47e1a6 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1369,6 +1369,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 = [ From 4ba1243ea2c31558fc8b49ca8397935d72d9f596 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 28 Mar 2020 23:14:55 +0900 Subject: [PATCH 2/2] Update CHANGES for PR #7222 --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index 6778d000a64..3210b8a53f2 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,8 @@ Dependencies Incompatible changes -------------------- +* #7222: ``sphinx.util.inspect.unwrap()`` is renamed to ``unwrap_all()`` + Deprecated ---------- @@ -17,6 +19,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 --------