Merge branch '3.x' into 7079_autodoc_typehints_description

This commit is contained in:
Takeshi KOMIYA 2020-03-08 15:08:47 +09:00 committed by GitHub
commit 3f21fd6041
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 322 additions and 36 deletions

14
CHANGES
View File

@ -23,18 +23,17 @@ Incompatible changes
* Due to the scoping changes for :rst:dir:`productionlist` some uses of
:rst:role:`token` must be modified to include the scope which was previously
ignored.
* #6903: js domain: Internal data structure has changed. Both objects and
modules have node_id for cross reference
* #6903: Internal data structure of Python, reST and standard domains have
changed. The node_id is added to the index of objects and modules. Now they
contains a pair of docname and node_id for cross reference.
* #7210: js domain: Non intended behavior is removed such as ``parseInt_`` links
to ``.. js:function:: parseInt``
* #6903: rst domain: Internal data structure has changed. Now objects have
node_id for cross reference
* #7229: rst domain: Non intended behavior is removed such as ``numref_`` links
to ``.. rst:role:: numref``
* #6903: py domain: Internal data structure has changed. Both objects and
modules have node_id for cross reference
* #6903: py domain: Non intended behavior is removed such as ``say_hello_``
links to ``.. py:function:: say_hello()``
* #7246: py domain: Drop special cross reference helper for exceptions,
functions and methods
Deprecated
----------
@ -42,6 +41,7 @@ Deprecated
* ``desc_signature['first']``
* ``sphinx.directives.DescDirective``
* ``sphinx.domains.std.StandardDomain.add_object()``
* ``sphinx.domains.python.PyDecoratorMixin``
* ``sphinx.parsers.Parser.app``
* ``sphinx.testing.path.Path.text()``
* ``sphinx.testing.path.Path.bytes()``
@ -58,6 +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 and methods
* #7079: autodoc: :confval:`autodoc_typehints` accepts ``"description"``
configuration. It shows typehints as object description
* #6558: glossary: emit a warning for duplicated glossary entry
@ -93,6 +94,7 @@ Bugs fixed
* C++, suppress warnings for directly dependent typenames in cross references
generated automatically in signatures.
* #5637: autodoc: Incorrect handling of nested class names on show-inheritance
* #7267: autodoc: error message for invalid directive options has wrong location
* #5637: inheritance_diagram: Incorrect handling of nested class names
* #7139: ``code-block:: guess`` does not work

View File

@ -41,6 +41,11 @@ The following is a list of deprecated interfaces.
- 5.0
- ``sphinx.domains.std.StandardDomain.note_object()``
* - ``sphinx.domains.python.PyDecoratorMixin``
- 3.0
- 5.0
- N/A
* - ``sphinx.parsers.Parser.app``
- 3.0
- 5.0

View File

@ -25,7 +25,7 @@ from sphinx import addnodes
from sphinx.addnodes import pending_xref, desc_signature
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType, Index, IndexEntry
from sphinx.environment import BuildEnvironment
@ -489,6 +489,23 @@ class PyFunction(PyObject):
return _('%s() (built-in function)') % name
class PyDecoratorFunction(PyFunction):
"""Description of a decorator."""
def run(self) -> List[Node]:
# a decorator function is a function after all
self.name = 'py:function'
return super().run()
def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
ret = super().handle_signature(sig, signode)
signode.insert(0, addnodes.desc_addname('@', '@'))
return ret
def needs_arglist(self) -> bool:
return False
class PyVariable(PyObject):
"""Description of a variable."""
@ -700,6 +717,22 @@ class PyStaticMethod(PyMethod):
return super().run()
class PyDecoratorMethod(PyMethod):
"""Description of a decoratormethod."""
def run(self) -> List[Node]:
self.name = 'py:method'
return super().run()
def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
ret = super().handle_signature(sig, signode)
signode.insert(0, addnodes.desc_addname('@', '@'))
return ret
def needs_arglist(self) -> bool:
return False
class PyAttribute(PyObject):
"""Description of an attribute."""
@ -742,6 +775,15 @@ class PyDecoratorMixin:
Mixin for decorator directives.
"""
def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
for cls in self.__class__.__mro__:
if cls.__name__ != 'DirectiveAdapter':
warnings.warn('PyDecoratorMixin is deprecated. '
'Please check the implementation of %s' % cls,
RemovedInSphinx50Warning)
break
else:
warnings.warn('PyDecoratorMixin is deprecated', RemovedInSphinx50Warning)
ret = super().handle_signature(sig, signode) # type: ignore
signode.insert(0, addnodes.desc_addname('@', '@'))
return ret
@ -750,25 +792,6 @@ class PyDecoratorMixin:
return False
class PyDecoratorFunction(PyDecoratorMixin, PyModulelevel):
"""
Directive to mark functions meant to be used as decorators.
"""
def run(self) -> List[Node]:
# a decorator function is a function after all
self.name = 'py:function'
return super().run()
class PyDecoratorMethod(PyDecoratorMixin, PyClassmember):
"""
Directive to mark methods meant to be used as decorators.
"""
def run(self) -> List[Node]:
self.name = 'py:method'
return super().run()
class PyModule(SphinxDirective):
"""
Directive to mark description of a new module.
@ -1110,14 +1133,6 @@ class PythonDomain(Domain):
elif modname and classname and \
modname + '.' + classname + '.' + name in self.objects:
newname = modname + '.' + classname + '.' + name
# special case: builtin exceptions have module "exceptions" set
elif type == 'exc' and '.' not in name and \
'exceptions.' + name in self.objects:
newname = 'exceptions.' + name
# special case: object methods
elif type in ('func', 'meth') and '.' not in name and \
'object.' + name in self.objects:
newname = 'object.' + name
if newname is not None:
matches.append((newname, self.objects[newname]))
return matches

