mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge branch '3.x' into 7079_autodoc_typehints_description
This commit is contained in:
commit
3f21fd6041
14
CHANGES
14
CHANGES
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -51,3 +51,11 @@ module
|
||||
.. py:attribute:: attr2
|
||||
|
||||
:type: :doc:`index`
|
||||
|
||||
.. py:module:: exceptions
|
||||
|
||||
.. py:exception:: Exception
|
||||
|
||||
.. py:module:: object
|
||||
|
||||
.. py:function:: sum()
|
||||
|
19
tests/roots/test-ext-autodoc/target/singledispatch.py
Normal file
19
tests/roots/test-ext-autodoc/target/singledispatch.py
Normal 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
|
20
tests/roots/test-ext-autodoc/target/singledispatchmethod.py
Normal file
20
tests/roots/test-ext-autodoc/target/singledispatchmethod.py
Normal 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
|
@ -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.',
|
||||
' '
|
||||
]
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user