Merge branch 'sphinx-doc:4.x' into 4.x

This commit is contained in:
Louis Maddox 2021-07-19 22:28:38 +01:00 committed by GitHub
commit 5ae46cb388
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 147 additions and 33 deletions

View File

@ -13,6 +13,10 @@ Deprecated
Features added Features added
-------------- --------------
* #9445: autodoc: Support class properties
* #9445: py domain: ``:py:property:`` directive supports ``:classmethod:``
option to describe the class property
Bugs fixed Bugs fixed
---------- ----------

View File

@ -1151,6 +1151,20 @@ Miscellany
Formerly, use of *fncychap* with other styles than ``Bjarne`` was Formerly, use of *fncychap* with other styles than ``Bjarne`` was
dysfunctional. dysfunctional.
- Docutils :dudir:`container` directives are supported in LaTeX output: to
let a container class with name ``foo`` influence the final PDF via LaTeX,
it is only needed to define in the preamble an environment
``sphinxclassfoo``. A simple example would be:
.. code-block:: latex
\newenvironment{sphinxclassred}{\color{red}}{}
Currently the class names must contain only ascii characters and avoid
characters special to LaTeX such as ``\``.
.. versionadded:: 4.1.0
.. hint:: .. hint::
As an experimental feature, Sphinx can use user-defined template file for As an experimental feature, Sphinx can use user-defined template file for

View File

@ -233,7 +233,7 @@ Sweet!
.. note:: .. note::
Generating a PDF using Sphinx can be done running ``make latexpdf``, Generating a PDF using Sphinx can be done running ``make latexpdf``,
provided that the system has a working :math:`\LaTeX` installation, provided that the system has a working LaTeX installation,
as explained in the documentation of :class:`sphinx.builders.latex.LaTeXBuilder`. as explained in the documentation of :class:`sphinx.builders.latex.LaTeXBuilder`.
Although this is perfectly feasible, such installations are often big, Although this is perfectly feasible, such installations are often big,
and in general LaTeX requires careful configuration in some cases, and in general LaTeX requires careful configuration in some cases,

View File

@ -329,6 +329,13 @@ The following directives are provided for module and class contents:
Indicate the property is abstract. Indicate the property is abstract.
.. rst:directive:option:: classmethod
:type: no value
Indicate the property is a classmethod.
.. versionaddedd: 4.2
.. rst:directive:option:: type: type of the property .. rst:directive:option:: type: type of the property
:type: text :type: text

View File

@ -852,6 +852,7 @@ class PyProperty(PyObject):
option_spec = PyObject.option_spec.copy() option_spec = PyObject.option_spec.copy()
option_spec.update({ option_spec.update({
'abstractmethod': directives.flag, 'abstractmethod': directives.flag,
'classmethod': directives.flag,
'type': directives.unchanged, 'type': directives.unchanged,
}) })
@ -865,10 +866,13 @@ class PyProperty(PyObject):
return fullname, prefix return fullname, prefix
def get_signature_prefix(self, sig: str) -> str: def get_signature_prefix(self, sig: str) -> str:
prefix = ['property'] prefix = []
if 'abstractmethod' in self.options: if 'abstractmethod' in self.options:
prefix.insert(0, 'abstract') prefix.append('abstract')
if 'classmethod' in self.options:
prefix.append('class')
prefix.append('property')
return ' '.join(prefix) + ' ' return ' '.join(prefix) + ' '
def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:

View File

@ -718,7 +718,7 @@ class Documenter:
isattr = False isattr = False
doc = getdoc(member, self.get_attr, self.config.autodoc_inherit_docstrings, doc = getdoc(member, self.get_attr, self.config.autodoc_inherit_docstrings,
self.parent, self.object_name) self.object, membername)
if not isinstance(doc, str): if not isinstance(doc, str):
# Ignore non-string __doc__ # Ignore non-string __doc__
doc = None doc = None
@ -2661,7 +2661,32 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): #
@classmethod @classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool: ) -> bool:
return inspect.isproperty(member) and isinstance(parent, ClassDocumenter) if isinstance(parent, ClassDocumenter):
if inspect.isproperty(member):
return True
else:
__dict__ = safe_getattr(parent.object, '__dict__', {})
obj = __dict__.get(membername)
return isinstance(obj, classmethod) and inspect.isproperty(obj.__func__)
else:
return False
def import_object(self, raiseerror: bool = False) -> bool:
"""Check the exisitence of uninitialized instance attribute when failed to import
the attribute."""
ret = super().import_object(raiseerror)
if ret and not inspect.isproperty(self.object):
__dict__ = safe_getattr(self.parent, '__dict__', {})
obj = __dict__.get(self.objpath[-1])
if isinstance(obj, classmethod) and inspect.isproperty(obj.__func__):
self.object = obj.__func__
self.isclassmethod = True
return True
else:
return False
self.isclassmethod = False
return ret
def document_members(self, all_members: bool = False) -> None: def document_members(self, all_members: bool = False) -> None:
pass pass
@ -2675,6 +2700,8 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): #
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
if inspect.isabstractmethod(self.object): if inspect.isabstractmethod(self.object):
self.add_line(' :abstractmethod:', sourcename) self.add_line(' :abstractmethod:', sourcename)
if self.isclassmethod:
self.add_line(' :classmethod:', sourcename)
if safe_getattr(self.object, 'fget', None) and self.config.autodoc_typehints != 'none': if safe_getattr(self.object, 'fget', None) and self.config.autodoc_typehints != 'none':
try: try:

View File

@ -245,12 +245,17 @@ def ispartial(obj: Any) -> bool:
return isinstance(obj, (partial, partialmethod)) return isinstance(obj, (partial, partialmethod))
def isclassmethod(obj: Any) -> bool: def isclassmethod(obj: Any, cls: Any = None, name: str = None) -> bool:
"""Check if the object is classmethod.""" """Check if the object is classmethod."""
if isinstance(obj, classmethod): if isinstance(obj, classmethod):
return True return True
elif inspect.ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__): elif inspect.ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__):
return True return True
elif cls and name:
for basecls in getmro(cls):
meth = basecls.__dict__.get(name)
if meth:
return isclassmethod(meth)
return False return False
@ -837,6 +842,12 @@ def getdoc(obj: Any, attrgetter: Callable = safe_getattr,
* inherited docstring * inherited docstring
* inherited decorated methods * inherited decorated methods
""" """
if cls and name and isclassmethod(obj, cls, name):
for basecls in getmro(cls):
meth = basecls.__dict__.get(name)
if meth:
return getdoc(meth.__func__)
doc = attrgetter(obj, '__doc__', None) doc = attrgetter(obj, '__doc__', None)
if ispartial(obj) and doc == obj.__class__.__doc__: if ispartial(obj) and doc == obj.__class__.__doc__:
return getdoc(obj.func) return getdoc(obj.func)

View File

@ -310,7 +310,10 @@ def stringify(annotation: Any) -> str:
return INVALID_BUILTIN_CLASSES[annotation] return INVALID_BUILTIN_CLASSES[annotation]
elif (getattr(annotation, '__module__', None) == 'builtins' and elif (getattr(annotation, '__module__', None) == 'builtins' and
hasattr(annotation, '__qualname__')): hasattr(annotation, '__qualname__')):
return annotation.__qualname__ if hasattr(annotation, '__args__'): # PEP 585 generic
return repr(annotation)
else:
return annotation.__qualname__
elif annotation is Ellipsis: elif annotation is Ellipsis:
return '...' return '...'

View File

@ -2,5 +2,10 @@ class Foo:
"""docstring""" """docstring"""
@property @property
def prop(self) -> int: def prop1(self) -> int:
"""docstring"""
@classmethod
@property
def prop2(self) -> int:
"""docstring""" """docstring"""

View File

@ -813,8 +813,12 @@ def test_pyattribute(app):
def test_pyproperty(app): def test_pyproperty(app):
text = (".. py:class:: Class\n" text = (".. py:class:: Class\n"
"\n" "\n"
" .. py:property:: prop\n" " .. py:property:: prop1\n"
" :abstractmethod:\n" " :abstractmethod:\n"
" :type: str\n"
"\n"
" .. py:property:: prop2\n"
" :classmethod:\n"
" :type: str\n") " :type: str\n")
domain = app.env.get_domain('py') domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text) doctree = restructuredtext.parse(app, text)
@ -822,15 +826,25 @@ def test_pyproperty(app):
[desc, ([desc_signature, ([desc_annotation, "class "], [desc, ([desc_signature, ([desc_annotation, "class "],
[desc_name, "Class"])], [desc_name, "Class"])],
[desc_content, (addnodes.index, [desc_content, (addnodes.index,
desc,
addnodes.index,
desc)])])) desc)])]))
assert_node(doctree[1][1][0], addnodes.index, assert_node(doctree[1][1][0], addnodes.index,
entries=[('single', 'prop (Class property)', 'Class.prop', '', None)]) entries=[('single', 'prop1 (Class property)', 'Class.prop1', '', None)])
assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "abstract property "], assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "abstract property "],
[desc_name, "prop"], [desc_name, "prop1"],
[desc_annotation, ": str"])], [desc_annotation, ": str"])],
[desc_content, ()])) [desc_content, ()]))
assert 'Class.prop' in domain.objects assert_node(doctree[1][1][2], addnodes.index,
assert domain.objects['Class.prop'] == ('index', 'Class.prop', 'property', False) entries=[('single', 'prop2 (Class property)', 'Class.prop2', '', None)])
assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, "class property "],
[desc_name, "prop2"],
[desc_annotation, ": str"])],
[desc_content, ()]))
assert 'Class.prop1' in domain.objects
assert domain.objects['Class.prop1'] == ('index', 'Class.prop1', 'property', False)
assert 'Class.prop2' in domain.objects
assert domain.objects['Class.prop2'] == ('index', 'Class.prop2', 'property', False)
def test_pydecorator_signature(app): def test_pydecorator_signature(app):

View File