View File

@ -14,7 +14,8 @@ import importlib
import re
import warnings
from types import ModuleType
from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Tuple, Union
from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Tuple, Type, Union
from unittest.mock import patch
from docutils.statemachine import StringList
@ -1056,6 +1057,62 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
self.add_line(' :async:', sourcename)
class SingledispatchFunctionDocumenter(FunctionDocumenter):
"""
Specialized Documenter subclass for singledispatch'ed functions.
"""
objtype = 'singledispatch_function'
directivetype = 'function'
member_order = 30
# before FunctionDocumenter
priority = FunctionDocumenter.priority + 1
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return (super().can_document_member(member, membername, isattr, parent) and
inspect.is_singledispatch_function(member))
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
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:
"""Annotate type hint to the first argument of function if needed."""
sig = inspect.signature(func)
if len(sig.parameters) == 0:
return
name = list(sig.parameters)[0]
if name not in func.__annotations__:
func.__annotations__[name] = typ
class DecoratorDocumenter(FunctionDocumenter):
"""
Specialized Documenter subclass for decorator functions.
@ -1400,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.
@ -1612,8 +1729,10 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_autodocumenter(DataDocumenter)
app.add_autodocumenter(DataDeclarationDocumenter)
app.add_autodocumenter(FunctionDocumenter)
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

@ -137,7 +137,7 @@ class AutodocDirective(SphinxDirective):
except (KeyError, ValueError, TypeError) as exc:
# an option is either unknown or has a wrong type
logger.error('An option to %s is either unknown or has an invalid value: %s' %
(self.name, exc), location=(source, lineno))
(self.name, exc), location=(self.env.docname, lineno))
return []
# generate the output

View File

@ -72,12 +72,14 @@ def setup_documenters(app: Any) -> None:
FunctionDocumenter, MethodDocumenter, AttributeDocumenter,
InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
SlotsAttributeDocumenter, DataDeclarationDocumenter,
SingledispatchFunctionDocumenter,
)
documenters = [
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
FunctionDocumenter, MethodDocumenter, AttributeDocumenter,
InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
SlotsAttributeDocumenter, DataDeclarationDocumenter,
SingledispatchFunctionDocumenter,
] # type: List[Type[Documenter]]
for documenter in documenters:
app.registry.add_documenter(documenter.objtype, documenter)

View File

@ -224,6 +224,26 @@ def isattributedescriptor(obj: Any) -> bool:
return False
def is_singledispatch_function(obj: Any) -> bool:
"""Check if the object is singledispatch function."""
if (inspect.isfunction(obj) and
hasattr(obj, 'dispatch') and
hasattr(obj, 'register') and
obj.dispatch.__module__ == 'functools'):
return True
else:
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

@ -51,3 +51,11 @@ module
.. py:attribute:: attr2
:type: :doc:`index`
.. py:module:: exceptions
.. py:exception:: Exception
.. py:module:: object
.. py:function:: sum()

View File

@ -0,0 +1,19 @@
from functools import singledispatch
@singledispatch
def func(arg, kwarg=None):
"""A function for general use."""
pass
@func.register(int)
def _func_int(arg, kwarg=None):
"""A function for int."""
pass
@func.register(str)
def _func_str(arg, kwarg=None):
"""A function for str."""
pass

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

@ -1563,3 +1563,49 @@ def test_autodoc_for_egged_code(app):
' :module: sample',
''
]
@pytest.mark.usefixtures('setup_test')
def test_singledispatch():
options = {"members": None}
actual = do_autodoc(app, 'module', 'target.singledispatch', options)
assert list(actual) == [
'',
'.. py:module:: target.singledispatch',
'',
'',
'.. py:function:: func(arg, kwarg=None)',
' func(arg: int, kwarg=None)',
' func(arg: str, kwarg=None)',
' :module: target.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.',
' '
]

View File

@ -585,6 +585,36 @@ def test_pyattribute(app):
assert domain.objects['Class.attr'] == ('index', 'class-attr', 'attribute')
def test_pydecorator_signature(app):
text = ".. py:decorator:: deco"
domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index,
[desc, ([desc_signature, ([desc_addname, "@"],
[desc_name, "deco"])],
desc_content)]))
assert_node(doctree[1], addnodes.desc, desctype="function",
domain="py", objtype="function", noindex=False)
assert 'deco' in domain.objects
assert domain.objects['deco'] == ('index', 'deco', 'function')
def test_pydecoratormethod_signature(app):
text = ".. py:decoratormethod:: deco"
domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index,
[desc, ([desc_signature, ([desc_addname, "@"],
[desc_name, "deco"])],
desc_content)]))
assert_node(doctree[1], addnodes.desc, desctype="method",
domain="py", objtype="method", noindex=False)
assert 'deco' in domain.objects
assert domain.objects['deco'] == ('index', 'deco', 'method')
@pytest.mark.sphinx(freshenv=True)
def test_module_index(app):
text = (".. py:module:: docutils\n"