Merge branch '3.x'

This commit is contained in:
Takeshi KOMIYA
2020-12-29 18:37:45 +09:00
36 changed files with 728 additions and 156 deletions

View File

@@ -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
View File

@@ -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)
=====================================

View File

@@ -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/>`__

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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', []),

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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__)

View File

@@ -27,3 +27,6 @@ class Qux:
class Quux(List[Union[int, float]]):
"""A subclass of List[Union[int, float]]"""
pass
Alias = Foo

View File

@@ -0,0 +1,16 @@
"""
docsting of empty_all module.
"""
__all__ = []
def foo():
"""docstring"""
def bar():
"""docstring"""
def baz():
"""docstring"""

View 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()

View File

@@ -28,4 +28,9 @@ class TestAutodoc(object):
return None
class Inherited(missing_module.Class):
"""docstring"""
pass
sphinx.missing_module4.missing_function(len(missing_name2))

View File

@@ -9,3 +9,7 @@ def _public_function(name):
:meta public:
"""
PRIVATE_CONSTANT = None #: :meta private:
_PUBLIC_CONSTANT = None #: :meta public:

View File

@@ -0,0 +1 @@
exclude_patterns = ['_build']

View File

@@ -0,0 +1,6 @@
.. image:: http://localhost:7777/
:target: http://localhost:7777/
`weblate.org`_
.. _weblate.org: http://localhost:7777/

View File

@@ -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': '',
}
]

View File

@@ -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)

View File

@@ -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:',
'',
]

View File

@@ -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:',
'',
]

View File

@@ -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`',
]

View File

@@ -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:',
'',
]

View 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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',
'',

View File

@@ -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')

View File

@@ -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`'