Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #7650: autodoc: undecorated signature is shown for decorated functions #7651

Merged
merged 2 commits into from
May 16, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Bugs fixed
``Union[foo, bar, None]``
* #7629: autodoc: autofunction emits an unfriendly warning if an invalid object
specified
* #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
Expand Down
27 changes: 18 additions & 9 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 as exc:
logger.warning(__("Failed to get a function signature for %s: %s"),
Expand Down Expand Up @@ -1451,7 +1453,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.
Expand All @@ -1460,12 +1461,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 TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"),
Expand Down Expand Up @@ -1522,7 +1529,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)
Expand Down
11 changes: 9 additions & 2 deletions sphinx/util/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions tests/roots/test-ext-autodoc/target/decorator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from functools import wraps


def deco1(func):
"""docstring for deco1"""
@wraps(func)
def wrapper():
return func()

Expand All @@ -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
25 changes: 24 additions & 1 deletion tests/test_ext_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__')
Expand All @@ -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,
Expand Down Expand Up @@ -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',
'',
]
Expand Down
2 changes: 1 addition & 1 deletion tests/test_util_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down