autodoc: Support singledispatch methods

This commit is contained in:
Takeshi KOMIYA 2020-03-01 22:15:10 +09:00
parent 961b4d1545
commit 8f7cc26b20
5 changed files with 118 additions and 1 deletions

View File

@ -58,7 +58,7 @@ Features added
* #6830: autodoc: consider a member private if docstring contains
``:meta private:`` in info-field-list
* #7165: autodoc: Support Annotated type (PEP-593)
* #2815: autodoc: Support singledispatch functions
* #2815: autodoc: Support singledispatch functions and methods
* #6558: glossary: emit a warning for duplicated glossary entry
* #3106: domain: Register hyperlink target for index page automatically
* #6558: std domain: emit a warning for duplicated generic objects

View File

@ -1457,6 +1457,66 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
pass
class SingledispatchMethodDocumenter(MethodDocumenter):
"""
Specialized Documenter subclass for singledispatch'ed methods.
"""
objtype = 'singledispatch_method'
directivetype = 'method'
member_order = 50
# before MethodDocumenter
priority = MethodDocumenter.priority + 1
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
if super().can_document_member(member, membername, isattr, parent) and parent.object:
meth = parent.object.__dict__.get(membername)
return inspect.is_singledispatch_method(meth)
else:
return False
def add_directive_header(self, sig: str) -> None:
sourcename = self.get_sourcename()
# intercept generated directive headers
# TODO: It is very hacky to use mock to intercept header generation
with patch.object(self, 'add_line') as add_line:
super().add_directive_header(sig)
# output first line of header
self.add_line(*add_line.call_args_list[0][0])
# inserts signature of singledispatch'ed functions
meth = self.parent.__dict__.get(self.objpath[-1])
for typ, func in meth.dispatcher.registry.items():
if typ is object:
pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)
documenter = MethodDocumenter(self.directive, '')
documenter.object = func
self.add_line(' %s%s' % (self.format_name(),
documenter.format_signature()),
sourcename)
# output remains of directive header
for call in add_line.call_args_list[1:]:
self.add_line(*call[0])
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
"""Annotate type hint to the first argument of function if needed."""
sig = inspect.signature(func, bound_method=True)
if len(sig.parameters) == 0:
return
name = list(sig.parameters)[0]
if name not in func.__annotations__:
func.__annotations__[name] = typ
class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore
"""
Specialized Documenter subclass for attributes.
@ -1672,6 +1732,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_autodocumenter(SingledispatchFunctionDocumenter)
app.add_autodocumenter(DecoratorDocumenter)
app.add_autodocumenter(MethodDocumenter)
app.add_autodocumenter(SingledispatchMethodDocumenter)
app.add_autodocumenter(AttributeDocumenter)
app.add_autodocumenter(PropertyDocumenter)
app.add_autodocumenter(InstanceAttributeDocumenter)

View File

@ -235,6 +235,15 @@ def is_singledispatch_function(obj: Any) -> bool:
return False
def is_singledispatch_method(obj: Any) -> bool:
"""Check if the object is singledispatch method."""
try:
from functools import singledispatchmethod # type: ignore
return isinstance(obj, singledispatchmethod)
except ImportError: # py35-37
return False
def isfunction(obj: Any) -> bool:
"""Check if the object is function."""
return inspect.isfunction(unwrap(obj))

View File

@ -0,0 +1,20 @@
from functools import singledispatchmethod
class Foo:
"""docstring"""
@singledispatchmethod
def meth(self, arg, kwarg=None):
"""A method for general use."""
pass
@meth.register(int)
def _meth_int(self, arg, kwarg=None):
"""A method for int."""
pass
@meth.register(str)
def _meth_str(self, arg, kwarg=None):
"""A method for str."""
pass

View File

@ -1582,3 +1582,30 @@ def test_singledispatch():
' A function for general use.',
' '
]
@pytest.mark.skipif(sys.version_info < (3, 8),
reason='singledispatchmethod is available since python3.8')
@pytest.mark.usefixtures('setup_test')
def test_singledispatchmethod():
options = {"members": None}
actual = do_autodoc(app, 'module', 'target.singledispatchmethod', options)
assert list(actual) == [
'',
'.. py:module:: target.singledispatchmethod',
'',
'',
'.. py:class:: Foo',
' :module: target.singledispatchmethod',
'',
' docstring',
' ',
' ',
' .. py:method:: Foo.meth(arg, kwarg=None)',
' Foo.meth(arg: int, kwarg=None)',
' Foo.meth(arg: str, kwarg=None)',
' :module: target.singledispatchmethod',
' ',
' A method for general use.',
' '
]