diff options
26 files changed, 711 insertions, 94 deletions
@@ -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) ===================================== diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a20192679..bd164694d 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -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. diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 73429938f..363c936bf 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -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 diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py index 439c8512d..44009fb72 100644 --- a/sphinx/builders/gettext.py +++ b/sphinx/builders/gettext.py @@ -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') diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 01acc33ed..fd80c4970 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -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: diff --git a/sphinx/config.py b/sphinx/config.py index 005bc812f..02c70cea0 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -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 diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index d71a4891d..3bbf68005 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -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 == '.': diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 5f610a365..eec738a75 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -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__(). + return False + + def update_annotations(self, parent: Any) -> None: + """Update __annotations__ to support type_comment and so on.""" 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 + annotations = inspect.getannotations(parent) - return False - except PycodeError: - pass + for cls in inspect.getmro(parent): + try: + module = safe_getattr(cls, '__module__') + qualname = safe_getattr(cls, '__qualname__') - return False + 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: diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 8513255c1..b7f2d558b 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -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 diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 2cadcdcb3..5f799e9cf 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -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) diff --git a/sphinx/themes/basic/layout.html b/sphinx/themes/basic/layout.html index 131d2c533..a7604ba8d 100644 --- a/sphinx/themes/basic/layout.html +++ b/sphinx/themes/basic/layout.html @@ -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 %} diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py index 4865aa615..a70023436 100644 --- a/sphinx/transforms/post_transforms/images.py +++ b/sphinx/transforms/post_transforms/images.py @@ -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 diff --git a/sphinx/util/docfields.py b/sphinx/util/docfields.py index 3fa76271f..82463ad0e 100644 --- a/sphinx/util/docfields.py +++ b/sphinx/util/docfields.py @@ -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]) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index d390da9fc..aa0d4aca6 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -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. diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 153808186..4672d5f04 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -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) diff --git a/tests/roots/test-ext-autodoc/target/annotations.py b/tests/roots/test-ext-autodoc/target/annotations.py index e9ff2f604..ef600e2af 100644 --- a/tests/roots/test-ext-autodoc/target/annotations.py +++ b/tests/roots/test-ext-autodoc/target/annotations.py @@ -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 diff --git a/tests/roots/test-ext-autodoc/target/instance_variable.py b/tests/roots/test-ext-autodoc/target/instance_variable.py new file mode 100644 index 000000000..ae86d1edb --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/instance_variable.py @@ -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 diff --git a/tests/roots/test-ext-autodoc/target/slots.py b/tests/roots/test-ext-autodoc/target/slots.py index 144f97c95..32822fd38 100644 --- a/tests/roots/test-ext-autodoc/target/slots.py +++ b/tests/roots/test-ext-autodoc/target/slots.py @@ -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' diff --git a/tests/roots/test-ext-autodoc/target/typed_vars.py b/tests/roots/test-ext-autodoc/target/typed_vars.py index ba9657f18..f909b80f1 100644 --- a/tests/roots/test-ext-autodoc/target/typed_vars.py +++ b/tests/roots/test-ext-autodoc/target/typed_vars.py @@ -29,3 +29,6 @@ class Class: class Derived(Class): attr7: int + + +Alias = Derived diff --git a/tests/test_build.py b/tests/test_build.py index 9dcf78165..756a56e6d 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -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(""" diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index ce4b49bc5..56a986a91 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -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" diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index ae280a627..9e1d9961f 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -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', diff --git a/tests/test_ext_autodoc_autoattribute.py b/tests/test_ext_autodoc_autoattribute.py index e44395ee3..7f79ca332 100644 --- a/tests/test_ext_autodoc_autoattribute.py +++ b/tests/test_ext_autodoc_autoattribute.py @@ -59,6 +59,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): actual = do_autodoc(app, 'attribute', 'target.typed_vars.Class.attr4') assert list(actual) == [ @@ -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') diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py index 17c7f8944..6130f233a 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_ext_autodoc_autoclass.py @@ -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): diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index d76ed25e3..ac0a2c11c 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -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', + '', ] diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index 38ff6e79c..e6127fe55 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -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 |