Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 25 additions & 23 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
39 changes: 25 additions & 14 deletions sphinx/util/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions tests/roots/test-ext-autodoc/target/wrappedfunction.py
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 13 additions & 0 deletions tests/test_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down