Fix #8872: autodoc: stacked singledispatches are wrongly rendered

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.
This commit is contained in:
Takeshi KOMIYA 2021-05-02 21:00:26 +09:00
parent 05abdad749
commit caa6579dbd
6 changed files with 46 additions and 25 deletions

View File

@ -25,6 +25,8 @@ Features added
Bugs fixed Bugs fixed
---------- ----------
* #8872: autodoc: stacked singledispatches are wrongly rendered
Testing Testing
-------- --------

View File

@ -1328,10 +1328,10 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
if typ is object: if typ is object:
pass # default implementation. skipped. pass # default implementation. skipped.
else: else:
self.annotate_to_first_argument(func, typ) dispatchfunc = self.annotate_to_first_argument(func, typ)
if dispatchfunc:
documenter = FunctionDocumenter(self.directive, '') documenter = FunctionDocumenter(self.directive, '')
documenter.object = func documenter.object = dispatchfunc
documenter.objpath = [None] documenter.objpath = [None]
sigs.append(documenter.format_signature()) sigs.append(documenter.format_signature())
if overloaded: if overloaded:
@ -1358,28 +1358,34 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
return overload.replace(parameters=parameters) 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.""" """Annotate type hint to the first argument of function if needed."""
try: try:
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
except TypeError as exc: except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"), logger.warning(__("Failed to get a function signature for %s: %s"),
self.fullname, exc) self.fullname, exc)
return return None
except ValueError: except ValueError:
return return None
if len(sig.parameters) == 0: if len(sig.parameters) == 0:
return return None
def dummy():
pass
params = list(sig.parameters.values()) params = list(sig.parameters.values())
if params[0].annotation is Parameter.empty: if params[0].annotation is Parameter.empty:
params[0] = params[0].replace(annotation=typ) params[0] = params[0].replace(annotation=typ)
try: try:
func.__signature__ = sig.replace(parameters=params) # type: ignore dummy.__signature__ = sig.replace(parameters=params) # type: ignore
return dummy
except (AttributeError, TypeError): except (AttributeError, TypeError):
# failed to update signature (ex. built-in or extension types) # failed to update signature (ex. built-in or extension types)
return return None
else:
return None
class DecoratorDocumenter(FunctionDocumenter): class DecoratorDocumenter(FunctionDocumenter):
@ -2118,11 +2124,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
if typ is object: if typ is object:
pass # default implementation. skipped. pass # default implementation. skipped.
else: else:
self.annotate_to_first_argument(func, typ) dispatchmeth = self.annotate_to_first_argument(func, typ)
if dispatchmeth:
documenter = MethodDocumenter(self.directive, '') documenter = MethodDocumenter(self.directive, '')
documenter.parent = self.parent documenter.parent = self.parent
documenter.object = func documenter.object = dispatchmeth
documenter.objpath = [None] documenter.objpath = [None]
sigs.append(documenter.format_signature()) sigs.append(documenter.format_signature())
if overloaded: if overloaded:
@ -2158,27 +2164,34 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
return overload.replace(parameters=parameters) 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.""" """Annotate type hint to the first argument of function if needed."""
try: try:
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
except TypeError as exc: except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"), logger.warning(__("Failed to get a method signature for %s: %s"),
self.fullname, exc) self.fullname, exc)
return return None
except ValueError: except ValueError:
return return None
if len(sig.parameters) == 1: if len(sig.parameters) == 1:
return return None
def dummy():
pass
params = list(sig.parameters.values()) params = list(sig.parameters.values())
if params[1].annotation is Parameter.empty: if params[1].annotation is Parameter.empty:
params[1] = params[1].replace(annotation=typ) params[1] = params[1].replace(annotation=typ)
try: try:
func.__signature__ = sig.replace(parameters=params) # type: ignore dummy.__signature__ = sig.replace(parameters=params) # type: ignore
return dummy
except (AttributeError, TypeError): except (AttributeError, TypeError):
# failed to update signature (ex. built-in or extension types) # failed to update signature (ex. built-in or extension types)
return return None
else:
return None
class NonDataDescriptorMixin(DataDocumenterMixinBase): class NonDataDescriptorMixin(DataDocumenterMixinBase):

View File

@ -15,6 +15,7 @@ def func(arg, kwarg=None):
@func.register(int) @func.register(int)
@func.register(float)
def _func_int(arg, kwarg=None): def _func_int(arg, kwarg=None):
"""A function for int.""" """A function for int."""
pass pass

View File

@ -10,6 +10,7 @@ class Foo:
pass pass
@meth.register(int) @meth.register(int)
@meth.register(float)
def _meth_int(self, arg, kwarg=None): def _meth_int(self, arg, kwarg=None):
"""A method for int.""" """A method for int."""
pass pass

View File

@ -2080,6 +2080,7 @@ def test_singledispatch(app):
'', '',
'', '',
'.. py:function:: func(arg, kwarg=None)', '.. py:function:: func(arg, kwarg=None)',
' func(arg: float, kwarg=None)',
' func(arg: int, kwarg=None)', ' func(arg: int, kwarg=None)',
' func(arg: str, kwarg=None)', ' func(arg: str, kwarg=None)',
' :module: target.singledispatch', ' :module: target.singledispatch',
@ -2107,6 +2108,7 @@ def test_singledispatchmethod(app):
'', '',
'', '',
' .. py:method:: Foo.meth(arg, kwarg=None)', ' .. py:method:: Foo.meth(arg, kwarg=None)',
' Foo.meth(arg: float, kwarg=None)',
' Foo.meth(arg: int, kwarg=None)', ' Foo.meth(arg: int, kwarg=None)',
' Foo.meth(arg: str, kwarg=None)', ' Foo.meth(arg: str, kwarg=None)',
' :module: target.singledispatchmethod', ' :module: target.singledispatchmethod',
@ -2125,6 +2127,7 @@ def test_singledispatchmethod_automethod(app):
assert list(actual) == [ assert list(actual) == [
'', '',
'.. py:method:: Foo.meth(arg, kwarg=None)', '.. py:method:: Foo.meth(arg, kwarg=None)',
' Foo.meth(arg: float, kwarg=None)',
' Foo.meth(arg: int, kwarg=None)', ' Foo.meth(arg: int, kwarg=None)',
' Foo.meth(arg: str, kwarg=None)', ' Foo.meth(arg: str, kwarg=None)',
' :module: target.singledispatchmethod', ' :module: target.singledispatchmethod',

View File

@ -119,6 +119,7 @@ def test_singledispatch(app):
assert list(actual) == [ assert list(actual) == [
'', '',
'.. py:function:: func(arg, kwarg=None)', '.. py:function:: func(arg, kwarg=None)',
' func(arg: float, kwarg=None)',
' func(arg: int, kwarg=None)', ' func(arg: int, kwarg=None)',
' func(arg: str, kwarg=None)', ' func(arg: str, kwarg=None)',
' :module: target.singledispatch', ' :module: target.singledispatch',