mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge branch '3.x'
This commit is contained in:
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -7,7 +7,7 @@ Subject: <short purpose of this pull request>
|
||||
- Critical or severe bugs: X.Y.Z
|
||||
- Others: X.Y
|
||||
|
||||
For more details, see https://www.sphinx-doc.org/en/master/devguide.html#branch-model
|
||||
For more details, see https://www.sphinx-doc.org/en/master/internals/release-process.html#branch-model
|
||||
-->
|
||||
|
||||
### Feature or Bugfix
|
||||
|
36
CHANGES
36
CHANGES
@@ -66,16 +66,32 @@ Incompatible changes
|
||||
Deprecated
|
||||
----------
|
||||
|
||||
* ``sphinx.ext.autodoc.importer.get_module_members()``
|
||||
|
||||
Features added
|
||||
--------------
|
||||
|
||||
* #8022: autodoc: autodata and autoattribute directives does not show right-hand
|
||||
value of the variable if docstring contains ``:meta hide-value:`` in
|
||||
info-field-list
|
||||
* #8132: Add :confval:`project_copyright` as an alias of :confval:`copyright`
|
||||
|
||||
Bugs fixed
|
||||
----------
|
||||
|
||||
* #741: autodoc: inherited-members doesn't work for instance attributes on super
|
||||
class
|
||||
* #8592: autodoc: ``:meta public:`` does not effect to variables
|
||||
* #8594: autodoc: empty __all__ attribute is ignored
|
||||
* #8306: autosummary: mocked modules are documented as empty page when using
|
||||
:recursive: option
|
||||
* #8094: texinfo: image files on the different directory with document are not
|
||||
copied
|
||||
|
||||
Testing
|
||||
--------
|
||||
|
||||
Release 3.4.1 (in development)
|
||||
Release 3.4.2 (in development)
|
||||
==============================
|
||||
|
||||
Dependencies
|
||||
@@ -93,9 +109,27 @@ Features added
|
||||
Bugs fixed
|
||||
----------
|
||||
|
||||
* #8164: autodoc: Classes that inherit mocked class are not documented
|
||||
|
||||
Testing
|
||||
--------
|
||||
|
||||
Release 3.4.1 (released Dec 25, 2020)
|
||||
=====================================
|
||||
|
||||
Bugs fixed
|
||||
----------
|
||||
|
||||
* #8559: autodoc: AttributeError is raised when using forward-reference type
|
||||
annotations
|
||||
* #8568: autodoc: TypeError is raised on checking slots attribute
|
||||
* #8567: autodoc: Instance attributes are incorrectly added to Parent class
|
||||
* #8566: autodoc: The ``autodoc-process-docstring`` event is emitted to the
|
||||
alias classes unexpectedly
|
||||
* #8583: autodoc: Unnecessary object comparision via ``__eq__`` method
|
||||
* #8565: linkcheck: Fix PriorityQueue crash when link tuples are not
|
||||
comparable
|
||||
|
||||
Release 3.4.0 (released Dec 20, 2020)
|
||||
=====================================
|
||||
|
||||
|
1
EXAMPLES
1
EXAMPLES
@@ -324,6 +324,7 @@ Documentation using a custom theme or integrated in a website
|
||||
* `Django <https://docs.djangoproject.com/>`__
|
||||
* `Doctrine <https://www.doctrine-project.org/>`__
|
||||
* `Enterprise Toolkit for Acrobat products <https://www.adobe.com/devnet-docs/acrobatetk/>`__
|
||||
* `FreeFEM <https://doc.freefem.org/introduction/>`__
|
||||
* `fmt <https://fmt.dev/>`__
|
||||
* `Gameduino <http://excamera.com/sphinx/gameduino/>`__
|
||||
* `gensim <https://radimrehurek.com/gensim/>`__
|
||||
|
@@ -56,6 +56,11 @@ The following is a list of deprecated interfaces.
|
||||
- 6.0
|
||||
- ``docutils.utils.smartyquotes``
|
||||
|
||||
* - ``sphinx.ext.autodoc.importer.get_module_members()``
|
||||
- 3.5
|
||||
- 5.0
|
||||
- ``sphinx.ext.autodoc.ModuleDocumenter.get_module_members()``
|
||||
|
||||
* - The ``follow_wrapped`` argument of ``sphinx.util.inspect.signature()``
|
||||
- 3.4
|
||||
- 5.0
|
||||
|
@@ -1,7 +1,7 @@
|
||||
.. _domain-api:
|
||||
|
||||
Domain API
|
||||
----------
|
||||
==========
|
||||
|
||||
.. module:: sphinx.domains
|
||||
|
||||
@@ -12,3 +12,16 @@ Domain API
|
||||
|
||||
.. autoclass:: Index
|
||||
:members:
|
||||
|
||||
|
||||
Python Domain
|
||||
-------------
|
||||
|
||||
.. module:: sphinx.domains.python
|
||||
|
||||
.. autoclass:: PythonDomain
|
||||
|
||||
.. autoattribute:: objects
|
||||
.. autoattribute:: modules
|
||||
.. automethod:: note_object
|
||||
.. automethod:: note_module
|
||||
|
@@ -70,9 +70,14 @@ Project information
|
||||
The author name(s) of the document. The default value is ``'unknown'``.
|
||||
|
||||
.. confval:: copyright
|
||||
.. confval:: project_copyright
|
||||
|
||||
A copyright statement in the style ``'2008, Author Name'``.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
As an alias, ``project_copyright`` is also allowed.
|
||||
|
||||
.. confval:: version
|
||||
|
||||
The major project version, used as the replacement for ``|version|``. For
|
||||
|
@@ -182,6 +182,16 @@ inserting them into the page source under a suitable :rst:dir:`py:module`,
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
* autodoc considers a variable member does not have any default value if its
|
||||
docstring contains ``:meta hide-value:`` in its :ref:`info-field-lists`.
|
||||
Example:
|
||||
|
||||
.. code-block:: rst
|
||||
|
||||
var1 = None #: :meta hide-value:
|
||||
|
||||
.. versionadded:: 3.5
|
||||
|
||||
* Python "special" members (that is, those named like ``__special__``) will
|
||||
be included if the ``special-members`` flag option is given::
|
||||
|
||||
@@ -554,7 +564,7 @@ There are also config values that you can set:
|
||||
...
|
||||
|
||||
If you set ``autodoc_type_aliases`` as
|
||||
``{'AliasType': 'your.module.TypeAlias'}``, it generates a following document
|
||||
``{'AliasType': 'your.module.AliasType'}``, it generates the following document
|
||||
internally::
|
||||
|
||||
.. py:function:: f() -> your.module.AliasType:
|
||||
|
@@ -22,7 +22,7 @@ from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.nodes import Node
|
||||
from docutils.nodes import Element, Node
|
||||
from requests import Response
|
||||
from requests.exceptions import HTTPError, TooManyRedirects
|
||||
|
||||
@@ -47,6 +47,14 @@ QUEUE_POLL_SECS = 1
|
||||
DEFAULT_DELAY = 60.0
|
||||
|
||||
|
||||
def node_line_or_0(node: Element) -> int:
|
||||
"""
|
||||
PriorityQueue items must be comparable. The line number is part of the
|
||||
tuple used by the PriorityQueue, keep an homogeneous type for comparison.
|
||||
"""
|
||||
return get_node_line(node) or 0
|
||||
|
||||
|
||||
class AnchorCheckParser(HTMLParser):
|
||||
"""Specialized HTML parser that looks for a specific anchor."""
|
||||
|
||||
@@ -406,7 +414,7 @@ class CheckExternalLinksBuilder(Builder):
|
||||
if 'refuri' not in refnode:
|
||||
continue
|
||||
uri = refnode['refuri']
|
||||
lineno = get_node_line(refnode)
|
||||
lineno = node_line_or_0(refnode)
|
||||
uri_info = (CHECK_IMMEDIATELY, uri, docname, lineno)
|
||||
self.wqueue.put(uri_info, False)
|
||||
n += 1
|
||||
@@ -415,7 +423,7 @@ class CheckExternalLinksBuilder(Builder):
|
||||
for imgnode in doctree.traverse(nodes.image):
|
||||
uri = imgnode['candidates'].get('?')
|
||||
if uri and '://' in uri:
|
||||
lineno = get_node_line(imgnode)
|
||||
lineno = node_line_or_0(imgnode)
|
||||
uri_info = (CHECK_IMMEDIATELY, uri, docname, lineno)
|
||||
self.wqueue.put(uri_info, False)
|
||||
n += 1
|
||||
|
@@ -179,7 +179,8 @@ class TexinfoBuilder(Builder):
|
||||
try:
|
||||
imagedir = path.join(self.outdir, targetname + '-figures')
|
||||
ensuredir(imagedir)
|
||||
copy_asset_file(path.join(self.srcdir, dest), imagedir)
|
||||
copy_asset_file(path.join(self.srcdir, src),
|
||||
path.join(imagedir, dest))
|
||||
except Exception as err:
|
||||
logger.warning(__('cannot copy image file %r: %s'),
|
||||
path.join(self.srcdir, src), err)
|
||||
|
@@ -92,7 +92,8 @@ class Config:
|
||||
# general options
|
||||
'project': ('Python', 'env', []),
|
||||
'author': ('unknown', 'env', []),
|
||||
'copyright': ('', 'html', []),
|
||||
'project_copyright': ('', 'html', [str]),
|
||||
'copyright': (lambda c: c.project_copyright, 'html', [str]),
|
||||
'version': ('', 'env', []),
|
||||
'release': ('', 'env', []),
|
||||
'today': ('', 'env', []),
|
||||
|
@@ -10,7 +10,6 @@
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import re
|
||||
import warnings
|
||||
from inspect import Parameter, Signature
|
||||
@@ -25,9 +24,9 @@ from sphinx.application import Sphinx
|
||||
from sphinx.config import ENUM, Config
|
||||
from sphinx.deprecation import RemovedInSphinx50Warning, RemovedInSphinx60Warning
|
||||
from sphinx.environment import BuildEnvironment
|
||||
from sphinx.ext.autodoc.importer import (get_class_members, get_module_members,
|
||||
get_object_members, import_object)
|
||||
from sphinx.ext.autodoc.mock import mock
|
||||
from sphinx.ext.autodoc.importer import (ClassAttribute, get_class_members, get_object_members,
|
||||
import_module, import_object)
|
||||
from sphinx.ext.autodoc.mock import ismock, mock
|
||||
from sphinx.locale import _, __
|
||||
from sphinx.pycode import ModuleAnalyzer, PycodeError
|
||||
from sphinx.util import inspect, logging
|
||||
@@ -88,7 +87,7 @@ SLOTSATTR = object()
|
||||
|
||||
def members_option(arg: Any) -> Union[object, List[str]]:
|
||||
"""Used to convert the :members: option to auto directives."""
|
||||
if arg is None or arg is True:
|
||||
if arg in (None, True):
|
||||
return ALL
|
||||
elif arg is False:
|
||||
return None
|
||||
@@ -107,14 +106,14 @@ def members_set_option(arg: Any) -> Union[object, Set[str]]:
|
||||
|
||||
def exclude_members_option(arg: Any) -> Union[object, Set[str]]:
|
||||
"""Used to convert the :exclude-members: option."""
|
||||
if arg is None:
|
||||
if arg in (None, True):
|
||||
return EMPTY
|
||||
return {x.strip() for x in arg.split(',') if x.strip()}
|
||||
|
||||
|
||||
def inherited_members_option(arg: Any) -> Union[object, Set[str]]:
|
||||
"""Used to convert the :members: option to auto directives."""
|
||||
if arg is None:
|
||||
if arg in (None, True):
|
||||
return 'object'
|
||||
else:
|
||||
return arg
|
||||
@@ -122,7 +121,7 @@ def inherited_members_option(arg: Any) -> Union[object, Set[str]]:
|
||||
|
||||
def member_order_option(arg: Any) -> Optional[str]:
|
||||
"""Used to convert the :members: option to auto directives."""
|
||||
if arg is None:
|
||||
if arg in (None, True):
|
||||
return None
|
||||
elif arg in ('alphabetical', 'bysource', 'groupwise'):
|
||||
return arg
|
||||
@@ -134,7 +133,7 @@ SUPPRESS = object()
|
||||
|
||||
|
||||
def annotation_option(arg: Any) -> Any:
|
||||
if arg is None:
|
||||
if arg in (None, True):
|
||||
# suppress showing the representation of the object
|
||||
return SUPPRESS
|
||||
else:
|
||||
@@ -272,11 +271,12 @@ class ObjectMember(tuple):
|
||||
return super().__new__(cls, (name, obj)) # type: ignore
|
||||
|
||||
def __init__(self, name: str, obj: Any, docstring: Optional[str] = None,
|
||||
skipped: bool = False) -> None:
|
||||
class_: Any = None, skipped: bool = False) -> None:
|
||||
self.__name__ = name
|
||||
self.object = obj
|
||||
self.docstring = docstring
|
||||
self.skipped = skipped
|
||||
self.class_ = class_
|
||||
|
||||
|
||||
ObjectMembers = Union[List[ObjectMember], List[Tuple[str, Any]]]
|
||||
@@ -534,8 +534,12 @@ class Documenter:
|
||||
# etc. don't support a prepended module name
|
||||
self.add_line(' :module: %s' % self.modname, sourcename)
|
||||
|
||||
def get_doc(self, ignore: int = None) -> List[List[str]]:
|
||||
"""Decode and return lines of the docstring(s) for the object."""
|
||||
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
|
||||
"""Decode and return lines of the docstring(s) for the object.
|
||||
|
||||
When it returns None value, autodoc-process-docstring will not be called for this
|
||||
object.
|
||||
"""
|
||||
if ignore is not None:
|
||||
warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated."
|
||||
% self.__class__.__name__,
|
||||
@@ -602,13 +606,17 @@ class Documenter:
|
||||
# add content from docstrings
|
||||
if not no_docstring:
|
||||
docstrings = self.get_doc()
|
||||
if not docstrings:
|
||||
# append at least a dummy docstring, so that the event
|
||||
# autodoc-process-docstring is fired and can add some
|
||||
# content if desired
|
||||
docstrings.append([])
|
||||
for i, line in enumerate(self.process_doc(docstrings)):
|
||||
self.add_line(line, sourcename, i)
|
||||
if docstrings is None:
|
||||
# Do not call autodoc-process-docstring on get_doc() returns None.
|
||||
pass
|
||||
else:
|
||||
if not docstrings:
|
||||
# append at least a dummy docstring, so that the event
|
||||
# autodoc-process-docstring is fired and can add some
|
||||
# content if desired
|
||||
docstrings.append([])
|
||||
for i, line in enumerate(self.process_doc(docstrings)):
|
||||
self.add_line(line, sourcename, i)
|
||||
|
||||
# add additional content (e.g. from document), if present
|
||||
if more_content:
|
||||
@@ -658,7 +666,7 @@ class Documenter:
|
||||
The user can override the skipping decision by connecting to the
|
||||
``autodoc-skip-member`` event.
|
||||
"""
|
||||
def is_filtered_inherited_member(name: str) -> bool:
|
||||
def is_filtered_inherited_member(name: str, obj: Any) -> bool:
|
||||
if inspect.isclass(self.object):
|
||||
for cls in self.object.__mro__:
|
||||
if cls.__name__ == self.options.inherited_members and cls != self.object:
|
||||
@@ -668,6 +676,8 @@ class Documenter:
|
||||
return False
|
||||
elif name in self.get_attr(cls, '__annotations__', {}):
|
||||
return False
|
||||
elif isinstance(obj, ObjectMember) and obj.class_ is cls:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
@@ -721,7 +731,7 @@ class Documenter:
|
||||
isprivate = membername.startswith('_')
|
||||
|
||||
keep = False
|
||||
if safe_getattr(member, '__sphinx_mock__', None) is not None:
|
||||
if ismock(member):
|
||||
# mocked module or object
|
||||
pass
|
||||
elif self.options.exclude_members and membername in self.options.exclude_members:
|
||||
@@ -732,7 +742,7 @@ class Documenter:
|
||||
if self.options.special_members and membername in self.options.special_members:
|
||||
if membername == '__doc__':
|
||||
keep = False
|
||||
elif is_filtered_inherited_member(membername):
|
||||
elif is_filtered_inherited_member(membername, obj):
|
||||
keep = False
|
||||
else:
|
||||
keep = has_doc or self.options.undoc_members
|
||||
@@ -752,14 +762,15 @@ class Documenter:
|
||||
if has_doc or self.options.undoc_members:
|
||||
if self.options.private_members is None:
|
||||
keep = False
|
||||
elif is_filtered_inherited_member(membername):
|
||||
elif is_filtered_inherited_member(membername, obj):
|
||||
keep = False
|
||||
else:
|
||||
keep = membername in self.options.private_members
|
||||
else:
|
||||
keep = False
|
||||
else:
|
||||
if self.options.members is ALL and is_filtered_inherited_member(membername):
|
||||
if (self.options.members is ALL and
|
||||
is_filtered_inherited_member(membername, obj)):
|
||||
keep = False
|
||||
else:
|
||||
# ignore undocumented members if :undoc-members: is not given
|
||||
@@ -1024,30 +1035,54 @@ class ModuleDocumenter(Documenter):
|
||||
if self.options.deprecated:
|
||||
self.add_line(' :deprecated:', sourcename)
|
||||
|
||||
def get_module_members(self) -> Dict[str, ObjectMember]:
|
||||
"""Get members of target module."""
|
||||
if self.analyzer:
|
||||
attr_docs = self.analyzer.attr_docs
|
||||
else:
|
||||
attr_docs = {}
|
||||
|
||||
members = {} # type: Dict[str, ObjectMember]
|
||||
for name in dir(self.object):
|
||||
try:
|
||||
value = safe_getattr(self.object, name, None)
|
||||
docstring = attr_docs.get(('', name), [])
|
||||
members[name] = ObjectMember(name, value, docstring="\n".join(docstring))
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
# annotation only member (ex. attr: int)
|
||||
try:
|
||||
for name in inspect.getannotations(self.object):
|
||||
if name not in members:
|
||||
docstring = attr_docs.get(('', name), [])
|
||||
members[name] = ObjectMember(name, INSTANCEATTR,
|
||||
docstring="\n".join(docstring))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return members
|
||||
|
||||
def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]:
|
||||
members = self.get_module_members()
|
||||
if want_all:
|
||||
members = get_module_members(self.object)
|
||||
if not self.__all__:
|
||||
if self.__all__ is None:
|
||||
# for implicit module members, check __module__ to avoid
|
||||
# documenting imported objects
|
||||
return True, members
|
||||
return True, list(members.values())
|
||||
else:
|
||||
ret = []
|
||||
for name, value in members:
|
||||
if name in self.__all__:
|
||||
ret.append(ObjectMember(name, value))
|
||||
else:
|
||||
ret.append(ObjectMember(name, value, skipped=True))
|
||||
for member in members.values():
|
||||
if member.__name__ not in self.__all__:
|
||||
member.skipped = True
|
||||
|
||||
return False, ret
|
||||
return False, list(members.values())
|
||||
else:
|
||||
memberlist = self.options.members or []
|
||||
ret = []
|
||||
for name in memberlist:
|
||||
try:
|
||||
value = safe_getattr(self.object, name)
|
||||
ret.append(ObjectMember(name, value))
|
||||
except AttributeError:
|
||||
if name in members:
|
||||
ret.append(members[name])
|
||||
else:
|
||||
logger.warning(__('missing attribute mentioned in :members: option: '
|
||||
'module %s, attribute %s') %
|
||||
(safe_getattr(self.object, '__name__', '???'), name),
|
||||
@@ -1567,6 +1602,10 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
||||
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
|
||||
|
||||
def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]:
|
||||
def convert(m: ClassAttribute) -> ObjectMember:
|
||||
"""Convert ClassAttribute object to ObjectMember."""
|
||||
return ObjectMember(m.name, m.value, class_=m.class_, docstring=m.docstring)
|
||||
|
||||
members = get_class_members(self.object, self.objpath, self.get_attr)
|
||||
if not want_all:
|
||||
if not self.options.members:
|
||||
@@ -1575,23 +1614,20 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
||||
selected = []
|
||||
for name in self.options.members: # type: str
|
||||
if name in members:
|
||||
selected.append(ObjectMember(name, members[name].value,
|
||||
docstring=members[name].docstring))
|
||||
selected.append(convert(members[name]))
|
||||
else:
|
||||
logger.warning(__('missing attribute %s in object %s') %
|
||||
(name, self.fullname), type='autodoc')
|
||||
return False, selected
|
||||
elif self.options.inherited_members:
|
||||
return False, [ObjectMember(m.name, m.value, docstring=m.docstring)
|
||||
for m in members.values()]
|
||||
return False, [convert(m) for m in members.values()]
|
||||
else:
|
||||
return False, [ObjectMember(m.name, m.value, docstring=m.docstring)
|
||||
for m in members.values() if m.class_ == self.object]
|
||||
return False, [convert(m) for m in members.values() if m.class_ == self.object]
|
||||
|
||||
def get_doc(self, ignore: int = None) -> List[List[str]]:
|
||||
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
|
||||
if self.doc_as_attr:
|
||||
# Don't show the docstring of the class when it is an alias.
|
||||
return []
|
||||
return None
|
||||
|
||||
lines = getattr(self, '_new_docstrings', None)
|
||||
if lines is not None:
|
||||
@@ -1746,7 +1782,7 @@ class TypeVarMixin(DataDocumenterMixinBase):
|
||||
return (isinstance(self.object, TypeVar) or
|
||||
super().should_suppress_directive_header())
|
||||
|
||||
def get_doc(self, ignore: int = None) -> List[List[str]]:
|
||||
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
|
||||
if ignore is not None:
|
||||
warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated."
|
||||
% self.__class__.__name__,
|
||||
@@ -1788,12 +1824,14 @@ class UninitializedGlobalVariableMixin(DataDocumenterMixinBase):
|
||||
except ImportError as exc:
|
||||
# annotation only instance variable (PEP-526)
|
||||
try:
|
||||
self.parent = importlib.import_module(self.modname)
|
||||
annotations = get_type_hints(self.parent, None,
|
||||
self.config.autodoc_type_aliases)
|
||||
if self.objpath[-1] in annotations:
|
||||
self.object = UNINITIALIZED_ATTR
|
||||
return True
|
||||
with mock(self.config.autodoc_mock_imports):
|
||||
parent = import_module(self.modname, self.config.autodoc_warningiserror)
|
||||
annotations = get_type_hints(parent, None,
|
||||
self.config.autodoc_type_aliases)
|
||||
if self.objpath[-1] in annotations:
|
||||
self.object = UNINITIALIZED_ATTR
|
||||
self.parent = parent
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -1805,10 +1843,10 @@ class UninitializedGlobalVariableMixin(DataDocumenterMixinBase):
|
||||
return False
|
||||
|
||||
def should_suppress_value_header(self) -> bool:
|
||||
return (self.object == UNINITIALIZED_ATTR or
|
||||
return (self.object is UNINITIALIZED_ATTR or
|
||||
super().should_suppress_value_header())
|
||||
|
||||
def get_doc(self, ignore: int = None) -> List[List[str]]:
|
||||
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
|
||||
if self.object is UNINITIALIZED_ATTR:
|
||||
return []
|
||||
else:
|
||||
@@ -1835,13 +1873,14 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
|
||||
def update_annotations(self, parent: Any) -> None:
|
||||
"""Update __annotations__ to support type_comment and so on."""
|
||||
try:
|
||||
annotations = inspect.getannotations(parent)
|
||||
annotations = dict(inspect.getannotations(parent))
|
||||
parent.__annotations__ = annotations
|
||||
|
||||
analyzer = ModuleAnalyzer.for_module(self.modname)
|
||||
analyzer.analyze()
|
||||
for (classname, attrname), annotation in analyzer.annotations.items():
|
||||
if classname == '' and attrname not in annotations:
|
||||
annotations[attrname] = annotation # type: ignore
|
||||
annotations[attrname] = annotation
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@@ -1852,6 +1891,17 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
|
||||
|
||||
return ret
|
||||
|
||||
def should_suppress_value_header(self) -> bool:
|
||||
if super().should_suppress_value_header():
|
||||
return True
|
||||
else:
|
||||
doc = self.get_doc()
|
||||
metadata = extract_metadata('\n'.join(sum(doc, [])))
|
||||
if 'hide-value' in metadata:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_directive_header(self, sig: str) -> None:
|
||||
super().add_directive_header(sig)
|
||||
sourcename = self.get_sourcename()
|
||||
@@ -1883,8 +1933,32 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
|
||||
return self.get_attr(self.parent or self.object, '__module__', None) \
|
||||
or self.modname
|
||||
|
||||
def get_module_comment(self, attrname: str) -> Optional[List[str]]:
|
||||
try:
|
||||
analyzer = ModuleAnalyzer.for_module(self.modname)
|
||||
analyzer.analyze()
|
||||
key = ('', attrname)
|
||||
if key in analyzer.attr_docs:
|
||||
return list(analyzer.attr_docs[key])
|
||||
except PycodeError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
|
||||
# Check the variable has a docstring-comment
|
||||
comment = self.get_module_comment(self.objpath[-1])
|
||||
if comment:
|
||||
return [comment]
|
||||
else:
|
||||
return super().get_doc(encoding, ignore)
|
||||
|
||||
def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
|
||||
) -> None:
|
||||
# Disable analyzing variable comment on Documenter.add_content() to control it on
|
||||
# DataDocumenter.add_content()
|
||||
self.analyzer = None
|
||||
|
||||
if not more_content:
|
||||
more_content = StringList()
|
||||
|
||||
@@ -2071,7 +2145,7 @@ class NonDataDescriptorMixin(DataDocumenterMixinBase):
|
||||
return (inspect.isattributedescriptor(self.object) or
|
||||
super().should_suppress_directive_header())
|
||||
|
||||
def get_doc(self, ignore: int = None) -> List[List[str]]:
|
||||
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
|
||||
if not inspect.isattributedescriptor(self.object):
|
||||
# the docstring of non datadescriptor is very probably the wrong thing
|
||||
# to display
|
||||
@@ -2093,7 +2167,7 @@ class SlotsMixin(DataDocumenterMixinBase):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except (AttributeError, ValueError):
|
||||
except (AttributeError, ValueError, TypeError):
|
||||
return False
|
||||
|
||||
def import_object(self, raiseerror: bool = False) -> bool:
|
||||
@@ -2110,7 +2184,7 @@ class SlotsMixin(DataDocumenterMixinBase):
|
||||
else:
|
||||
return super().should_suppress_directive_header()
|
||||
|
||||
def get_doc(self, ignore: int = None) -> List[List[str]]:
|
||||
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
|
||||
if self.object is SLOTSATTR:
|
||||
try:
|
||||
__slots__ = inspect.getslots(self.parent)
|
||||
@@ -2127,9 +2201,9 @@ class SlotsMixin(DataDocumenterMixinBase):
|
||||
return super().get_doc(ignore) # type: ignore
|
||||
|
||||
|
||||
class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
|
||||
class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase):
|
||||
"""
|
||||
Mixin for AttributeDocumenter to provide the feature for supporting uninitialized
|
||||
Mixin for AttributeDocumenter to provide the feature for supporting runtime
|
||||
instance attributes (that are defined in __init__() methods with doc-comments).
|
||||
|
||||
Example:
|
||||
@@ -2139,38 +2213,69 @@ class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
|
||||
self.attr = None #: This is a target of this mix-in.
|
||||
"""
|
||||
|
||||
def get_attribute_comment(self, parent: Any) -> Optional[List[str]]:
|
||||
try:
|
||||
for cls in inspect.getmro(parent):
|
||||
try:
|
||||
module = safe_getattr(cls, '__module__')
|
||||
qualname = safe_getattr(cls, '__qualname__')
|
||||
RUNTIME_INSTANCE_ATTRIBUTE = object()
|
||||
|
||||
analyzer = ModuleAnalyzer.for_module(module)
|
||||
analyzer.analyze()
|
||||
if qualname and self.objpath:
|
||||
key = (qualname, self.objpath[-1])
|
||||
if key in analyzer.attr_docs:
|
||||
return list(analyzer.attr_docs[key])
|
||||
except (AttributeError, PycodeError):
|
||||
pass
|
||||
except (AttributeError, PycodeError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def is_uninitialized_instance_attribute(self, parent: Any) -> bool:
|
||||
def is_runtime_instance_attribute(self, parent: Any) -> bool:
|
||||
"""Check the subject is an attribute defined in __init__()."""
|
||||
# An instance variable defined in __init__().
|
||||
if self.get_attribute_comment(parent):
|
||||
if self.get_attribute_comment(parent, self.objpath[-1]): # type: ignore
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def import_object(self, raiseerror: bool = False) -> bool:
|
||||
"""Check the exisitence of uninitizlied instance attribute when failed to import
|
||||
the attribute.
|
||||
"""
|
||||
"""Check the existence of runtime instance attribute when failed to import the
|
||||
attribute."""
|
||||
try:
|
||||
return super().import_object(raiseerror=True) # type: ignore
|
||||
except ImportError as exc:
|
||||
try:
|
||||
with mock(self.config.autodoc_mock_imports):
|
||||
ret = import_object(self.modname, self.objpath[:-1], 'class',
|
||||
attrgetter=self.get_attr, # type: ignore
|
||||
warningiserror=self.config.autodoc_warningiserror)
|
||||
parent = ret[3]
|
||||
if self.is_runtime_instance_attribute(parent):
|
||||
self.object = self.RUNTIME_INSTANCE_ATTRIBUTE
|
||||
self.parent = parent
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if raiseerror:
|
||||
raise
|
||||
else:
|
||||
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
|
||||
self.env.note_reread()
|
||||
return False
|
||||
|
||||
def should_suppress_value_header(self) -> bool:
|
||||
return (self.object is self.RUNTIME_INSTANCE_ATTRIBUTE or
|
||||
super().should_suppress_value_header())
|
||||
|
||||
|
||||
class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
|
||||
"""
|
||||
Mixin for AttributeDocumenter to provide the feature for supporting uninitialized
|
||||
instance attributes (PEP-526 styled, annotation only attributes).
|
||||
|
||||
Example:
|
||||
|
||||
class Foo:
|
||||
attr: int #: This is a target of this mix-in.
|
||||
"""
|
||||
|
||||
def is_uninitialized_instance_attribute(self, parent: Any) -> bool:
|
||||
"""Check the subject is an annotation only attribute."""
|
||||
annotations = get_type_hints(parent, None, self.config.autodoc_type_aliases)
|
||||
if self.objpath[-1] in annotations:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def import_object(self, raiseerror: bool = False) -> bool:
|
||||
"""Check the exisitence of uninitialized instance attribute when failed to import
|
||||
the attribute."""
|
||||
try:
|
||||
return super().import_object(raiseerror=True) # type: ignore
|
||||
except ImportError as exc:
|
||||
@@ -2197,26 +2302,11 @@ class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
|
||||
return (self.object is UNINITIALIZED_ATTR or
|
||||
super().should_suppress_value_header())
|
||||
|
||||
def get_doc(self, ignore: int = None) -> List[List[str]]:
|
||||
if self.object is UNINITIALIZED_ATTR:
|
||||
comment = self.get_attribute_comment(self.parent)
|
||||
if comment:
|
||||
return [comment]
|
||||
|
||||
return super().get_doc(ignore) # type: ignore
|
||||
|
||||
def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
|
||||
) -> None:
|
||||
if self.object is UNINITIALIZED_ATTR:
|
||||
self.analyzer = None
|
||||
|
||||
super().add_content(more_content, no_docstring=no_docstring) # type: ignore
|
||||
|
||||
|
||||
class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: ignore
|
||||
TypeVarMixin, UninitializedInstanceAttributeMixin,
|
||||
NonDataDescriptorMixin, DocstringStripSignatureMixin,
|
||||
ClassLevelDocumenter):
|
||||
TypeVarMixin, RuntimeInstanceAttributeMixin,
|
||||
UninitializedInstanceAttributeMixin, NonDataDescriptorMixin,
|
||||
DocstringStripSignatureMixin, ClassLevelDocumenter):
|
||||
"""
|
||||
Specialized Documenter subclass for attributes.
|
||||
"""
|
||||
@@ -2271,7 +2361,8 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
|
||||
def update_annotations(self, parent: Any) -> None:
|
||||
"""Update __annotations__ to support type_comment and so on."""
|
||||
try:
|
||||
annotations = inspect.getannotations(parent)
|
||||
annotations = dict(inspect.getannotations(parent))
|
||||
parent.__annotations__ = annotations
|
||||
|
||||
for cls in inspect.getmro(parent):
|
||||
try:
|
||||
@@ -2282,28 +2373,19 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
|
||||
analyzer.analyze()
|
||||
for (classname, attrname), annotation in analyzer.annotations.items():
|
||||
if classname == qualname and attrname not in annotations:
|
||||
annotations[attrname] = annotation # type: ignore
|
||||
annotations[attrname] = annotation
|
||||
except (AttributeError, PycodeError):
|
||||
pass
|
||||
except AttributeError:
|
||||
pass
|
||||
except TypeError:
|
||||
# Failed to set __annotations__ (built-in, extensions, etc.)
|
||||
pass
|
||||
|
||||
def import_object(self, raiseerror: bool = False) -> bool:
|
||||
try:
|
||||
ret = super().import_object(raiseerror=True)
|
||||
if inspect.isenumattribute(self.object):
|
||||
self.object = self.object.value
|
||||
except ImportError as exc:
|
||||
if self.isinstanceattribute():
|
||||
self.object = INSTANCEATTR
|
||||
ret = True
|
||||
elif raiseerror:
|
||||
raise
|
||||
else:
|
||||
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
|
||||
self.env.note_reread()
|
||||
ret = False
|
||||
|
||||
ret = super().import_object(raiseerror)
|
||||
if inspect.isenumattribute(self.object):
|
||||
self.object = self.object.value
|
||||
if self.parent:
|
||||
self.update_annotations(self.parent)
|
||||
|
||||
@@ -2313,6 +2395,17 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
|
||||
return self.get_attr(self.parent or self.object, '__module__', None) \
|
||||
or self.modname
|
||||
|
||||
def should_suppress_value_header(self) -> bool:
|
||||
if super().should_suppress_value_header():
|
||||
return True
|
||||
else:
|
||||
doc = self.get_doc()
|
||||
metadata = extract_metadata('\n'.join(sum(doc, [])))
|
||||
if 'hide-value' in metadata:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_directive_header(self, sig: str) -> None:
|
||||
super().add_directive_header(sig)
|
||||
sourcename = self.get_sourcename()
|
||||
@@ -2328,8 +2421,7 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
|
||||
self.add_line(' :type: ' + objrepr, sourcename)
|
||||
|
||||
try:
|
||||
if (self.object is INSTANCEATTR or self.options.no_value or
|
||||
self.should_suppress_value_header()):
|
||||
if self.options.no_value or self.should_suppress_value_header():
|
||||
pass
|
||||
else:
|
||||
objrepr = object_description(self.object)
|
||||
@@ -2337,9 +2429,31 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def get_doc(self, ignore: int = None) -> List[List[str]]:
|
||||
if self.object is INSTANCEATTR:
|
||||
return []
|
||||
def get_attribute_comment(self, parent: Any, attrname: str) -> Optional[List[str]]:
|
||||
try:
|
||||
for cls in inspect.getmro(parent):
|
||||
try:
|
||||
module = safe_getattr(cls, '__module__')
|
||||
qualname = safe_getattr(cls, '__qualname__')
|
||||
|
||||
analyzer = ModuleAnalyzer.for_module(module)
|
||||
analyzer.analyze()
|
||||
if qualname and self.objpath:
|
||||
key = (qualname, attrname)
|
||||
if key in analyzer.attr_docs:
|
||||
return list(analyzer.attr_docs[key])
|
||||
except (AttributeError, PycodeError):
|
||||
pass
|
||||
except (AttributeError, PycodeError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
|
||||
# Check the attribute has a docstring-comment
|
||||
comment = self.get_attribute_comment(self.parent, self.objpath[-1])
|
||||
if comment:
|
||||
return [comment]
|
||||
|
||||
try:
|
||||
# Disable `autodoc_inherit_docstring` temporarily to avoid to obtain
|
||||
@@ -2353,6 +2467,10 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
|
||||
|
||||
def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
|
||||
) -> None:
|
||||
# Disable analyzing attribute comment on Documenter.add_content() to control it on
|
||||
# AttributeDocumenter.add_content()
|
||||
self.analyzer = None
|
||||
|
||||
if more_content is None:
|
||||
more_content = StringList()
|
||||
self.update_content(more_content)
|
||||
|
@@ -13,6 +13,7 @@ import traceback
|
||||
import warnings
|
||||
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple
|
||||
|
||||
from sphinx.deprecation import RemovedInSphinx50Warning
|
||||
from sphinx.pycode import ModuleAnalyzer, PycodeError
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.inspect import (getannotations, getmro, getslots, isclass, isenumclass,
|
||||
@@ -140,6 +141,9 @@ def get_module_members(module: Any) -> List[Tuple[str, Any]]:
|
||||
"""Get members of target module."""
|
||||
from sphinx.ext.autodoc import INSTANCEATTR
|
||||
|
||||
warnings.warn('sphinx.ext.autodoc.importer.get_module_members() is deprecated.',
|
||||
RemovedInSphinx50Warning)
|
||||
|
||||
members = {} # type: Dict[str, Tuple[str, Any]]
|
||||
for name in dir(module):
|
||||
try:
|
||||
|
@@ -17,6 +17,7 @@ from types import FunctionType, MethodType, ModuleType
|
||||
from typing import Any, Generator, Iterator, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.inspect import safe_getattr
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -147,3 +148,24 @@ def mock(modnames: List[str]) -> Generator[None, None, None]:
|
||||
finally:
|
||||
sys.meta_path.remove(finder)
|
||||
finder.invalidate_caches()
|
||||
|
||||
|
||||
def ismock(subject: Any) -> bool:
|
||||
"""Check if the object is mocked."""
|
||||
# check the object has '__sphinx_mock__' attribute
|
||||
if not hasattr(subject, '__sphinx_mock__'):
|
||||
return False
|
||||
|
||||
# check the object is mocked module
|
||||
if isinstance(subject, _MockModule):
|
||||
return True
|
||||
|
||||
try:
|
||||
# check the object is mocked object
|
||||
__mro__ = safe_getattr(type(subject), '__mro__', [])
|
||||
if len(__mro__) > 2 and __mro__[1] is _MockObject:
|
||||
return True
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
@@ -40,6 +40,7 @@ from sphinx.builders import Builder
|
||||
from sphinx.config import Config
|
||||
from sphinx.deprecation import RemovedInSphinx50Warning
|
||||
from sphinx.ext.autodoc import Documenter
|
||||
from sphinx.ext.autodoc.importer import import_module
|
||||
from sphinx.ext.autosummary import get_documenter, import_by_name, import_ivar_by_name
|
||||
from sphinx.locale import __
|
||||
from sphinx.pycode import ModuleAnalyzer, PycodeError
|
||||
@@ -281,6 +282,13 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
|
||||
items = [] # type: List[str]
|
||||
for _, modname, ispkg in pkgutil.iter_modules(obj.__path__):
|
||||
fullname = name + '.' + modname
|
||||
try:
|
||||
module = import_module(fullname)
|
||||
if module and hasattr(module, '__sphinx_mock__'):
|
||||
continue
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
items.append(fullname)
|
||||
public = [x for x in items if not x.split('.')[-1].startswith('_')]
|
||||
return public, items
|
||||
|
@@ -245,3 +245,15 @@ def tempdir(tmpdir: str) -> "util.path":
|
||||
this fixture is for compat with old test implementation.
|
||||
"""
|
||||
return util.path(tmpdir)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rollback_sysmodules():
|
||||
"""Rollback sys.modules to before testing to unload modules during tests."""
|
||||
try:
|
||||
sysmodules = list(sys.modules)
|
||||
yield
|
||||
finally:
|
||||
for modname in list(sys.modules):
|
||||
if modname not in sysmodules:
|
||||
sys.modules.pop(modname)
|
||||
|
@@ -187,6 +187,7 @@ def getslots(obj: Any) -> Optional[Dict]:
|
||||
|
||||
Return None if gienv *obj* does not have __slots__.
|
||||
Raises AttributeError if given *obj* raises an error on accessing __slots__.
|
||||
Raises TypeError if given *obj* is not a class.
|
||||
Raises ValueError if given *obj* have invalid __slots__.
|
||||
"""
|
||||
if not inspect.isclass(obj):
|
||||
|
@@ -150,6 +150,8 @@ def _restify_py37(cls: Optional["Type"]) -> str:
|
||||
return ':obj:`%s`' % cls._name
|
||||
else:
|
||||
return ':obj:`%s.%s`' % (cls.__module__, cls._name)
|
||||
elif isinstance(cls, ForwardRef):
|
||||
return ':class:`%s`' % cls.__forward_arg__
|
||||
else:
|
||||
# not a class (ex. TypeVar)
|
||||
return ':obj:`%s.%s`' % (cls.__module__, cls.__name__)
|
||||
|
@@ -27,3 +27,6 @@ class Qux:
|
||||
class Quux(List[Union[int, float]]):
|
||||
"""A subclass of List[Union[int, float]]"""
|
||||
pass
|
||||
|
||||
|
||||
Alias = Foo
|
||||
|
16
tests/roots/test-ext-autodoc/target/empty_all.py
Normal file
16
tests/roots/test-ext-autodoc/target/empty_all.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
docsting of empty_all module.
|
||||
"""
|
||||
__all__ = []
|
||||
|
||||
|
||||
def foo():
|
||||
"""docstring"""
|
||||
|
||||
|
||||
def bar():
|
||||
"""docstring"""
|
||||
|
||||
|
||||
def baz():
|
||||
"""docstring"""
|
19
tests/roots/test-ext-autodoc/target/hide_value.py
Normal file
19
tests/roots/test-ext-autodoc/target/hide_value.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#: docstring
|
||||
#:
|
||||
#: :meta hide-value:
|
||||
SENTINEL1 = object()
|
||||
|
||||
#: :meta hide-value:
|
||||
SENTINEL2 = object()
|
||||
|
||||
|
||||
class Foo:
|
||||
"""docstring"""
|
||||
|
||||
#: docstring
|
||||
#:
|
||||
#: :meta hide-value:
|
||||
SENTINEL1 = object()
|
||||
|
||||
#: :meta hide-value:
|
||||
SENTINEL2 = object()
|
@@ -28,4 +28,9 @@ class TestAutodoc(object):
|
||||
return None
|
||||
|
||||
|
||||
class Inherited(missing_module.Class):
|
||||
"""docstring"""
|
||||
pass
|
||||
|
||||
|
||||
sphinx.missing_module4.missing_function(len(missing_name2))
|
||||
|
@@ -9,3 +9,7 @@ def _public_function(name):
|
||||
|
||||
:meta public:
|
||||
"""
|
||||
|
||||
|
||||
PRIVATE_CONSTANT = None #: :meta private:
|
||||
_PUBLIC_CONSTANT = None #: :meta public:
|
||||
|
1
tests/roots/test-linkcheck-localserver-two-links/conf.py
Normal file
1
tests/roots/test-linkcheck-localserver-two-links/conf.py
Normal file
@@ -0,0 +1 @@
|
||||
exclude_patterns = ['_build']
|
@@ -0,0 +1,6 @@
|
||||
.. image:: http://localhost:7777/
|
||||
:target: http://localhost:7777/
|
||||
|
||||
`weblate.org`_
|
||||
|
||||
.. _weblate.org: http://localhost:7777/
|
@@ -573,3 +573,40 @@ def test_limit_rate_bails_out_after_waiting_max_time(app):
|
||||
checker.rate_limits = {"localhost": RateLimit(90.0, 0.0)}
|
||||
next_check = checker.limit_rate(FakeResponse())
|
||||
assert next_check is None
|
||||
|
||||
|
||||
@pytest.mark.sphinx(
|
||||
'linkcheck', testroot='linkcheck-localserver-two-links', freshenv=True,
|
||||
)
|
||||
def test_priorityqueue_items_are_comparable(app):
|
||||
with http_server(OKHandler):
|
||||
app.builder.build_all()
|
||||
content = (app.outdir / 'output.json').read_text()
|
||||
rows = [json.loads(x) for x in sorted(content.splitlines())]
|
||||
assert rows == [
|
||||
{
|
||||
'filename': 'index.rst',
|
||||
# Should not be None.
|
||||
'lineno': 0,
|
||||
'status': 'working',
|
||||
'code': 0,
|
||||
'uri': 'http://localhost:7777/',
|
||||
'info': '',
|
||||
},
|
||||
{
|
||||
'filename': 'index.rst',
|
||||
'lineno': 0,
|
||||
'status': 'working',
|
||||
'code': 0,
|
||||
'uri': 'http://localhost:7777/',
|
||||
'info': '',
|
||||
},
|
||||
{
|
||||
'filename': 'index.rst',
|
||||
'lineno': 4,
|
||||
'status': 'working',
|
||||
'code': 0,
|
||||
'uri': 'http://localhost:7777/',
|
||||
'info': '',
|
||||
}
|
||||
]
|
||||
|
@@ -23,7 +23,7 @@ def test_create_single_index(app):
|
||||
".. index:: Sphinx\n"
|
||||
".. index:: Ель\n"
|
||||
".. index:: ёлка\n"
|
||||
".. index:: תירבע\n"
|
||||
".. index:: עברית\n"
|
||||
".. index:: 9-symbol\n"
|
||||
".. index:: &-symbol\n"
|
||||
".. index:: £100\n")
|
||||
@@ -41,7 +41,9 @@ def test_create_single_index(app):
|
||||
assert index[4] == ('Е',
|
||||
[('ёлка', [[('', '#index-6')], [], None]),
|
||||
('Ель', [[('', '#index-5')], [], None])])
|
||||
assert index[5] == ('ת', [('תירבע', [[('', '#index-7')], [], None])])
|
||||
# Here the word starts with U+200F RIGHT-TO-LEFT MARK, which should be
|
||||
# ignored when getting the first letter.
|
||||
assert index[5] == ('ע', [('עברית', [[('', '#index-7')], [], None])])
|
||||
|
||||
|
||||
@pytest.mark.sphinx('dummy', freshenv=True)
|
||||
|
@@ -689,6 +689,7 @@ def test_autodoc_special_members(app):
|
||||
actual = do_autodoc(app, 'class', 'target.Class', options)
|
||||
assert list(filter(lambda l: '::' in l, actual)) == [
|
||||
'.. py:class:: Class(arg)',
|
||||
' .. py:attribute:: Class.__annotations__',
|
||||
' .. py:attribute:: Class.__dict__',
|
||||
' .. py:method:: Class.__init__(arg)',
|
||||
' .. py:attribute:: Class.__module__',
|
||||
@@ -2223,3 +2224,49 @@ def test_name_mangling(app):
|
||||
' name of Foo',
|
||||
'',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
|
||||
@pytest.mark.sphinx('html', testroot='ext-autodoc')
|
||||
def test_hide_value(app):
|
||||
options = {'members': True}
|
||||
actual = do_autodoc(app, 'module', 'target.hide_value', options)
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:module:: target.hide_value',
|
||||
'',
|
||||
'',
|
||||
'.. py:class:: Foo()',
|
||||
' :module: target.hide_value',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
'',
|
||||
' .. py:attribute:: Foo.SENTINEL1',
|
||||
' :module: target.hide_value',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
' :meta hide-value:',
|
||||
'',
|
||||
'',
|
||||
' .. py:attribute:: Foo.SENTINEL2',
|
||||
' :module: target.hide_value',
|
||||
'',
|
||||
' :meta hide-value:',
|
||||
'',
|
||||
'',
|
||||
'.. py:data:: SENTINEL1',
|
||||
' :module: target.hide_value',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
' :meta hide-value:',
|
||||
'',
|
||||
'',
|
||||
'.. py:data:: SENTINEL2',
|
||||
' :module: target.hide_value',
|
||||
'',
|
||||
' :meta hide-value:',
|
||||
'',
|
||||
]
|
||||
|
@@ -189,3 +189,29 @@ def test_autoattribute_TypeVar(app):
|
||||
" alias of TypeVar('T1')",
|
||||
'',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
|
||||
@pytest.mark.sphinx('html', testroot='ext-autodoc')
|
||||
def test_autoattribute_hide_value(app):
|
||||
actual = do_autodoc(app, 'attribute', 'target.hide_value.Foo.SENTINEL1')
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:attribute:: Foo.SENTINEL1',
|
||||
' :module: target.hide_value',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
' :meta hide-value:',
|
||||
'',
|
||||
]
|
||||
|
||||
actual = do_autodoc(app, 'attribute', 'target.hide_value.Foo.SENTINEL2')
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:attribute:: Foo.SENTINEL2',
|
||||
' :module: target.hide_value',
|
||||
'',
|
||||
' :meta hide-value:',
|
||||
'',
|
||||
]
|
||||
|
@@ -173,3 +173,21 @@ def test_show_inheritance_for_subclass_of_generic_type(app):
|
||||
' A subclass of List[Union[int, float]]',
|
||||
'',
|
||||
]
|
||||
|
||||
|
||||
def test_class_alias(app):
|
||||
def autodoc_process_docstring(*args):
|
||||
"""A handler always raises an error.
|
||||
This confirms this handler is never called for class aliases.
|
||||
"""
|
||||
raise
|
||||
|
||||
app.connect('autodoc-process-docstring', autodoc_process_docstring)
|
||||
actual = do_autodoc(app, 'class', 'target.classes.Alias')
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:attribute:: Alias',
|
||||
' :module: target.classes',
|
||||
'',
|
||||
' alias of :class:`target.classes.Foo`',
|
||||
]
|
||||
|
@@ -129,3 +129,29 @@ def test_autodata_TypeVar(app):
|
||||
" alias of TypeVar('T1')",
|
||||
'',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
|
||||
@pytest.mark.sphinx('html', testroot='ext-autodoc')
|
||||
def test_autodata_hide_value(app):
|
||||
actual = do_autodoc(app, 'data', 'target.hide_value.SENTINEL1')
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:data:: SENTINEL1',
|
||||
' :module: target.hide_value',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
' :meta hide-value:',
|
||||
'',
|
||||
]
|
||||
|
||||
actual = do_autodoc(app, 'data', 'target.hide_value.SENTINEL2')
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:data:: SENTINEL2',
|
||||
' :module: target.hide_value',
|
||||
'',
|
||||
' :meta hide-value:',
|
||||
'',
|
||||
]
|
||||
|
44
tests/test_ext_autodoc_automodule.py
Normal file
44
tests/test_ext_autodoc_automodule.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
test_ext_autodoc_autocmodule
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Test the autodoc extension. This tests mainly the Documenters; the auto
|
||||
directives are tested in a test source file translated by test_build.
|
||||
|
||||
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from .test_ext_autodoc import do_autodoc
|
||||
|
||||
|
||||
@pytest.mark.sphinx('html', testroot='ext-autodoc')
|
||||
def test_empty_all(app):
|
||||
options = {'members': True}
|
||||
actual = do_autodoc(app, 'module', 'target.empty_all', options)
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:module:: target.empty_all',
|
||||
'',
|
||||
'docsting of empty_all module.',
|
||||
'',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.sphinx('html', testroot='ext-autodoc',
|
||||
confoverrides={'autodoc_mock_imports': ['missing_module',
|
||||
'missing_package1',
|
||||
'missing_package2',
|
||||
'missing_package3',
|
||||
'sphinx.missing_module4']})
|
||||
@pytest.mark.usefixtures("rollback_sysmodules")
|
||||
def test_subclass_of_mocked_object(app):
|
||||
sys.modules.pop('target', None) # unload target module to clear the module cache
|
||||
|
||||
options = {'members': True}
|
||||
actual = do_autodoc(app, 'module', 'target.need_mocks', options)
|
||||
assert '.. py:class:: Inherited(*args: Any, **kwargs: Any)' in actual
|
@@ -429,7 +429,10 @@ def test_autoclass_content_and_docstring_signature_both(app):
|
||||
|
||||
|
||||
@pytest.mark.sphinx('html', testroot='ext-autodoc')
|
||||
@pytest.mark.usefixtures("rollback_sysmodules")
|
||||
def test_mocked_module_imports(app, warning):
|
||||
sys.modules.pop('target', None) # unload target module to clear the module cache
|
||||
|
||||
# no autodoc_mock_imports
|
||||
options = {"members": 'TestAutodoc,decoratedFunction,func'}
|
||||
actual = do_autodoc(app, 'module', 'target.need_mocks', options)
|
||||
|
@@ -15,7 +15,7 @@ from typing import TypeVar
|
||||
|
||||
import pytest
|
||||
|
||||
from sphinx.ext.autodoc.mock import _MockModule, _MockObject, mock
|
||||
from sphinx.ext.autodoc.mock import _MockModule, _MockObject, ismock, mock
|
||||
|
||||
|
||||
def test_MockModule():
|
||||
@@ -129,3 +129,19 @@ def test_mock_decorator():
|
||||
assert func.__doc__ == "docstring"
|
||||
assert Foo.meth.__doc__ == "docstring"
|
||||
assert Bar.__doc__ == "docstring"
|
||||
|
||||
|
||||
def test_ismock():
|
||||
with mock(['sphinx.unknown']):
|
||||
mod1 = import_module('sphinx.unknown')
|
||||
mod2 = import_module('sphinx.application')
|
||||
|
||||
class Inherited(mod1.Class):
|
||||
pass
|
||||
|
||||
assert ismock(mod1) is True
|
||||
assert ismock(mod1.Class) is True
|
||||
assert ismock(Inherited) is False
|
||||
|
||||
assert ismock(mod2) is False
|
||||
assert ismock(mod2.Sphinx) is False
|
||||
|
@@ -23,6 +23,13 @@ def test_private_field(app):
|
||||
'.. py:module:: target.private',
|
||||
'',
|
||||
'',
|
||||
'.. py:data:: _PUBLIC_CONSTANT',
|
||||
' :module: target.private',
|
||||
' :value: None',
|
||||
'',
|
||||
' :meta public:',
|
||||
'',
|
||||
'',
|
||||
'.. py:function:: _public_function(name)',
|
||||
' :module: target.private',
|
||||
'',
|
||||
@@ -44,6 +51,20 @@ def test_private_field_and_private_members(app):
|
||||
'.. py:module:: target.private',
|
||||
'',
|
||||
'',
|
||||
'.. py:data:: PRIVATE_CONSTANT',
|
||||
' :module: target.private',
|
||||
' :value: None',
|
||||
'',
|
||||
' :meta private:',
|
||||
'',
|
||||
'',
|
||||
'.. py:data:: _PUBLIC_CONSTANT',
|
||||
' :module: target.private',
|
||||
' :value: None',
|
||||
'',
|
||||
' :meta public:',
|
||||
'',
|
||||
'',
|
||||
'.. py:function:: _public_function(name)',
|
||||
' :module: target.private',
|
||||
'',
|
||||
@@ -66,13 +87,20 @@ def test_private_field_and_private_members(app):
|
||||
def test_private_members(app):
|
||||
app.config.autoclass_content = 'class'
|
||||
options = {"members": None,
|
||||
"private-members": "_public_function"}
|
||||
"private-members": "_PUBLIC_CONSTANT,_public_function"}
|
||||
actual = do_autodoc(app, 'module', 'target.private', options)
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:module:: target.private',
|
||||
'',
|
||||
'',
|
||||
'.. py:data:: _PUBLIC_CONSTANT',
|
||||
' :module: target.private',
|
||||
' :value: None',
|
||||
'',
|
||||
' :meta public:',
|
||||
'',
|
||||
'',
|
||||
'.. py:function:: _public_function(name)',
|
||||
' :module: target.private',
|
||||
'',
|
||||
|
@@ -379,27 +379,47 @@ def test_autosummary_generate_overwrite2(app_params, make_app):
|
||||
|
||||
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-recursive')
|
||||
def test_autosummary_recursive(app, status, warning):
|
||||
app.build()
|
||||
try:
|
||||
app.build()
|
||||
|
||||
# autosummary having :recursive: option
|
||||
assert (app.srcdir / 'generated' / 'package.rst').exists()
|
||||
assert (app.srcdir / 'generated' / 'package.module.rst').exists()
|
||||
assert (app.srcdir / 'generated' / 'package.module_importfail.rst').exists() is False
|
||||
assert (app.srcdir / 'generated' / 'package.package.rst').exists()
|
||||
assert (app.srcdir / 'generated' / 'package.package.module.rst').exists()
|
||||
# autosummary having :recursive: option
|
||||
assert (app.srcdir / 'generated' / 'package.rst').exists()
|
||||
assert (app.srcdir / 'generated' / 'package.module.rst').exists()
|
||||
assert (app.srcdir / 'generated' / 'package.module_importfail.rst').exists() is False
|
||||
assert (app.srcdir / 'generated' / 'package.package.rst').exists()
|
||||
assert (app.srcdir / 'generated' / 'package.package.module.rst').exists()
|
||||
|
||||
# autosummary not having :recursive: option
|
||||
assert (app.srcdir / 'generated' / 'package2.rst').exists()
|
||||
assert (app.srcdir / 'generated' / 'package2.module.rst').exists() is False
|
||||
# autosummary not having :recursive: option
|
||||
assert (app.srcdir / 'generated' / 'package2.rst').exists()
|
||||
assert (app.srcdir / 'generated' / 'package2.module.rst').exists() is False
|
||||
|
||||
# Check content of recursively generated stub-files
|
||||
content = (app.srcdir / 'generated' / 'package.rst').read_text()
|
||||
assert 'package.module' in content
|
||||
assert 'package.package' in content
|
||||
assert 'package.module_importfail' in content
|
||||
# Check content of recursively generated stub-files
|
||||
content = (app.srcdir / 'generated' / 'package.rst').read_text()
|
||||
assert 'package.module' in content
|
||||
assert 'package.package' in content
|
||||
assert 'package.module_importfail' in content
|
||||
|
||||
content = (app.srcdir / 'generated' / 'package.package.rst').read_text()
|
||||
assert 'package.package.module' in content
|
||||
content = (app.srcdir / 'generated' / 'package.package.rst').read_text()
|
||||
assert 'package.package.module' in content
|
||||
finally:
|
||||
sys.modules.pop('package.package', None)
|
||||
sys.modules.pop('package.package.module', None)
|
||||
|
||||
|
||||
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-recursive',
|
||||
srcdir='test_autosummary_recursive_skips_mocked_modules',
|
||||
confoverrides={'autosummary_mock_imports': ['package.package']})
|
||||
def test_autosummary_recursive_skips_mocked_modules(app, status, warning):
|
||||
try:
|
||||
app.build()
|
||||
|
||||
assert (app.srcdir / 'generated' / 'package.rst').exists()
|
||||
assert (app.srcdir / 'generated' / 'package.module.rst').exists()
|
||||
assert (app.srcdir / 'generated' / 'package.package.rst').exists() is False
|
||||
assert (app.srcdir / 'generated' / 'package.package.module.rst').exists() is False
|
||||
finally:
|
||||
sys.modules.pop('package.package', None)
|
||||
sys.modules.pop('package.package.module', None)
|
||||
|
||||
|
||||
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-filename-map')
|
||||
|
@@ -109,6 +109,12 @@ def test_restify_type_hints_alias():
|
||||
assert restify(MyTuple) == ":class:`Tuple`\\ [:class:`str`, :class:`str`]" # type: ignore
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
|
||||
def test_restify_type_ForwardRef():
|
||||
from typing import ForwardRef # type: ignore
|
||||
assert restify(ForwardRef("myint")) == ":class:`myint`"
|
||||
|
||||
|
||||
def test_restify_broken_type_hints():
|
||||
assert restify(BrokenType) == ':class:`tests.test_util_typing.BrokenType`'
|
||||
|
||||
|
Reference in New Issue
Block a user