@ -1398,16 +1398,9 @@ def test_slots(app):
def test_enum_class(app): def test_enum_class(app):
options = {"members": None} options = {"members": None}
actual = do_autodoc(app, 'class', 'target.enums.EnumCls', options) actual = do_autodoc(app, 'class', 'target.enums.EnumCls', options)
if sys.version_info < (3, 10):
sig = '(value)'
else:
sig = ('(value, names=None, *, module=None, qualname=None, type=None, start=1, '
'boundary=None)')
assert list(actual) == [ assert list(actual) == [
'', '',
'.. py:class:: EnumCls%s' % sig, '.. py:class:: EnumCls(value)',
' :module: target.enums', ' :module: target.enums',
'', '',
' this is enum class', ' this is enum class',

View File

@ -212,12 +212,20 @@ def test_properties(app):
' docstring', ' docstring',
'', '',
'', '',
' .. py:property:: Foo.prop', ' .. py:property:: Foo.prop1',
' :module: target.properties', ' :module: target.properties',
' :type: int', ' :type: int',
'', '',
' docstring', ' docstring',
'', '',
'',
' .. py:property:: Foo.prop2',
' :module: target.properties',
' :classmethod:',
' :type: int',
'',
' docstring',
'',
] ]

View File

@ -16,13 +16,28 @@ from .test_ext_autodoc import do_autodoc
@pytest.mark.sphinx('html', testroot='ext-autodoc') @pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_properties(app): def test_properties(app):
actual = do_autodoc(app, 'property', 'target.properties.Foo.prop') actual = do_autodoc(app, 'property', 'target.properties.Foo.prop1')
assert list(actual) == [ assert list(actual) == [
'', '',
'.. py:property:: Foo.prop', '.. py:property:: Foo.prop1',
' :module: target.properties', ' :module: target.properties',
' :type: int', ' :type: int',
'', '',
' docstring', ' docstring',
'', '',
] ]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_class_properties(app):
actual = do_autodoc(app, 'property', 'target.properties.Foo.prop2')
assert list(actual) == [
'',
'.. py:property:: Foo.prop2',
' :module: target.properties',
' :classmethod:',
' :type: int',
'',
' docstring',
'',
]

View File

@ -215,11 +215,8 @@ def test_signature_annotations():
# optional union # optional union
sig = inspect.signature(f20) sig = inspect.signature(f20)
if sys.version_info < (3, 7): assert stringify_signature(sig) in ('() -> Optional[Union[int, str]]',
assert stringify_signature(sig) in ('() -> Optional[Union[int, str]]', '() -> Optional[Union[str, int]]')
'() -> Optional[Union[str, int]]')
else:
assert stringify_signature(sig) == '() -> Optional[Union[int, str]]'
# Any # Any
sig = inspect.signature(f14) sig = inspect.signature(f14)

View File

@ -142,9 +142,9 @@ def test_restify_type_Literal():
@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.') @pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.')
def test_restify_type_union_operator(): def test_restify_type_union_operator():
assert restify(int | None) == "Optional[:class:`int`]" # type: ignore assert restify(int | None) == ":class:`int` | :obj:`None`" # type: ignore
assert restify(int | str) == ":class:`int` | :class:`str`" # type: ignore assert restify(int | str) == ":class:`int` | :class:`str`" # type: ignore
assert restify(int | str | None) == "Optional[:class:`int` | :class:`str`]" # type: ignore assert restify(int | str | None) == ":class:`int` | :class:`str` | :obj:`None`" # type: ignore
def test_restify_broken_type_hints(): def test_restify_broken_type_hints():
@ -175,6 +175,18 @@ def test_stringify_type_hints_containers():
assert stringify(Generator[None, None, None]) == "Generator[None, None, None]" assert stringify(Generator[None, None, None]) == "Generator[None, None, None]"
@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.')
def test_stringify_type_hints_pep_585():
assert stringify(list[int]) == "list[int]"
assert stringify(list[str]) == "list[str]"
assert stringify(dict[str, float]) == "dict[str, float]"
assert stringify(tuple[str, str, str]) == "tuple[str, str, str]"
assert stringify(tuple[str, ...]) == "tuple[str, ...]"
assert stringify(tuple[()]) == "tuple[()]"
assert stringify(list[dict[str, tuple]]) == "list[dict[str, tuple]]"
assert stringify(type[int]) == "type[int]"
@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.') @pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.')
def test_stringify_Annotated(): def test_stringify_Annotated():
from typing import Annotated # type: ignore from typing import Annotated # type: ignore
@ -253,9 +265,9 @@ def test_stringify_type_Literal():
@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.') @pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.')
def test_stringify_type_union_operator(): def test_stringify_type_union_operator():
assert stringify(int | None) == "Optional[int]" # type: ignore assert stringify(int | None) == "int | None" # type: ignore
assert stringify(int | str) == "int | str" # type: ignore assert stringify(int | str) == "int | str" # type: ignore
assert stringify(int | str | None) == "Optional[int | str]" # type: ignore assert stringify(int | str | None) == "int | str | None" # type: ignore
def test_stringify_broken_type_hints(): def test_stringify_broken_type_hints():