Skip to content

Commit

Permalink
Fix sphinx-doc#8872: autodoc: stacked singledispatches are wrongly re…
Browse files Browse the repository at this point in the history
…ndered

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 2, 2021
1 parent 30237c0 commit 8d86d35
Show file tree
Hide file tree
Showing 6 changed files with 37 additions and 16 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Expand Up @@ -22,6 +22,8 @@ Features added
Bugs fixed
----------

* #8872: autodoc: stacked singledispatches are wrongly rendered

Testing
--------

Expand Down
45 changes: 29 additions & 16 deletions sphinx/ext/autodoc/__init__.py
Expand Up @@ -1320,10 +1320,10 @@ def format_signature(self, **kwargs: Any) -> str:
if typ is object:
pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)
dispatchfunc = self.annotate_to_first_argument(func, typ)

documenter = FunctionDocumenter(self.directive, '')
documenter.object = func
documenter.object = dispatchfunc
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
Expand All @@ -1350,28 +1350,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 @@ -2109,11 +2115,11 @@ def format_signature(self, **kwargs: Any) -> str:
if typ is object:
pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)
dispatchmeth = self.annotate_to_first_argument(func, typ)

documenter = MethodDocumenter(self.directive, '')
documenter.parent = self.parent
documenter.object = func
documenter.object = dispatchmeth
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
Expand Down Expand Up @@ -2149,27 +2155,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 8d86d35

Please sign in to comment.