From fcfdc19624e91ae4c684aba359d19efd3f5790b0 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Fri, 15 May 2020 01:28:39 +0900 Subject: [PATCH] refactor: autodoc: Remove magic mock from singledispatch processing --- sphinx/ext/autodoc/__init__.py | 103 +++++++++++++-------------------- tests/test_ext_autodoc.py | 17 +++--- 2 files changed, 50 insertions(+), 70 deletions(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 203c21e8b..3aaf02dc6 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -16,7 +16,6 @@ import warnings from inspect import Parameter from types import ModuleType from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Tuple, Type, Union -from unittest.mock import patch from docutils.statemachine import StringList @@ -421,8 +420,15 @@ class Documenter: directive = getattr(self, 'directivetype', self.objtype) name = self.format_name() 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: self.add_line(' :noindex:', sourcename) if self.objpath: @@ -1075,41 +1081,28 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ def add_directive_header(self, sig: str) -> None: sourcename = self.get_sourcename() - if inspect.is_singledispatch_function(self.object): - self.add_singledispatch_directive_header(sig) - else: - super().add_directive_header(sig) + super().add_directive_header(sig) if inspect.iscoroutinefunction(self.object): self.add_line(' :async:', sourcename) - def add_singledispatch_directive_header(self, sig: str) -> None: - sourcename = self.get_sourcename() + def format_signature(self, **kwargs: Any) -> str: + 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) + if inspect.is_singledispatch_function(self.object): + # append signature of singledispatch'ed functions + for typ, func in self.object.registry.items(): + if typ is object: + pass # default implementation. skipped. + else: + self.annotate_to_first_argument(func, typ) - # output first line of header - self.add_line(*add_line.call_args_list[0][0]) + documenter = FunctionDocumenter(self.directive, '') + documenter.object = func + sigs.append(documenter.format_signature()) - # inserts signature of singledispatch'ed functions - 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]) + return "\n".join(sigs) def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: """Annotate type hint to the first argument of function if needed.""" @@ -1487,11 +1480,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: return args def add_directive_header(self, sig: str) -> None: - meth = self.parent.__dict__.get(self.objpath[-1]) - if inspect.is_singledispatch_method(meth): - self.add_singledispatch_directive_header(sig) - else: - super().add_directive_header(sig) + super().add_directive_header(sig) sourcename = self.get_sourcename() 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: pass - def add_singledispatch_directive_header(self, sig: str) -> None: - sourcename = self.get_sourcename() + def format_signature(self, **kwargs: Any) -> str: + 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]) - for typ, func in meth.dispatcher.registry.items(): - if typ is object: - pass # default implementation. skipped. - else: - self.annotate_to_first_argument(func, typ) + if inspect.is_singledispatch_method(meth): + # append signature of singledispatch'ed functions + 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.parent = self.parent - documenter.object = func - documenter.objpath = self.objpath - self.add_line(' %s%s' % (self.format_name(), - documenter.format_signature()), - sourcename) + documenter = MethodDocumenter(self.directive, '') + documenter.parent = self.parent + documenter.object = func + documenter.objpath = [None] + sigs.append(documenter.format_signature()) - # output remains of directive header - for call in add_line.call_args_list[1:]: - self.add_line(*call[0]) + return "\n".join(sigs) def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: """Annotate type hint to the first argument of function if needed.""" diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index cecb171cd..88d7deb68 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -252,6 +252,7 @@ def test_get_doc(app): def getdocl(objtype, obj): inst = app.registry.documenters[objtype](directive, 'tmp') + inst.parent = object # dummy inst.object = obj inst.objpath = [obj.__name__] inst.doc_as_attr = False @@ -1658,8 +1659,8 @@ def test_singledispatch(app): '', '', '.. py:function:: func(arg, kwarg=None)', - ' func(arg: int, kwarg=None)', - ' func(arg: str, kwarg=None)', + ' func(arg: int, kwarg=None)', + ' func(arg: str, kwarg=None)', ' :module: target.singledispatch', '', ' A function for general use.', @@ -1674,8 +1675,8 @@ def test_singledispatch_autofunction(app): assert list(actual) == [ '', '.. py:function:: func(arg, kwarg=None)', - ' func(arg: int, kwarg=None)', - ' func(arg: str, kwarg=None)', + ' func(arg: int, kwarg=None)', + ' func(arg: str, kwarg=None)', ' :module: target.singledispatch', '', ' A function for general use.', @@ -1701,8 +1702,8 @@ def test_singledispatchmethod(app): '', '', ' .. py:method:: Foo.meth(arg, kwarg=None)', - ' Foo.meth(arg: int, kwarg=None)', - ' Foo.meth(arg: str, kwarg=None)', + ' Foo.meth(arg: int, kwarg=None)', + ' Foo.meth(arg: str, kwarg=None)', ' :module: target.singledispatchmethod', '', ' A method for general use.', @@ -1719,8 +1720,8 @@ def test_singledispatchmethod_automethod(app): assert list(actual) == [ '', '.. py:method:: Foo.meth(arg, kwarg=None)', - ' Foo.meth(arg: int, kwarg=None)', - ' Foo.meth(arg: str, kwarg=None)', + ' Foo.meth(arg: int, kwarg=None)', + ' Foo.meth(arg: str, kwarg=None)', ' :module: target.singledispatchmethod', '', ' A method for general use.',