Merge branch '3.x'

This commit is contained in:
Takeshi KOMIYA
2020-12-20 21:33:57 +09:00
26 changed files with 713 additions and 96 deletions

82
CHANGES
View File

@@ -54,7 +54,7 @@ Bugs fixed
Testing
--------
Release 3.4.0 (in development)
Release 3.5.0 (in development)
==============================
Dependencies
@@ -63,6 +63,45 @@ Dependencies
Incompatible changes
--------------------
Deprecated
----------
Features added
--------------
Bugs fixed
----------
Testing
--------
Release 3.4.1 (in development)
==============================
Dependencies
------------
Incompatible changes
--------------------
Deprecated
----------
Features added
--------------
Bugs fixed
----------
Testing
--------
Release 3.4.0 (released Dec 20, 2020)
=====================================
Incompatible changes
--------------------
* #8105: autodoc: the signature of class constructor will be shown for decorated
classes, not a signature of decorator
@@ -79,7 +118,9 @@ Deprecated
* ``sphinx.ext.autodoc.SlotsAttributeDocumenter``
* ``sphinx.ext.autodoc.TypeVarDocumenter``
* ``sphinx.ext.autodoc.importer._getannotations()``
* ``sphinx.ext.autodoc.importer._getmro()``
* ``sphinx.pycode.ModuleAnalyzer.parse()``
* ``sphinx.util.osutil.movefile()``
* ``sphinx.util.requests.is_ssl_error()``
Features added
@@ -98,6 +139,7 @@ Features added
* #8460: autodoc: Support custom types defined by typing.NewType
* #8285: napoleon: Add :confval:`napoleon_attr_annotations` to merge type hints
on source code automatically if any type is specified in docstring
* #8236: napoleon: Support numpydoc's "Receives" section
* #6914: Add a new event :event:`warn-missing-reference` to custom warning
messages when failed to resolve a cross-reference
* #6914: Emit a detailed warning when failed to resolve a ``:ref:`` reference
@@ -121,49 +163,37 @@ Bugs fixed
attributes
* #8503: autodoc: autoattribute could not create document for a GenericAlias as
class attributes correctly
* #8534: autodoc: autoattribute could not create document for a commented
attribute in alias class
* #8452: autodoc: autodoc_type_aliases doesn't work when autodoc_typehints is
set to "description"
* #8541: autodoc: autodoc_type_aliases doesn't work for the type annotation to
instance attributes
* #8460: autodoc: autodata and autoattribute directives do not display type
information of TypeVars
* #8493: autodoc: references to builtins not working in class aliases
* #8522: autodoc: ``__bool__`` method could be called
* #8067: autodoc: A typehint for the instance variable having type_comment on
super class is not displayed
* #8545: autodoc: a __slots__ attribute is not documented even having docstring
* #741: autodoc: inherited-members doesn't work for instance attributes on super
class
* #8477: autosummary: non utf-8 reST files are generated when template contains
multibyte characters
* #8501: autosummary: summary extraction splits text after "el at." unexpectedly
* #8524: html: Wrong url_root has been generated on a document named "index"
* #8419: html search: Do not load ``language_data.js`` in non-search pages
* #8549: i18n: ``-D gettext_compact=0`` is no longer working
* #8454: graphviz: The layout option for graph and digraph directives don't work
* #8131: linkcheck: Use GET when HEAD requests cause Too Many Redirects, to
accommodate infinite redirect loops on HEAD
* #8437: Makefile: ``make clean`` with empty BUILDDIR is dangerous
* #8365: py domain: ``:type:`` and ``:rtype:`` gives false ambiguous class
lookup warnings
* #8352: std domain: Failed to parse an option that starts with bracket
* #8519: LaTeX: Prevent page brake in the middle of a seealso
Testing
--------
Release 3.3.2 (in development)
==============================
Dependencies
------------
Incompatible changes
--------------------
Deprecated
----------
Features added
--------------
Bugs fixed
----------
* #8520: C, fix copying of AliasNode.
Testing
--------
Release 3.3.1 (released Nov 12, 2020)
=====================================

View File

@@ -14,4 +14,5 @@ You can also browse it from this repository from
``doc/internals/contributing.rst``
Sphinx uses GitHub to host source code, track patches and bugs, and more.
Please make an effort to provide as much possible when filing bugs.
Please make an effort to provide as much detail as possible when filing
bugs.

