diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f90d4aeb7..89dc8fde9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,12 +42,10 @@ jobs: with: python-version: ${{ matrix.python }} - name: Set up Python ${{ matrix.python }} (deadsnakes) - uses: deadsnakes/action@v1.0.0 + uses: deadsnakes/action@v2.0.1 if: endsWith(matrix.python, '-dev') with: python-version: ${{ matrix.python }} - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: true - name: Check Python version run: python --version - name: Install graphviz diff --git a/CHANGES b/CHANGES index 59f2b0aff..5ad0d36a5 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,7 @@ Deprecated ---------- * The ``follow_wrapped`` argument of ``sphinx.util.inspect.signature()`` +* ``sphinx.ext.autodoc.DataDeclarationDocumenter`` * ``sphinx.util.requests.is_ssl_error()`` Features added @@ -42,6 +43,12 @@ Bugs fixed is decorated * #8434: autodoc: :confval:`autodoc_type_aliases` does not effect to variables and attributes +* #8443: autodoc: autodata directive can't create document for PEP-526 based + type annotated variables +* #8443: autodoc: autoattribute directive can't create document for PEP-526 + based uninitalized variables +* #8419: html search: Do not load ``language_data.js`` in non-search pages +* #8454: graphviz: The layout option for graph and digraph directives don't work * #8437: Makefile: ``make clean`` with empty BUILDDIR is dangerous Testing diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 392325246..31de41a10 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -31,6 +31,11 @@ The following is a list of deprecated interfaces. - 5.0 - N/A + * - ``sphinx.ext.autodoc.DataDeclarationDocumenter`` + - 3.4 + - 5.0 + - ``sphinx.ext.autodoc.DataDocumenter`` + * - ``sphinx.util.requests.is_ssl_error()`` - 3.4 - 5.0 diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index d7986e7dc..6d263a9ed 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -301,7 +301,6 @@ class StandaloneHTMLBuilder(Builder): self.add_js_file('jquery.js') self.add_js_file('underscore.js') self.add_js_file('doctools.js') - self.add_js_file('language_data.js') for filename, attrs in self.app.registry.js_files: self.add_js_file(filename, **attrs) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 1bd49fc38..243d13572 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -961,7 +961,7 @@ class ModuleDocumenter(Documenter): def __init__(self, *args: Any) -> None: super().__init__(*args) merge_members_option(self.options) - self.__all__ = None + self.__all__ = None # type: Optional[Sequence[str]] @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any @@ -985,26 +985,16 @@ class ModuleDocumenter(Documenter): return ret def import_object(self, raiseerror: bool = False) -> bool: - def is_valid_module_all(__all__: Any) -> bool: - """Check the given *__all__* is valid for a module.""" - if (isinstance(__all__, (list, tuple)) and - all(isinstance(e, str) for e in __all__)): - return True - else: - return False - ret = super().import_object(raiseerror) - if not self.options.ignore_module_all: - __all__ = getattr(self.object, '__all__', None) - if is_valid_module_all(__all__): - # valid __all__ found. copy it to self.__all__ - self.__all__ = __all__ - elif __all__: - # invalid __all__ found. - logger.warning(__('__all__ should be a list of strings, not %r ' - '(in module %s) -- ignoring __all__') % - (__all__, self.fullname), type='autodoc') + try: + if not self.options.ignore_module_all: + self.__all__ = inspect.getall(self.object) + except ValueError as exc: + # invalid __all__ found. + logger.warning(__('__all__ should be a list of strings, not %r ' + '(in module %s) -- ignoring __all__') % + (exc.args[0], self.fullname), type='autodoc') return ret @@ -1697,6 +1687,28 @@ class DataDocumenter(ModuleLevelDocumenter): ) -> bool: return isinstance(parent, ModuleDocumenter) and isattr + def import_object(self, raiseerror: bool = False) -> bool: + try: + return super().import_object(raiseerror=True) + 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 + except ImportError: + pass + + if raiseerror: + raise + else: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + return False + def add_directive_header(self, sig: str) -> None: super().add_directive_header(sig) sourcename = self.get_sourcename() @@ -1733,6 +1745,13 @@ class DataDocumenter(ModuleLevelDocumenter): return self.get_attr(self.parent or self.object, '__module__', None) \ or self.modname + def add_content(self, more_content: Any, no_docstring: bool = False) -> None: + if self.object is UNINITIALIZED_ATTR: + # suppress docstring of the value + super().add_content(more_content, no_docstring=True) + else: + super().add_content(more_content, no_docstring=no_docstring) + class DataDeclarationDocumenter(DataDocumenter): """ @@ -1746,30 +1765,10 @@ class DataDeclarationDocumenter(DataDocumenter): # must be higher than AttributeDocumenter priority = 11 - @classmethod - def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any - ) -> bool: - """This documents only INSTANCEATTR members.""" - return (isinstance(parent, ModuleDocumenter) and - isattr and - member is INSTANCEATTR) - - def import_object(self, raiseerror: bool = False) -> bool: - """Never import anything.""" - # disguise as a data - self.objtype = 'data' - self.object = UNINITIALIZED_ATTR - try: - # import module to obtain type annotation - self.parent = importlib.import_module(self.modname) - except ImportError: - pass - - return True - - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: - """Never try to get a docstring from the object.""" - super().add_content(more_content, no_docstring=True) + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn("%s is deprecated." % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + super().__init__(*args, **kwargs) class GenericAliasDocumenter(DataDocumenter): @@ -2036,6 +2035,22 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): def isinstanceattribute(self) -> bool: """Check the subject is an instance attribute.""" + # uninitialized instance variable (PEP-526) + with mock(self.config.autodoc_mock_imports): + try: + ret = import_object(self.modname, self.objpath[:-1], 'class', + attrgetter=self.get_attr, + warningiserror=self.config.autodoc_warningiserror) + self.parent = ret[3] + annotations = get_type_hints(self.parent, None, + self.config.autodoc_type_aliases) + if self.objpath[-1] in annotations: + self.object = UNINITIALIZED_ATTR + return True + except ImportError: + pass + + # An instance variable defined inside __init__(). try: analyzer = ModuleAnalyzer.for_module(self.modname) attr_docs = analyzer.find_attr_docs() @@ -2046,7 +2061,9 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): return False except PycodeError: - return False + pass + + return False def import_object(self, raiseerror: bool = False) -> bool: try: @@ -2282,7 +2299,6 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_autodocumenter(ClassDocumenter) app.add_autodocumenter(ExceptionDocumenter) app.add_autodocumenter(DataDocumenter) - app.add_autodocumenter(DataDeclarationDocumenter) app.add_autodocumenter(GenericAliasDocumenter) app.add_autodocumenter(TypeVarDocumenter) app.add_autodocumenter(FunctionDocumenter) diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 7e187e36a..f6a84b4e4 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -85,8 +85,7 @@ AutosummaryEntry = NamedTuple('AutosummaryEntry', [('name', str), def setup_documenters(app: Any) -> None: - from sphinx.ext.autodoc import (AttributeDocumenter, ClassDocumenter, - DataDeclarationDocumenter, DataDocumenter, + from sphinx.ext.autodoc import (AttributeDocumenter, ClassDocumenter, DataDocumenter, DecoratorDocumenter, ExceptionDocumenter, FunctionDocumenter, GenericAliasDocumenter, InstanceAttributeDocumenter, MethodDocumenter, @@ -96,8 +95,7 @@ def setup_documenters(app: Any) -> None: ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, FunctionDocumenter, MethodDocumenter, AttributeDocumenter, InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter, - SlotsAttributeDocumenter, DataDeclarationDocumenter, GenericAliasDocumenter, - SingledispatchFunctionDocumenter, + SlotsAttributeDocumenter, GenericAliasDocumenter, SingledispatchFunctionDocumenter, ] # type: List[Type[Documenter]] for documenter in documenters: app.registry.add_documenter(documenter.objtype, documenter) diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index 62375f95b..dc1cbb686 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -182,7 +182,8 @@ class GraphvizSimple(SphinxDirective): 'alt': directives.unchanged, 'align': align_spec, 'caption': directives.unchanged, - 'graphviz_dot': directives.unchanged, + 'layout': directives.unchanged, + 'graphviz_dot': directives.unchanged, # an old alias of `layout` option 'name': directives.unchanged, 'class': directives.class_option, } @@ -194,6 +195,8 @@ class GraphvizSimple(SphinxDirective): node['options'] = {'docname': self.env.docname} if 'graphviz_dot' in self.options: node['options']['graphviz_dot'] = self.options['graphviz_dot'] + if 'layout' in self.options: + node['options']['graphviz_dot'] = self.options['layout'] if 'alt' in self.options: node['alt'] = self.options['alt'] if 'align' in self.options: diff --git a/sphinx/themes/basic/search.html b/sphinx/themes/basic/search.html index 2673369f2..cf574f8d5 100644 --- a/sphinx/themes/basic/search.html +++ b/sphinx/themes/basic/search.html @@ -12,6 +12,7 @@ {%- block scripts %} {{ super() }} + {%- endblock %} {% block extrahead %} diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 4f4f12e1d..c7375aa57 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, List, Mapping, Optional, Tuple, cast +from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, cast from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.pycode.ast import ast # for py35-37 @@ -137,6 +137,22 @@ def unwrap_all(obj: Any, *, stop: Callable = None) -> Any: return obj +def getall(obj: Any) -> Optional[Sequence[str]]: + """Get __all__ attribute of the module as dict. + + Return None if given *obj* does not have __all__. + Raises ValueError if given *obj* have invalid __all__. + """ + __all__ = safe_getattr(obj, '__all__', None) + if __all__ is None: + return None + else: + if (isinstance(__all__, (list, tuple)) and all(isinstance(e, str) for e in __all__)): + return __all__ + else: + raise ValueError(__all__) + + def getslots(obj: Any) -> Optional[Dict]: """Get __slots__ attribute of the class as dict. diff --git a/tests/test_ext_autodoc_autoattribute.py b/tests/test_ext_autodoc_autoattribute.py index ca74f9aeb..31dccbd03 100644 --- a/tests/test_ext_autodoc_autoattribute.py +++ b/tests/test_ext_autodoc_autoattribute.py @@ -9,6 +9,8 @@ :license: BSD, see LICENSE for details. """ +import sys + import pytest from test_ext_autodoc import do_autodoc @@ -39,3 +41,31 @@ def test_autoattribute_novalue(app): ' should be documented -- süß', '', ] + + +@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(app): + actual = do_autodoc(app, 'attribute', 'target.typed_vars.Class.attr2') + assert list(actual) == [ + '', + '.. py:attribute:: Class.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) == [ + '', + '.. py:attribute:: Class.attr4', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr4', + '', + ] diff --git a/tests/test_ext_autodoc_autodata.py b/tests/test_ext_autodoc_autodata.py index 7e4471db9..72665cdba 100644 --- a/tests/test_ext_autodoc_autodata.py +++ b/tests/test_ext_autodoc_autodata.py @@ -9,6 +9,8 @@ :license: BSD, see LICENSE for details. """ +import sys + import pytest from test_ext_autodoc import do_autodoc @@ -39,3 +41,34 @@ def test_autodata_novalue(app): ' documentation for the integer', '', ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_typed_variable(app): + actual = do_autodoc(app, 'data', 'target.typed_vars.attr2') + assert list(actual) == [ + '', + '.. py:data:: attr2', + ' :module: target.typed_vars', + ' :type: str', + '', + ' attr2', + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_type_comment(app): + actual = do_autodoc(app, 'data', 'target.typed_vars.attr3') + assert list(actual) == [ + '', + '.. py:data:: attr3', + ' :module: target.typed_vars', + ' :type: str', + " :value: ''", + '', + ' attr3', + '', + ]