Skip to content

Commit

Permalink
Fix #8872: autodoc: stacked singledispatches are wrongly rendered
Browse files Browse the repository at this point in the history
When multiple singledispatch decorators are stacked, the first typehints
are copied to the subsequent definitions unexpectedly.

Now autodoc generates a dummy function not to affect typehints to
subsequent functions.
  • Loading branch information
tk0miya committed May 3, 2021
1 parent 05abdad commit caa6579
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 25 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Expand Up @@ -25,6 +25,8 @@ Features added
Bugs fixed
----------

* #8872: autodoc: stacked singledispatches are wrongly rendered

Testing
--------

Expand Down
63 changes: 38 additions & 25 deletions sphinx/ext/autodoc/__init__.py
Expand Up @@ -1328,12 +1328,12 @@ def format_signature(self, **kwargs: Any) -> str:
if typ is object:
pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)

documenter = FunctionDocumenter(self.directive, '')
documenter.object = func
documenter.objpath = [None]
sigs.append(documenter.format_signature())
dispatchfunc = self.annotate_to_first_argument(func, typ)
if dispatchfunc:
documenter = FunctionDocumenter(self.directive, '')
documenter.object = dispatchfunc
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
actual = inspect.signature(self.object,
type_aliases=self.config.autodoc_type_aliases)
Expand All @@ -1358,28 +1358,34 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu

return overload.replace(parameters=parameters)

def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]:
"""Annotate type hint to the first argument of function if needed."""
try:
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"),
self.fullname, exc)
return
return None
except ValueError:
return
return None

if len(sig.parameters) == 0:
return
return None

def dummy():
pass

params = list(sig.parameters.values())
if params[0].annotation is Parameter.empty:
params[0] = params[0].replace(annotation=typ)
try:
func.__signature__ = sig.replace(parameters=params) # type: ignore
dummy.__signature__ = sig.replace(parameters=params) # type: ignore
return dummy
except (AttributeError, TypeError):
# failed to update signature (ex. built-in or extension types)
return
return None
else:
return None


class DecoratorDocumenter(FunctionDocumenter):
Expand Down Expand Up @@ -2118,13 +2124,13 @@ def format_signature(self, **kwargs: Any) -> str:
if typ is object:
pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)

documenter = MethodDocumenter(self.directive, '')
documenter.parent = self.parent
documenter.object = func
documenter.objpath = [None]
sigs.append(documenter.format_signature())
dispatchmeth = self.annotate_to_first_argument(func, typ)
if dispatchmeth:
documenter = MethodDocumenter(self.directive, '')
documenter.parent = self.parent
documenter.object = dispatchmeth
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
actual = inspect.signature(self.object, bound_method=False,
Expand Down Expand Up @@ -2158,27 +2164,34 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu

return overload.replace(parameters=parameters)

def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]:
"""Annotate type hint to the first argument of function if needed."""
try:
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"),
self.fullname, exc)
return
return None
except ValueError:
return
return None

if len(sig.parameters) == 1:
return
return None

def dummy():
pass

params = list(sig.parameters.values())
if params[1].annotation is Parameter.empty:
params[1] = params[1].replace(annotation=typ)
try:
func.__signature__ = sig.replace(parameters=params) # type: ignore
dummy.__signature__ = sig.replace(parameters=params) # type: ignore
return dummy
except (AttributeError, TypeError):
# failed to update signature (ex. built-in or extension types)
return
return None
else:
return None


class NonDataDescriptorMixin(DataDocumenterMixinBase):
Expand Down
1 change: 1 addition & 0 deletions tests/roots/test-ext-autodoc/target/singledispatch.py
Expand Up @@ -15,6 +15,7 @@ def func(arg, kwarg=None):


@func.register(int)
@func.register(float)
def _func_int(arg, kwarg=None):
"""A function for int."""
pass
Expand Down
Expand Up @@ -10,6 +10,7 @@ def meth(self, arg, kwarg=None):
pass

@meth.register(int)
@meth.register(float)
def _meth_int(self, arg, kwarg=None):
"""A method for int."""
pass
Expand Down
3 changes: 3 additions & 0 deletions tests/test_ext_autodoc.py
Expand Up @@ -2080,6 +2080,7 @@ def test_singledispatch(app):
'',
'',
'.. py:function:: func(arg, kwarg=None)',
' func(arg: float, kwarg=None)',
' func(arg: int, kwarg=None)',
' func(arg: str, kwarg=None)',
' :module: target.singledispatch',
Expand Down Expand Up @@ -2107,6 +2108,7 @@ def test_singledispatchmethod(app):
'',
'',
' .. py:method:: Foo.meth(arg, kwarg=None)',
' Foo.meth(arg: float, kwarg=None)',
' Foo.meth(arg: int, kwarg=None)',
' Foo.meth(arg: str, kwarg=None)',
' :module: target.singledispatchmethod',
Expand All @@ -2125,6 +2127,7 @@ def test_singledispatchmethod_automethod(app):
assert list(actual) == [
'',
'.. py:method:: Foo.meth(arg, kwarg=None)',
' Foo.meth(arg: float, kwarg=None)',
' Foo.meth(arg: int, kwarg=None)',
' Foo.meth(arg: str, kwarg=None)',
' :module: target.singledispatchmethod',
Expand Down
1 change: 1 addition & 0 deletions tests/test_ext_autodoc_autofunction.py
Expand Up @@ -119,6 +119,7 @@ def test_singledispatch(app):
assert list(actual) == [
'',
'.. py:function:: func(arg, kwarg=None)',
' func(arg: float, kwarg=None)',
' func(arg: int, kwarg=None)',
' func(arg: str, kwarg=None)',
' :module: target.singledispatch',
Expand Down

0 comments on commit caa6579

Please sign in to comment.