View File

@@ -102,11 +102,21 @@ The following is a list of deprecated interfaces.
- 4.0
- ``sphinx.util.inspect.getannotations()``
* - ``sphinx.ext.autodoc.importer._getmro()``
- 3.4
- 4.0
- ``sphinx.util.inspect.getmro()``
* - ``sphinx.pycode.ModuleAnalyzer.parse()``
- 3.4
- 5.0
- ``sphinx.pycode.ModuleAnalyzer.analyze()``
* - ``sphinx.util.osutil.movefile()``
- 3.4
- 5.0
- ``os.replace()``
* - ``sphinx.util.requests.is_ssl_error()``
- 3.4
- 5.0

View File

@@ -290,7 +290,7 @@ class MessageCatalogBuilder(I18nBuilder):
def setup(app: Sphinx) -> Dict[str, Any]:
app.add_builder(MessageCatalogBuilder)
app.add_config_value('gettext_compact', True, 'gettext', Any)
app.add_config_value('gettext_compact', True, 'gettext', {bool, str})
app.add_config_value('gettext_location', True, 'gettext')
app.add_config_value('gettext_uuid', False, 'gettext')
app.add_config_value('gettext_auto_build', True, 'env')

View File

@@ -9,6 +9,7 @@
"""
import html
import os
import posixpath
import re
import sys
@@ -43,7 +44,7 @@ from sphinx.util.fileutil import copy_asset
from sphinx.util.i18n import format_date
from sphinx.util.inventory import InventoryFile
from sphinx.util.matching import DOTFILES, Matcher, patmatch
from sphinx.util.osutil import copyfile, ensuredir, movefile, os_path, relative_uri
from sphinx.util.osutil import copyfile, ensuredir, os_path, relative_uri
from sphinx.util.tags import Tags
from sphinx.writers.html import HTMLTranslator, HTMLWriter
@@ -1064,7 +1065,7 @@ class StandaloneHTMLBuilder(Builder):
else:
with open(searchindexfn + '.tmp', 'wb') as fb:
self.indexer.dump(fb, self.indexer_format)
movefile(searchindexfn + '.tmp', searchindexfn)
os.replace(searchindexfn + '.tmp', searchindexfn)
def convert_html_css_files(app: Sphinx, config: Config) -> None:

View File

@@ -173,6 +173,14 @@ class Config:
defvalue = self.values[name][0]
if self.values[name][2] == Any:
return value
elif self.values[name][2] == {bool, str}:
if value == '0':
# given falsy string from command line option
return False
elif value == '1':
return True
else:
return value
elif type(defvalue) is bool or self.values[name][2] == [bool]:
if value == '0':
# given falsy string from command line option

View File

@@ -271,6 +271,8 @@ class PyXrefMixin:
result = super().make_xref(rolename, domain, target, # type: ignore
innernode, contnode, env)
result['refspecific'] = True
result['py:module'] = env.ref_context.get('py:module')
result['py:class'] = env.ref_context.get('py:class')
if target.startswith(('.', '~')):
prefix, result['reftarget'] = target[0], target[1:]
if prefix == '.':

View File

@@ -25,7 +25,8 @@ 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_module_members, get_object_members, import_object
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.locale import _, __
from sphinx.pycode import ModuleAnalyzer, PycodeError
@@ -270,9 +271,11 @@ class ObjectMember(tuple):
def __new__(cls, name: str, obj: Any, **kwargs: Any) -> Any:
return super().__new__(cls, (name, obj)) # type: ignore
def __init__(self, name: str, obj: Any, skipped: bool = False) -> None:
def __init__(self, name: str, obj: Any, docstring: Optional[str] = None,
skipped: bool = False) -> None:
self.__name__ = name
self.object = obj
self.docstring = docstring
self.skipped = skipped
@@ -700,6 +703,11 @@ class Documenter:
cls_doc = self.get_attr(cls, '__doc__', None)
if cls_doc == doc:
doc = None
if isinstance(obj, ObjectMember) and obj.docstring:
# hack for ClassDocumenter to inject docstring via ObjectMember
doc = obj.docstring
has_doc = bool(doc)
metadata = extract_metadata(doc)
@@ -1559,7 +1567,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]:
members = get_object_members(self.object, self.objpath, self.get_attr, self.analyzer)
members = get_class_members(self.object, self.objpath, self.get_attr)
if not want_all:
if not self.options.members:
return False, [] # type: ignore
@@ -1567,16 +1575,18 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
selected = []
for name in self.options.members: # type: str
if name in members:
selected.append((name, members[name].value))
selected.append(ObjectMember(name, members[name].value,
docstring=members[name].docstring))
else:
logger.warning(__('missing attribute %s in object %s') %
(name, self.fullname), type='autodoc')
return False, selected
elif self.options.inherited_members:
return False, [(m.name, m.value) for m in members.values()]
return False, [ObjectMember(m.name, m.value, docstring=m.docstring)
for m in members.values()]
else:
return False, [(m.name, m.value) for m in members.values()
if m.directly_defined]
return False, [ObjectMember(m.name, m.value, docstring=m.docstring)
for m in members.values() if m.class_ == self.object]
def get_doc(self, ignore: int = None) -> List[List[str]]:
if self.doc_as_attr:
@@ -1822,6 +1832,26 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
) -> bool:
return isinstance(parent, ModuleDocumenter) and isattr
def update_annotations(self, parent: Any) -> None:
"""Update __annotations__ to support type_comment and so on."""
try:
annotations = inspect.getannotations(parent)
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
except AttributeError:
pass
def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror)
if self.parent:
self.update_annotations(self.parent)
return ret
def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
sourcename = self.get_sourcename()
@@ -1836,11 +1866,6 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
if self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
else:
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
if self.analyzer and key in self.analyzer.annotations:
self.add_line(' :type: ' + self.analyzer.annotations[key],
sourcename)
try:
if self.options.no_value or self.should_suppress_value_header():
@@ -2033,6 +2058,28 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
return
class NonDataDescriptorMixin(DataDocumenterMixinBase):
"""
Mixin for AttributeDocumenter to provide the feature for supporting non
data-descriptors.
.. note:: This mix-in must be inherited after other mix-ins. Otherwise, docstring
and :value: header will be suppressed unexpectedly.
"""
def should_suppress_value_header(self) -> bool:
return (inspect.isattributedescriptor(self.object) or
super().should_suppress_directive_header())
def get_doc(self, ignore: int = None) -> List[List[str]]:
if not inspect.isattributedescriptor(self.object):
# the docstring of non datadescriptor is very probably the wrong thing
# to display
return []
else:
return super().get_doc(ignore) # type: ignore
class SlotsMixin(DataDocumenterMixinBase):
"""
Mixin for AttributeDocumenter to provide the feature for supporting __slots__.
@@ -2080,8 +2127,96 @@ class SlotsMixin(DataDocumenterMixinBase):
return super().get_doc(ignore) # type: ignore
class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
"""
Mixin for AttributeDocumenter to provide the feature for supporting uninitialized
instance attributes (that are defined in __init__() methods with doc-comments).
Example:
class Foo:
def __init__(self):
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__')
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:
"""Check the subject is an attribute defined in __init__()."""
# An instance variable defined in __init__().
if self.get_attribute_comment(parent):
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.
"""
try:
return super().import_object(raiseerror=True) # type: ignore
except ImportError as exc:
try:
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_uninitialized_instance_attribute(parent):
self.object = UNINITIALIZED_ATTR
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 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, DocstringStripSignatureMixin, ClassLevelDocumenter):
TypeVarMixin, UninitializedInstanceAttributeMixin,
NonDataDescriptorMixin, DocstringStripSignatureMixin,
ClassLevelDocumenter):
"""
Specialized Documenter subclass for attributes.
"""
@@ -2131,35 +2266,36 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
except ImportError:
pass
# An instance variable defined inside __init__().
try:
analyzer = ModuleAnalyzer.for_module(self.modname)
attr_docs = analyzer.find_attr_docs()
if self.objpath:
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
if key in attr_docs:
return True
return False
except PycodeError:
pass
return False
def update_annotations(self, parent: Any) -> None:
"""Update __annotations__ to support type_comment and so on."""
try:
annotations = inspect.getannotations(parent)
for cls in inspect.getmro(parent):
try:
module = safe_getattr(cls, '__module__')
qualname = safe_getattr(cls, '__qualname__')
analyzer = ModuleAnalyzer.for_module(module)
analyzer.analyze()
for (classname, attrname), annotation in analyzer.annotations.items():
if classname == qualname and attrname not in annotations:
annotations[attrname] = annotation # type: ignore
except (AttributeError, PycodeError):
pass
except AttributeError:
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
if inspect.isattributedescriptor(self.object):
self._datadescriptor = True
else:
# if it's not a data descriptor
self._datadescriptor = False
except ImportError as exc:
if self.isinstanceattribute():
self.object = INSTANCEATTR
self._datadescriptor = False
ret = True
elif raiseerror:
raise
@@ -2168,6 +2304,9 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
self.env.note_reread()
ret = False
if self.parent:
self.update_annotations(self.parent)
return ret
def get_real_modname(self) -> str:
@@ -2187,27 +2326,19 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
if self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
else:
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
if self.analyzer and key in self.analyzer.annotations:
self.add_line(' :type: ' + self.analyzer.annotations[key],
sourcename)
# data descriptors do not have useful values
if not self._datadescriptor:
try:
if self.object is INSTANCEATTR or self.options.no_value:
pass
else:
objrepr = object_description(self.object)
self.add_line(' :value: ' + objrepr, sourcename)
except ValueError:
try:
if (self.object is INSTANCEATTR or self.options.no_value or
self.should_suppress_value_header()):
pass
else:
objrepr = object_description(self.object)
self.add_line(' :value: ' + objrepr, sourcename)
except ValueError:
pass
def get_doc(self, ignore: int = None) -> List[List[str]]:
if not self._datadescriptor:
# if it's not a data descriptor, its docstring is very probably the
# wrong thing to display
if self.object is INSTANCEATTR:
return []
try:

View File

@@ -13,9 +13,10 @@ import traceback
import warnings
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple
from sphinx.pycode import ModuleAnalyzer
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import logging
from sphinx.util.inspect import getannotations, getslots, isclass, isenumclass, safe_getattr
from sphinx.util.inspect import (getannotations, getmro, getslots, isclass, isenumclass,
safe_getattr)
if False:
# For type annotation
@@ -165,12 +166,9 @@ class Attribute(NamedTuple):
def _getmro(obj: Any) -> Tuple["Type", ...]:
"""Get __mro__ from given *obj* safely."""
__mro__ = safe_getattr(obj, '__mro__', None)
if isinstance(__mro__, tuple):
return __mro__
else:
return tuple()
warnings.warn('sphinx.ext.autodoc.importer._getmro() is deprecated.',
RemovedInSphinx40Warning)
return getmro(obj)
def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
@@ -218,7 +216,7 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
continue
# annotation only member (ex. attr: int)
for i, cls in enumerate(_getmro(subject)):
for i, cls in enumerate(getmro(subject)):
try:
for name in getannotations(cls):
name = unmangle(cls, name)
@@ -235,3 +233,88 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
members[name] = Attribute(name, True, INSTANCEATTR)
return members
class ClassAttribute:
"""The attribute of the class."""
def __init__(self, cls: Any, name: str, value: Any, docstring: Optional[str] = None):
self.class_ = cls
self.name = name
self.value = value
self.docstring = docstring
def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable
) -> Dict[str, ClassAttribute]:
"""Get members and attributes of target class."""
from sphinx.ext.autodoc import INSTANCEATTR
# the members directly defined in the class
obj_dict = attrgetter(subject, '__dict__', {})
members = {} # type: Dict[str, ClassAttribute]
# enum members
if isenumclass(subject):
for name, value in subject.__members__.items():
if name not in members:
members[name] = ClassAttribute(subject, name, value)
superclass = subject.__mro__[1]
for name in obj_dict:
if name not in superclass.__dict__:
value = safe_getattr(subject, name)
members[name] = ClassAttribute(subject, name, value)
# members in __slots__
try:
__slots__ = getslots(subject)
if __slots__:
from sphinx.ext.autodoc import SLOTSATTR
for name, docstring in __slots__.items():
members[name] = ClassAttribute(subject, name, SLOTSATTR, docstring)
except (AttributeError, TypeError, ValueError):
pass
# other members
for name in dir(subject):
try:
value = attrgetter(subject, name)
unmangled = unmangle(subject, name)
if unmangled and unmangled not in members:
if name in obj_dict:
members[unmangled] = ClassAttribute(subject, unmangled, value)
else:
members[unmangled] = ClassAttribute(None, unmangled, value)
except AttributeError:
continue
try:
for cls in getmro(subject):
# annotation only member (ex. attr: int)
try:
for name in getannotations(cls):
name = unmangle(cls, name)
if name and name not in members:
members[name] = ClassAttribute(cls, name, INSTANCEATTR)
except AttributeError:
pass
# append instance attributes (cf. self.attr1) if analyzer knows
try:
modname = safe_getattr(cls, '__module__')
qualname = safe_getattr(cls, '__qualname__')
analyzer = ModuleAnalyzer.for_module(modname)
analyzer.analyze()
for (ns, name), docstring in analyzer.attr_docs.items():
if ns == qualname and name not in members:
members[name] = ClassAttribute(cls, name, INSTANCEATTR,
'\n'.join(docstring))
except (AttributeError, PycodeError):
pass
except AttributeError:
pass
return members

