diff --git a/.circleci/config.yml b/.circleci/config.yml index 04c319340..7f4de4ae0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,4 +8,7 @@ jobs: - checkout - run: /python3.6/bin/pip install -U pip setuptools - run: /python3.6/bin/pip install -U .[test] - - run: make test PYTHON=/python3.6/bin/python + - run: mkdir -p test-reports/pytest + - run: make test PYTHON=/python3.6/bin/python TEST=--junitxml=test-reports/pytest/results.xml + - store_test_results: + path: test-reports diff --git a/CHANGES b/CHANGES index 82da14ec4..4c983abaf 100644 --- a/CHANGES +++ b/CHANGES @@ -47,6 +47,7 @@ Deprecated been changed to Sphinx object * ``sphinx.ext.autosummary.generate.AutosummaryRenderer`` takes an object type as an argument +* The ``ignore`` argument of ``sphinx.ext.autodoc.Documenter.get_doc()`` * The ``template_dir`` argument of ``sphinx.ext.autosummary.generate. AutosummaryRenderer`` * The ``module`` argument of ``sphinx.ext.autosummary.generate. @@ -55,6 +56,7 @@ Deprecated generate_autosummary_docs()`` * The ``template_dir`` argument of ``sphinx.ext.autosummary.generate. generate_autosummary_docs()`` +* The ``ignore`` argument of ``sphinx.util.docstring.prepare_docstring()`` * ``sphinx.ext.autosummary.generate.AutosummaryRenderer.exists()`` Features added @@ -80,6 +82,7 @@ Features added to generate stub files recursively * #4030: autosummary: Add :confval:`autosummary_context` to add template variables for custom templates +* #7530: html: Support nested elements * #7481: html theme: Add right margin to footnote/citation labels * #7482: html theme: CSS spacing for code blocks with captions and line numbers * #7443: html theme: Add new options :confval:`globaltoc_collapse` and @@ -98,6 +101,7 @@ Features added * C++, parse trailing return types. * #7143: py domain: Add ``:final:`` option to :rst:dir:`py:class:`, :rst:dir:`py:exception:` and :rst:dir:`py:method:` directives +* #7582: napoleon: a type for attribute are represented like type annotation Bugs fixed ---------- @@ -108,6 +112,9 @@ Bugs fixed * #7469: autodoc: The change of autodoc-process-docstring for variables is cached unexpectedly * #7559: autodoc: misdetects a sync function is async +* #6857: autodoc: failed to detect a classmethod on Enum class +* #7562: autodoc: a typehint contains spaces is wrongly rendered under + autodoc_typehints='description' mode * #7535: sphinx-autogen: crashes when custom template uses inheritance * #7536: sphinx-autogen: crashes when template uses i18n feature * #2785: html: Bad alignment of equation links diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 8a85e4b9a..2df82ed5a 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -69,6 +69,11 @@ The following is a list of deprecated interfaces. - 5.0 - N/A + * - The ``ignore`` argument of ``sphinx.ext.autodoc.Documenter.get_doc()`` + - 3.1 + - 5.0 + - N/A + * - The ``template_dir`` argument of ``sphinx.ext.autosummary.generate.AutosummaryRenderer`` - 3.1 @@ -98,6 +103,11 @@ The following is a list of deprecated interfaces. - 5.0 - N/A + * - The ``ignore`` argument of ``sphinx.util.docstring.prepare_docstring()`` + - 3.1 + - 5.0 + - N/A + * - ``desc_signature['first']`` - - 3.0 diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 9ac265e1e..2a2691851 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -1227,6 +1227,9 @@ def setup(app: Sphinx) -> Dict[str, Any]: # load default math renderer app.setup_extension('sphinx.ext.mathjax') + # load transforms for HTML builder + app.setup_extension('sphinx.builders.html.transforms') + return { 'version': 'builtin', 'parallel_read_safe': True, diff --git a/sphinx/builders/html/transforms.py b/sphinx/builders/html/transforms.py new file mode 100644 index 000000000..c91da57e9 --- /dev/null +++ b/sphinx/builders/html/transforms.py @@ -0,0 +1,69 @@ +""" + sphinx.builders.html.transforms + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Transforms for HTML builder. + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import re +from typing import Any, Dict + +from docutils import nodes + +from sphinx.application import Sphinx +from sphinx.transforms.post_transforms import SphinxPostTransform +from sphinx.util.nodes import NodeMatcher + + +class KeyboardTransform(SphinxPostTransform): + """Transform :kbd: role to more detailed form. + + Before:: + + + Control-x + + After:: + + + + Control + - + + x + """ + default_priority = 400 + builders = ('html',) + pattern = re.compile(r'(-|\+|\^|\s+)') + + def run(self, **kwargs: Any) -> None: + matcher = NodeMatcher(nodes.literal, classes=["kbd"]) + for node in self.document.traverse(matcher): # type: nodes.literal + parts = self.pattern.split(node[-1].astext()) + if len(parts) == 1: + continue + + node.pop() + while parts: + key = parts.pop(0) + node += nodes.literal('', key, classes=["kbd"]) + + try: + # key separator (ex. -, +, ^) + sep = parts.pop(0) + node += nodes.Text(sep) + except IndexError: + pass + + +def setup(app: Sphinx) -> Dict[str, Any]: + app.add_post_transform(KeyboardTransform) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 712b2c83e..2cebccaab 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -775,10 +775,11 @@ class PyDecoratorMixin: if cls.__name__ != 'DirectiveAdapter': warnings.warn('PyDecoratorMixin is deprecated. ' 'Please check the implementation of %s' % cls, - RemovedInSphinx50Warning) + RemovedInSphinx50Warning, stacklevel=2) break else: - warnings.warn('PyDecoratorMixin is deprecated', RemovedInSphinx50Warning) + warnings.warn('PyDecoratorMixin is deprecated', + RemovedInSphinx50Warning, stacklevel=2) ret = super().handle_signature(sig, signode) # type: ignore signode.insert(0, addnodes.desc_addname('@', '@')) diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index cd86f5bc1..430af3043 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -645,7 +645,7 @@ class StandardDomain(Domain): def add_object(self, objtype: str, name: str, docname: str, labelid: str) -> None: warnings.warn('StandardDomain.add_object() is deprecated.', - RemovedInSphinx50Warning) + RemovedInSphinx50Warning, stacklevel=2) self.objects[objtype, name] = (docname, labelid) @property diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index e2c47dc88..e18dc47b9 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -395,9 +395,9 @@ class Documenter: except TypeError: # retry without arguments for old documenters args = self.format_args() - except Exception as err: - logger.warning(__('error while formatting arguments for %s: %s') % - (self.fullname, err), type='autodoc') + except Exception: + logger.warning(__('error while formatting arguments for %s:') % + self.fullname, type='autodoc', exc_info=True) args = None retann = self.retann @@ -428,8 +428,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 = 1) -> List[List[str]]: + def get_doc(self, ignore: int = None) -> List[List[str]]: """Decode and return lines of the docstring(s) for the object.""" + if ignore is not None: + warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated." + % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) docstring = getdoc(self.object, self.get_attr, self.env.config.autodoc_inherit_docstrings, self.parent, self.object_name) @@ -741,8 +745,8 @@ class Documenter: # parse right now, to get PycodeErrors on parsing (results will # be cached anyway) self.analyzer.find_attr_docs() - except PycodeError as err: - logger.debug('[autodoc] module analyzer failed: %s', err) + except PycodeError: + logger.debug('[autodoc] module analyzer failed:', exc_info=True) # no source file -- e.g. for builtin and C modules self.analyzer = None # at least add the module.__file__ as a dependency @@ -844,7 +848,7 @@ class ModuleDocumenter(Documenter): if self.options.deprecated: self.add_line(' :deprecated:', sourcename) - def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, object]]]: + def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, Any]]]: if want_all: if (self.options.ignore_module_all or not hasattr(self.object, '__all__')): @@ -970,7 +974,7 @@ class DocstringSignatureMixin: break return result - def get_doc(self, ignore: int = 1) -> List[List[str]]: + def get_doc(self, ignore: int = None) -> List[List[str]]: lines = getattr(self, '_new_docstrings', None) if lines is not None: return lines @@ -1226,7 +1230,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename) - def get_doc(self, ignore: int = 1) -> List[List[str]]: + def get_doc(self, ignore: int = None) -> List[List[str]]: lines = getattr(self, '_new_docstrings', None) if lines is not None: return lines @@ -1719,8 +1723,12 @@ class SlotsAttributeDocumenter(AttributeDocumenter): self.env.note_reread() return False - def get_doc(self, ignore: int = 1) -> List[List[str]]: + def get_doc(self, ignore: int = None) -> List[List[str]]: """Decode and return lines of the docstring(s) for the object.""" + if ignore is not None: + warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated." + % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) name = self.objpath[-1] __slots__ = safe_getattr(self.parent, '__slots__', []) if isinstance(__slots__, dict) and isinstance(__slots__.get(name), str): @@ -1732,7 +1740,7 @@ class SlotsAttributeDocumenter(AttributeDocumenter): def get_documenters(app: Sphinx) -> Dict[str, Type[Documenter]]: """Returns registered Documenter classes""" - warnings.warn("get_documenters() is deprecated.", RemovedInSphinx50Warning) + warnings.warn("get_documenters() is deprecated.", RemovedInSphinx50Warning, stacklevel=2) return app.registry.documenters diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 67106b77a..cb06edbac 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -13,6 +13,7 @@ import traceback import warnings from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Tuple +from sphinx.pycode import ModuleAnalyzer from sphinx.util import logging from sphinx.util.inspect import isclass, isenumclass, safe_getattr @@ -127,7 +128,7 @@ class Attribute(NamedTuple): def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, - analyzer: Any = None) -> Dict[str, Attribute]: + analyzer: ModuleAnalyzer = None) -> Dict[str, Attribute]: """Get members and attributes of target object.""" from sphinx.ext.autodoc import INSTANCEATTR @@ -143,8 +144,9 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, members[name] = Attribute(name, True, value) superclass = subject.__mro__[1] - for name, value in obj_dict.items(): + for name in obj_dict: if name not in superclass.__dict__: + value = safe_getattr(subject, name) members[name] = Attribute(name, True, value) # members in __slots__ diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index dc00fe825..b2eb33db7 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -122,7 +122,7 @@ class AutosummaryRenderer: RemovedInSphinx50Warning, stacklevel=2) if template_dir: warnings.warn('template_dir argument for AutosummaryRenderer is deprecated.', - RemovedInSphinx50Warning) + RemovedInSphinx50Warning, stacklevel=2) system_templates_path = [os.path.join(package_dir, 'ext', 'autosummary', 'templates')] loader = SphinxTemplateLoader(app.srcdir, app.config.templates_path, @@ -274,11 +274,11 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, overwrite: bool = True) -> None: if builder: warnings.warn('builder argument for generate_autosummary_docs() is deprecated.', - RemovedInSphinx50Warning) + RemovedInSphinx50Warning, stacklevel=2) if template_dir: warnings.warn('template_dir argument for generate_autosummary_docs() is deprecated.', - RemovedInSphinx50Warning) + RemovedInSphinx50Warning, stacklevel=2) showed_sources = list(sorted(sources)) if len(showed_sources) > 20: @@ -371,7 +371,7 @@ def find_autosummary_in_docstring(name: str, module: str = None, filename: str = """ if module: warnings.warn('module argument for find_autosummary_in_docstring() is deprecated.', - RemovedInSphinx50Warning) + RemovedInSphinx50Warning, stacklevel=2) try: real_name, obj, parent, modname = import_by_name(name) diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py index 10b1ff3a3..9b41152fc 100644 --- a/sphinx/ext/napoleon/__init__.py +++ b/sphinx/ext/napoleon/__init__.py @@ -168,11 +168,10 @@ class Config: **If False**:: .. attribute:: attr1 + :type: int Description of `attr1` - :type: int - napoleon_use_param : :obj:`bool` (Defaults to True) True to use a ``:param:`` role for each function parameter. False to use a single ``:parameters:`` role for all the parameters. diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 196924bdd..acfc9977f 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -584,13 +584,12 @@ class GoogleDocstring: lines.append('.. attribute:: ' + _name) if self._opt and 'noindex' in self._opt: lines.append(' :noindex:') + if _type: + lines.extend(self._indent([':type: %s' % _type], 3)) lines.append('') fields = self._format_field('', '', _desc) lines.extend(self._indent(fields, 3)) - if _type: - lines.append('') - lines.extend(self._indent([':type: %s' % _type], 3)) lines.append('') if self._config.napoleon_use_ivar: lines.append('') diff --git a/sphinx/parsers.py b/sphinx/parsers.py index 635cd6d50..94c17dc0d 100644 --- a/sphinx/parsers.py +++ b/sphinx/parsers.py @@ -63,7 +63,7 @@ class Parser(docutils.parsers.Parser): @property def app(self) -> "Sphinx": - warnings.warn('parser.app is deprecated.', RemovedInSphinx50Warning) + warnings.warn('parser.app is deprecated.', RemovedInSphinx50Warning, stacklevel=2) return self._app diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py index 7b3f011d1..64fdbf1d7 100644 --- a/sphinx/util/docstrings.py +++ b/sphinx/util/docstrings.py @@ -10,10 +10,13 @@ import re import sys +import warnings from typing import Dict, List from docutils.parsers.rst.states import Body +from sphinx.deprecation import RemovedInSphinx50Warning + field_list_item_re = re.compile(Body.patterns['field_marker']) @@ -42,7 +45,7 @@ def extract_metadata(s: str) -> Dict[str, str]: return metadata -def prepare_docstring(s: str, ignore: int = 1, tabsize: int = 8) -> List[str]: +def prepare_docstring(s: str, ignore: int = None, tabsize: int = 8) -> List[str]: """Convert a docstring into lines of parseable reST. Remove common leading indentation, where the indentation of a given number of lines (usually just one) is ignored. @@ -51,6 +54,12 @@ def prepare_docstring(s: str, ignore: int = 1, tabsize: int = 8) -> List[str]: ViewList (used as argument of nested_parse().) An empty line is added to act as a separator between this docstring and following content. """ + if ignore is None: + ignore = 1 + else: + warnings.warn("The 'ignore' argument to parepare_docstring() is deprecated.", + RemovedInSphinx50Warning, stacklevel=2) + lines = s.expandtabs(tabsize).splitlines() # Find minimum indentation of any non-blank lines after ignored lines. margin = sys.maxsize diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 49d8636c1..5118ee98c 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -58,7 +58,7 @@ def getargspec(func: Callable) -> Any: """Like inspect.getfullargspec but supports bound methods, and wrapped methods.""" warnings.warn('sphinx.ext.inspect.getargspec() is deprecated', - RemovedInSphinx50Warning) + RemovedInSphinx50Warning, stacklevel=2) # On 3.5+, signature(int) or similar raises ValueError. On 3.4, it # succeeds with a bogus signature. We want a TypeError uniformly, to # match historical behavior. diff --git a/sphinx/util/logging.py b/sphinx/util/logging.py index 5526d7538..5206363a5 100644 --- a/sphinx/util/logging.py +++ b/sphinx/util/logging.py @@ -411,9 +411,13 @@ class WarningIsErrorFilter(logging.Filter): message = record.msg # use record.msg itself if location: - raise SphinxWarning(location + ":" + str(message)) + exc = SphinxWarning(location + ":" + str(message)) else: - raise SphinxWarning(message) + exc = SphinxWarning(message) + if record.exc_info is not None: + raise exc from record.exc_info[1] + else: + raise exc else: return True diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 1d92ce836..3980e9707 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -58,8 +58,8 @@ class NodeMatcher: # => [, , ...] """ - def __init__(self, *classes: "Type[Node]", **attrs: Any) -> None: - self.classes = classes + def __init__(self, *node_classes: "Type[Node]", **attrs: Any) -> None: + self.classes = node_classes self.attrs = attrs def match(self, node: Node) -> bool: diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 2dc1e2e8c..09b483913 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -93,7 +93,7 @@ class LaTeXWriter(writers.Writer): visitor = self.builder.create_translator(self.document, self.builder, self.theme) except TypeError: warnings.warn('LaTeXTranslator now takes 3rd argument; "theme".', - RemovedInSphinx50Warning) + RemovedInSphinx50Warning, stacklevel=2) visitor = self.builder.create_translator(self.document, self.builder) self.document.walkabout(visitor) @@ -289,7 +289,7 @@ class LaTeXTranslator(SphinxTranslator): if theme is None: warnings.warn('LaTeXTranslator now takes 3rd argument; "theme".', - RemovedInSphinx50Warning) + RemovedInSphinx50Warning, stacklevel=2) # flags self.in_title = 0 diff --git a/tests/roots/test-ext-autodoc/target/enum.py b/tests/roots/test-ext-autodoc/target/enum.py index d0a59c71c..c69455fb7 100644 --- a/tests/roots/test-ext-autodoc/target/enum.py +++ b/tests/roots/test-ext-autodoc/target/enum.py @@ -16,3 +16,8 @@ class EnumCls(enum.Enum): def say_hello(self): """a method says hello to you.""" pass + + @classmethod + def say_goodbye(cls): + """a classmethod says good-bye to you.""" + pass diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 6d57d795d..c2271b60a 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1112,8 +1112,7 @@ def test_slots(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_enum_class(app): - options = {"members": None, - "undoc-members": True} + options = {"members": None} actual = do_autodoc(app, 'class', 'target.enum.EnumCls', options) assert list(actual) == [ '', @@ -1123,6 +1122,13 @@ def test_enum_class(app): ' this is enum class', '', '', + ' .. py:method:: EnumCls.say_goodbye()', + ' :module: target.enum', + ' :classmethod:', + '', + ' a classmethod says good-bye to you.', + '', + '', ' .. py:method:: EnumCls.say_hello()', ' :module: target.enum', '', @@ -1149,11 +1155,6 @@ def test_enum_class(app): '', ' doc for val3', '', - '', - ' .. py:attribute:: EnumCls.val4', - ' :module: target.enum', - ' :value: 34', - '' ] # checks for an attribute of EnumClass diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index c4c5f1502..3027a4cb2 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -53,22 +53,19 @@ class NamedtupleSubclassTest(BaseDocstringTest): Sample namedtuple subclass .. attribute:: attr1 + :type: Arbitrary type Quick description of attr1 - :type: Arbitrary type - .. attribute:: attr2 + :type: Another arbitrary type Quick description of attr2 - :type: Another arbitrary type - .. attribute:: attr3 + :type: Type Adds a newline after the type - - :type: Type """ self.assertEqual(expected, actual) @@ -412,10 +409,9 @@ Attributes: actual = str(GoogleDocstring(docstring)) expected = """\ .. attribute:: in_attr + :type: :class:`numpy.ndarray` super-dooper attribute - - :type: :class:`numpy.ndarray` """ self.assertEqual(expected, actual) @@ -427,10 +423,9 @@ Attributes: actual = str(GoogleDocstring(docstring)) expected = """\ .. attribute:: in_attr + :type: numpy.ndarray super-dooper attribute - - :type: numpy.ndarray """ self.assertEqual(expected, actual) diff --git a/tests/test_markup.py b/tests/test_markup.py index b6d99db90..1d5c81bfa 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -16,6 +16,7 @@ from docutils.parsers.rst import Parser as RstParser from docutils.transforms.universal import SmartQuotes from sphinx import addnodes +from sphinx.builders.html.transforms import KeyboardTransform from sphinx.builders.latex import LaTeXBuilder from sphinx.roles import XRefRole from sphinx.testing.util import Struct, assert_node @@ -94,6 +95,7 @@ class ForgivingLaTeXTranslator(LaTeXTranslator, ForgivingTranslator): def verify_re_html(app, parse): def verify(rst, html_expected): document = parse(rst) + KeyboardTransform(document).apply() html_translator = ForgivingHTMLTranslator(document, app.builder) document.walkabout(html_translator) html_translated = ''.join(html_translator.fragment).strip() @@ -237,6 +239,32 @@ def get_verifier(verify, verify_re): '

space

', '\\sphinxkeyboard{\\sphinxupquote{space}}', ), + ( + # kbd role + 'verify', + ':kbd:`Control+X`', + ('

' + 'Control' + '+' + 'X' + '

'), + '\\sphinxkeyboard{\\sphinxupquote{Control+X}}', + ), + ( + # kbd role + 'verify', + ':kbd:`M-x M-s`', + ('

' + 'M' + '-' + 'x' + ' ' + 'M' + '-' + 's' + '

'), + '\\sphinxkeyboard{\\sphinxupquote{M\\sphinxhyphen{}x M\\sphinxhyphen{}s}}', + ), ( # non-interpolation of dashes in option role 'verify_re',