Merge pull request #7677 from tk0miya/refactor_singledispatch

refactor: autodoc: Remove magic mock from singledispatch processing
This commit is contained in:
Takeshi KOMIYA
2020-05-16 23:05:13 +09:00
committed by GitHub
2 changed files with 50 additions and 70 deletions

View File

@@ -16,7 +16,6 @@ import warnings
from inspect import Parameter from inspect import Parameter
from types import ModuleType from types import ModuleType
from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Tuple, Type, Union from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Tuple, Type, Union
from unittest.mock import patch
from docutils.statemachine import StringList from docutils.statemachine import StringList
@@ -421,8 +420,15 @@ class Documenter:
directive = getattr(self, 'directivetype', self.objtype) directive = getattr(self, 'directivetype', self.objtype)
name = self.format_name() name = self.format_name()
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
self.add_line('.. %s:%s:: %s%s' % (domain, directive, name, sig),
sourcename) # one signature per line, indented by column
prefix = '.. %s:%s:: ' % (domain, directive)
for i, sig_line in enumerate(sig.split("\n")):
self.add_line('%s%s%s' % (prefix, name, sig_line),
sourcename)
if i == 0:
prefix = " " * len(prefix)
if self.options.noindex: if self.options.noindex:
self.add_line(' :noindex:', sourcename) self.add_line(' :noindex:', sourcename)
if self.objpath: if self.objpath:
@@ -1075,41 +1081,28 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
def add_directive_header(self, sig: str) -> None: def add_directive_header(self, sig: str) -> None:
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
if inspect.is_singledispatch_function(self.object): super().add_directive_header(sig)
self.add_singledispatch_directive_header(sig)
else:
super().add_directive_header(sig)
if inspect.iscoroutinefunction(self.object): if inspect.iscoroutinefunction(self.object):
self.add_line(' :async:', sourcename) self.add_line(' :async:', sourcename)
def add_singledispatch_directive_header(self, sig: str) -> None: def format_signature(self, **kwargs: Any) -> str:
sourcename = self.get_sourcename() sig = super().format_signature(**kwargs)
sigs = [sig]
# intercept generated directive headers if inspect.is_singledispatch_function(self.object):
# TODO: It is very hacky to use mock to intercept header generation # append signature of singledispatch'ed functions
with patch.object(self, 'add_line') as add_line: for typ, func in self.object.registry.items():
super().add_directive_header(sig) if typ is object:
pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)
# output first line of header documenter = FunctionDocumenter(self.directive, '')
self.add_line(*add_line.call_args_list[0][0]) documenter.object = func
sigs.append(documenter.format_signature())
# inserts signature of singledispatch'ed functions return "\n".join(sigs)
for typ, func in self.object.registry.items():
if typ is object:
pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)
documenter = FunctionDocumenter(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: def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
"""Annotate type hint to the first argument of function if needed.""" """Annotate type hint to the first argument of function if needed."""
@@ -1487,11 +1480,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
return args return args
def add_directive_header(self, sig: str) -> None: def add_directive_header(self, sig: str) -> None:
meth = self.parent.__dict__.get(self.objpath[-1]) super().add_directive_header(sig)
if inspect.is_singledispatch_method(meth):
self.add_singledispatch_directive_header(sig)
else:
super().add_directive_header(sig)
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
obj = self.parent.__dict__.get(self.object_name, self.object) obj = self.parent.__dict__.get(self.object_name, self.object)
@@ -1509,36 +1498,26 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
def document_members(self, all_members: bool = False) -> None: def document_members(self, all_members: bool = False) -> None:
pass pass
def add_singledispatch_directive_header(self, sig: str) -> None: def format_signature(self, **kwargs: Any) -> str:
sourcename = self.get_sourcename() sig = super().format_signature(**kwargs)
sigs = [sig]
# 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]) meth = self.parent.__dict__.get(self.objpath[-1])
for typ, func in meth.dispatcher.registry.items(): if inspect.is_singledispatch_method(meth):
if typ is object: # append signature of singledispatch'ed functions
pass # default implementation. skipped. for typ, func in meth.dispatcher.registry.items():
else: if typ is object:
self.annotate_to_first_argument(func, typ) pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)
documenter = MethodDocumenter(self.directive, '') documenter = MethodDocumenter(self.directive, '')
documenter.parent = self.parent documenter.parent = self.parent
documenter.object = func documenter.object = func
documenter.objpath = self.objpath documenter.objpath = [None]
self.add_line(' %s%s' % (self.format_name(), sigs.append(documenter.format_signature())
documenter.format_signature()),
sourcename)
# output remains of directive header return "\n".join(sigs)
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: def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
"""Annotate type hint to the first argument of function if needed.""" """Annotate type hint to the first argument of function if needed."""

View File

@@ -252,6 +252,7 @@ def test_get_doc(app):
def getdocl(objtype, obj): def getdocl(objtype, obj):
inst = app.registry.documenters[objtype](directive, 'tmp') inst = app.registry.documenters[objtype](directive, 'tmp')
inst.parent = object # dummy
inst.object = obj inst.object = obj
inst.objpath = [obj.__name__] inst.objpath = [obj.__name__]
inst.doc_as_attr = False inst.doc_as_attr = False
@@ -1658,8 +1659,8 @@ def test_singledispatch(app):
'', '',
'', '',
'.. py:function:: func(arg, kwarg=None)', '.. py:function:: func(arg, 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',
'', '',
' A function for general use.', ' A function for general use.',
@@ -1674,8 +1675,8 @@ def test_singledispatch_autofunction(app):
assert list(actual) == [ assert list(actual) == [
'', '',
'.. py:function:: func(arg, kwarg=None)', '.. py:function:: func(arg, 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',
'', '',
' A function for general use.', ' A function for general use.',
@@ -1701,8 +1702,8 @@ def test_singledispatchmethod(app):
'', '',
'', '',
' .. py:method:: Foo.meth(arg, kwarg=None)', ' .. py:method:: Foo.meth(arg, 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',
'', '',
' A method for general use.', ' A method for general use.',
@@ -1719,8 +1720,8 @@ 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: 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',
'', '',
' A method for general use.', ' A method for general use.',