View File

@@ -174,6 +174,8 @@ class GoogleDocstring:
'notes': self._parse_notes_section,
'other parameters': self._parse_other_parameters_section,
'parameters': self._parse_parameters_section,
'receive': self._parse_receives_section,
'receives': self._parse_receives_section,
'return': self._parse_returns_section,
'returns': self._parse_returns_section,
'raise': self._parse_raises_section,
@@ -709,6 +711,15 @@ class GoogleDocstring:
lines.append('')
return lines
def _parse_receives_section(self, section: str) -> List[str]:
if self._config.napoleon_use_param:
# Allow to declare multiple parameters at once (ex: x, y: int)
fields = self._consume_fields(multiple=True)
return self._format_docutils_params(fields)
else:
fields = self._consume_fields()
return self._format_fields(_('Receives'), fields)
def _parse_references_section(self, section: str) -> List[str]:
use_admonition = self._config.napoleon_use_admonition_for_references
return self._parse_generic_section(_('References'), use_admonition)

View File

@@ -18,7 +18,7 @@
{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and
(sidebars != []) %}
{%- set url_root = pathto('', 1) %}
{# XXX necessary? #}
{# URL root should never be #, then all links are fragments #}
{%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
{%- if not embedded and docstitle %}
{%- set titlesuffix = " — "|safe + docstitle|e %}
@@ -88,7 +88,7 @@
{%- endmacro %}
{%- macro script() %}
<script id="documentation_options" data-url_root="{{ pathto('', 1) }}" src="{{ pathto('_static/documentation_options.js', 1) }}"></script>
<script id="documentation_options" data-url_root="{{ url_root }}" src="{{ pathto('_static/documentation_options.js', 1) }}"></script>
{%- for js in script_files %}
{{ js_tag(js) }}
{%- endfor %}

View File

@@ -20,7 +20,7 @@ from sphinx.locale import __
from sphinx.transforms import SphinxTransform
from sphinx.util import epoch_to_rfc1123, logging, requests, rfc1123_to_epoch, sha1
from sphinx.util.images import get_image_extension, guess_mimetype, parse_data_uri
from sphinx.util.osutil import ensuredir, movefile
from sphinx.util.osutil import ensuredir
logger = logging.getLogger(__name__)
@@ -99,7 +99,7 @@ class ImageDownloader(BaseImageConverter):
# append a suffix if URI does not contain suffix
ext = get_image_extension(mimetype)
newpath = os.path.join(self.imagedir, dirname, basename + ext)
movefile(path, newpath)
os.replace(path, newpath)
self.app.env.original_image_uri.pop(path)
self.app.env.original_image_uri[newpath] = node['uri']
path = newpath

View File

@@ -271,6 +271,7 @@ class DocFieldTransformer:
self.directive.domain,
target,
contnode=content[0],
env=self.directive.state.document.settings.env
)
if _is_single_paragraph(field_body):
paragraph = cast(nodes.paragraph, field_body[0])

View File

@@ -20,7 +20,7 @@ import warnings
from functools import partial, partialmethod
from inspect import Parameter, isclass, ismethod, ismethoddescriptor, ismodule # NOQA
from io import StringIO
from typing import Any, Callable, Dict, Mapping, Optional, Sequence, cast
from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple, cast
from sphinx.deprecation import RemovedInSphinx50Warning
from sphinx.pycode.ast import ast # for py36-37
@@ -36,6 +36,10 @@ else:
MethodDescriptorType = type(str.join)
WrapperDescriptorType = type(dict.__dict__['fromkeys'])
if False:
# For type annotation
from typing import Type # NOQA
logger = logging.getLogger(__name__)
memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE)
@@ -166,6 +170,18 @@ def getannotations(obj: Any) -> Mapping[str, Any]:
return {}
def getmro(obj: Any) -> Tuple["Type", ...]:
"""Get __mro__ from given *obj* safely.
Raises AttributeError if given *obj* raises an error on accessing __mro__.
"""
__mro__ = safe_getattr(obj, '__mro__', None)
if isinstance(__mro__, tuple):
return __mro__
else:
return tuple()
def getslots(obj: Any) -> Optional[Dict]:
"""Get __slots__ attribute of the class as dict.

View File

@@ -18,6 +18,8 @@ from io import StringIO
from os import path
from typing import Any, Generator, Iterator, List, Optional, Type
from sphinx.deprecation import RemovedInSphinx50Warning
try:
# for ALT Linux (#6712)
from sphinx.testing.path import path as Path
@@ -83,6 +85,9 @@ def mtimes_of_files(dirnames: List[str], suffix: str) -> Iterator[float]:
def movefile(source: str, dest: str) -> None:
"""Move a file, removing the destination if it exists."""
warnings.warn('sphinx.util.osutil.movefile() is deprecated for removal. '
'Please use os.replace() instead.',
RemovedInSphinx50Warning, stacklevel=2)
if os.path.exists(dest):
try:
os.unlink(dest)

View File

@@ -7,6 +7,9 @@ myint = int
#: docstring
variable: myint
#: docstring
variable2 = None # type: myint
def sum(x: myint, y: myint) -> myint:
"""docstring"""
@@ -32,4 +35,7 @@ class Foo:
"""docstring"""
#: docstring
attr: myint
attr1: myint
def __init__(self):
self.attr2: myint = None #: docstring

View File

@@ -0,0 +1,10 @@
class Foo:
def __init__(self):
self.attr1 = None #: docstring foo
self.attr2 = None #: docstring foo
class Bar(Foo):
def __init__(self):
self.attr2 = None #: docstring bar
self.attr3 = None #: docstring bar

View File

@@ -1,8 +1,12 @@
class Foo:
"""docstring"""
__slots__ = ['attr']
class Bar:
"""docstring"""
__slots__ = {'attr1': 'docstring of attr1',
'attr2': 'docstring of attr2',
'attr3': None}
@@ -12,4 +16,6 @@ class Bar:
class Baz:
"""docstring"""
__slots__ = 'attr'

View File

@@ -29,3 +29,6 @@ class Class:
class Derived(Class):
attr7: int
Alias = Derived

View File

@@ -36,7 +36,11 @@ def nonascii_srcdir(request, rootdir, sphinx_test_tempdir):
if not srcdir.exists():
(rootdir / 'test-root').copytree(srcdir)
except UnicodeEncodeError:
# Now Python 3.7+ follows PEP-540 and uses utf-8 encoding for filesystem by default.
# So this error handling will be no longer used (after dropping python 3.6 support).
srcdir = basedir / 'all'
if not srcdir.exists():
(rootdir / 'test-root').copytree(srcdir)
else:
# add a doc with a non-ASCII file name to the source dir
(srcdir / (test_name + '.txt')).write_text(dedent("""

View File

@@ -791,6 +791,53 @@ def test_canonical(app):
assert domain.objects['_io.StringIO'] == ('index', 'io.StringIO', 'class', True)
def test_info_field_list(app):
text = (".. py:module:: example\n"
".. py:class:: Class\n"
"\n"
" :param str name: blah blah\n"
" :param age: blah blah\n"
" :type age: int\n")
doctree = restructuredtext.parse(app, text)
print(doctree)
assert_node(doctree, (nodes.target,
addnodes.index,
addnodes.index,
[desc, ([desc_signature, ([desc_annotation, "class "],
[desc_addname, "example."],
[desc_name, "Class"])],
[desc_content, nodes.field_list, nodes.field])]))
assert_node(doctree[3][1][0][0],
([nodes.field_name, "Parameters"],
[nodes.field_body, nodes.bullet_list, ([nodes.list_item, nodes.paragraph],
[nodes.list_item, nodes.paragraph])]))
# :param str name:
assert_node(doctree[3][1][0][0][1][0][0][0],
([addnodes.literal_strong, "name"],
" (",
[pending_xref, addnodes.literal_emphasis, "str"],
")",
" -- ",
"blah blah"))
assert_node(doctree[3][1][0][0][1][0][0][0][2], pending_xref,
refdomain="py", reftype="class", reftarget="str",
**{"py:module": "example", "py:class": "Class"})
# :param age: + :type age:
assert_node(doctree[3][1][0][0][1][0][1][0],
([addnodes.literal_strong, "age"],
" (",
[pending_xref, addnodes.literal_emphasis, "int"],
")",
" -- ",
"blah blah"))
assert_node(doctree[3][1][0][0][1][0][1][0][2], pending_xref,
refdomain="py", reftype="class", reftarget="int",
**{"py:module": "example", "py:class": "Class"})
@pytest.mark.sphinx(freshenv=True)
def test_module_index(app):
text = (".. py:module:: docutils\n"

View File

@@ -1171,6 +1171,8 @@ def test_slots(app):
'.. py:class:: Bar()',
' :module: target.slots',
'',
' docstring',
'',
'',
' .. py:attribute:: Bar.attr1',
' :module: target.slots',
@@ -1191,6 +1193,8 @@ def test_slots(app):
'.. py:class:: Baz()',
' :module: target.slots',
'',
' docstring',
'',
'',
' .. py:attribute:: Baz.attr',
' :module: target.slots',
@@ -1199,6 +1203,8 @@ def test_slots(app):
'.. py:class:: Foo()',
' :module: target.slots',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr',
' :module: target.slots',
@@ -1559,6 +1565,11 @@ def test_autodoc_typed_instance_variables(app):
'.. py:module:: target.typed_vars',
'',
'',
'.. py:attribute:: Alias',
' :module: target.typed_vars',
'',
' alias of :class:`target.typed_vars.Derived`',
'',
'.. py:class:: Class()',
' :module: target.typed_vars',
'',
@@ -1667,9 +1678,31 @@ def test_autodoc_typed_inherited_instance_variables(app):
'',
' .. py:attribute:: Derived.attr3',
' :module: target.typed_vars',
' :type: int',
' :value: 0',
'',
'',
' .. py:attribute:: Derived.attr4',
' :module: target.typed_vars',
' :type: int',
'',
' attr4',
'',
'',
' .. py:attribute:: Derived.attr5',
' :module: target.typed_vars',
' :type: int',
'',
' attr5',
'',
'',
' .. py:attribute:: Derived.attr6',
' :module: target.typed_vars',
' :type: int',
'',
' attr6',
'',
'',
' .. py:attribute:: Derived.attr7',
' :module: target.typed_vars',
' :type: int',

View File

@@ -57,6 +57,19 @@ def test_autoattribute_typed_variable(app):
]
@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_typed_variable_in_alias(app):
actual = do_autodoc(app, 'attribute', 'target.typed_vars.Alias.attr2')
assert list(actual) == [
'',
'.. py:attribute:: Alias.attr2',
' :module: target.typed_vars',
' :type: int',
'',
]
@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_instance_variable(app):
@@ -72,6 +85,21 @@ def test_autoattribute_instance_variable(app):
]
@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_instance_variable_in_alias(app):
actual = do_autodoc(app, 'attribute', 'target.typed_vars.Alias.attr4')
assert list(actual) == [
'',
'.. py:attribute:: Alias.attr4',
' :module: target.typed_vars',
' :type: int',
'',
' attr4',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_slots_variable_list(app):
actual = do_autodoc(app, 'attribute', 'target.slots.Foo.attr')

View File

@@ -51,6 +51,61 @@ def test_classes(app):
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_instance_variable(app):
options = {'members': True}
actual = do_autodoc(app, 'class', 'target.instance_variable.Bar', options)
assert list(actual) == [
'',
'.. py:class:: Bar()',
' :module: target.instance_variable',
'',
'',
' .. py:attribute:: Bar.attr2',
' :module: target.instance_variable',
'',
' docstring bar',
'',
'',
' .. py:attribute:: Bar.attr3',
' :module: target.instance_variable',
'',
' docstring bar',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_inherited_instance_variable(app):
options = {'members': True,
'inherited-members': True}
actual = do_autodoc(app, 'class', 'target.instance_variable.Bar', options)
assert list(actual) == [
'',
'.. py:class:: Bar()',
' :module: target.instance_variable',
'',
'',
' .. py:attribute:: Bar.attr1',
' :module: target.instance_variable',
'',
' docstring foo',
'',
'',
' .. py:attribute:: Bar.attr2',
' :module: target.instance_variable',
'',
' docstring bar',
'',
'',
' .. py:attribute:: Bar.attr3',
' :module: target.instance_variable',
'',
' docstring bar',
'',
]
def test_decorators(app):
actual = do_autodoc(app, 'class', 'target.decorator.Baz')
assert list(actual) == [
@@ -77,6 +132,32 @@ def test_decorators(app):
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_slots_attribute(app):
options = {"members": None}
actual = do_autodoc(app, 'class', 'target.slots.Bar', options)
assert list(actual) == [
'',
'.. py:class:: Bar()',
' :module: target.slots',
'',
' docstring',
'',
'',
' .. py:attribute:: Bar.attr1',
' :module: target.slots',
'',
' docstring of attr1',
'',
'',
' .. py:attribute:: Bar.attr2',
' :module: target.slots',
'',
' docstring of instance attr2',
'',
]
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_show_inheritance_for_subclass_of_generic_type(app):

View File

@@ -707,7 +707,14 @@ def test_autodoc_type_aliases(app):
' docstring',
'',
'',
' .. py:attribute:: Foo.attr',
' .. py:attribute:: Foo.attr1',
' :module: target.annotations',
' :type: int',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr2',
' :module: target.annotations',
' :type: int',
'',
@@ -733,6 +740,14 @@ def test_autodoc_type_aliases(app):
'',
' docstring',
'',
'',
'.. py:data:: variable2',
' :module: target.annotations',
' :type: int',
' :value: None',
'',
' docstring',
'',
]
# define aliases
@@ -749,7 +764,14 @@ def test_autodoc_type_aliases(app):
' docstring',
'',
'',
' .. py:attribute:: Foo.attr',
' .. py:attribute:: Foo.attr1',
' :module: target.annotations',
' :type: myint',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr2',
' :module: target.annotations',
' :type: myint',
'',
@@ -775,6 +797,14 @@ def test_autodoc_type_aliases(app):
'',
' docstring',
'',
'',
'.. py:data:: variable2',
' :module: target.annotations',
' :type: myint',
' :value: None',
'',
' docstring',
'',
]

View File

@@ -303,6 +303,34 @@ class GoogleDocstringTest(BaseDocstringTest):
"""
Single line summary
Receive:
arg1 (list(int)): Description
arg2 (list[int]): Description
""",
"""
Single line summary
:Receives: * **arg1** (*list(int)*) -- Description
* **arg2** (*list[int]*) -- Description
"""
), (
"""
Single line summary
Receives:
arg1 (list(int)): Description
arg2 (list[int]): Description
""",
"""
Single line summary
:Receives: * **arg1** (*list(int)*) -- Description
* **arg2** (*list[int]*) -- Description
"""
), (
"""
Single line summary
Yield:
str:Extended
description of yielded value
@@ -1263,6 +1291,48 @@ class NumpyDocstringTest(BaseDocstringTest):
"""
Single line summary
Receive
-------
arg1:str
Extended
description of arg1
arg2 : int
Extended
description of arg2
""",
"""
Single line summary
:Receives: * **arg1** (:class:`str`) -- Extended
description of arg1
* **arg2** (:class:`int`) -- Extended
description of arg2
"""
), (
"""
Single line summary
Receives
--------
arg1:str
Extended
description of arg1
arg2 : int
Extended
description of arg2
""",
"""
Single line summary
:Receives: * **arg1** (:class:`str`) -- Extended
description of arg1
* **arg2** (:class:`int`) -- Extended
description of arg2
"""
), (
"""
Single line summary
Yield
-----
str