diff options
author | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2020-02-09 00:57:53 +0900 |
---|---|---|
committer | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2020-02-09 00:57:53 +0900 |
commit | 2e87ee85a22279ee4ecc658f830520a88c90236d (patch) | |
tree | 124b080e0203aead764362ba1e5bd11da62822e5 | |
parent | d4aeae475943aef9b935f7487baf90ccc1c58b42 (diff) | |
parent | 1e5342faa9147c7a3c60e41dc7671e88f6795855 (diff) | |
download | sphinx-git-2e87ee85a22279ee4ecc658f830520a88c90236d.tar.gz |
Merge branch '2.0'
-rw-r--r-- | CHANGES | 13 | ||||
-rw-r--r-- | doc/man/sphinx-apidoc.rst | 5 | ||||
-rw-r--r-- | doc/usage/restructuredtext/domains.rst | 24 | ||||
-rw-r--r-- | doc/usage/restructuredtext/field-lists.rst | 6 | ||||
-rw-r--r-- | doc/usage/theming.rst | 4 | ||||
-rw-r--r-- | sphinx/directives/other.py | 1 | ||||
-rw-r--r-- | sphinx/domains/python.py | 43 | ||||
-rw-r--r-- | sphinx/domains/std.py | 10 | ||||
-rw-r--r-- | sphinx/ext/apidoc.py | 15 | ||||
-rw-r--r-- | sphinx/ext/autodoc/__init__.py | 36 | ||||
-rw-r--r-- | sphinx/ext/autosummary/generate.py | 3 | ||||
-rw-r--r-- | sphinx/pycode/__init__.py | 8 | ||||
-rw-r--r-- | sphinx/pycode/ast.py | 2 | ||||
-rw-r--r-- | sphinx/pycode/parser.py | 29 | ||||
-rw-r--r-- | sphinx/themes/basic/static/doctools.js | 5 | ||||
-rw-r--r-- | sphinx/writers/latex.py | 2 | ||||
-rw-r--r-- | tests/roots/test-domain-py/module.rst | 6 | ||||
-rw-r--r-- | tests/roots/test-ext-autodoc/target/typed_vars.py | 9 | ||||
-rw-r--r-- | tests/test_autodoc.py | 78 | ||||
-rw-r--r-- | tests/test_directive_other.py | 17 | ||||
-rw-r--r-- | tests/test_domain_py.py | 62 | ||||
-rw-r--r-- | tests/test_environment_toctree.py | 6 | ||||
-rw-r--r-- | tests/test_pycode_parser.py | 11 |
23 files changed, 329 insertions, 66 deletions
@@ -110,6 +110,7 @@ Features added images (imagesize-1.2.0 or above is required) * #6994: imgconverter: Support illustrator file (.ai) to .png conversion * autodoc: Support Positional-Only Argument separator (PEP-570 compliant) +* autodoc: Support type annotations for variables * #2755: autodoc: Add new event: :event:`autodoc-before-process-signature` * #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``) annotation (python3.8+ or `typed_ast <https://github.com/python/typed_ast>`_ @@ -122,6 +123,10 @@ Features added * SphinxTranslator now calls visitor/departure method for super node class if visitor/departure method for original node class not found * #6418: Add new event: :event:`object-description-transform` +* py domain: :rst:dir:`py:data` and :rst:dir:`py:attribute` take new options + named ``:type:`` and ``:value:`` to describe its type and initial value +* #6785: py domain: ``:py:attr:`` is able to refer properties again +* #6772: apidoc: Add ``-q`` option for quiet mode Bugs fixed ---------- @@ -129,6 +134,8 @@ Bugs fixed * #6925: html: Remove redundant type="text/javascript" from <script> elements * #6906, #6907: autodoc: failed to read the source codes encoeded in cp1251 * #6961: latex: warning for babel shown twice +* #7059: latex: LaTeX compilation falls into infinite loop (wrapfig issue) +* #6581: latex: ``:reversed:`` option for toctree does not effect to LaTeX build * #6559: Wrong node-ids are generated in glossary directive * #6986: apidoc: misdetects module name for .so file inside module * #6899: apidoc: private members are not shown even if ``--private`` given @@ -138,7 +145,13 @@ Bugs fixed * #7023: autodoc: partial functions imported from other modules are listed as module members without :impoprted-members: option * #6889: autodoc: Trailing comma in ``:members::`` option causes cryptic warning +* #6568: autosummary: ``autosummary_imported_members`` is ignored on generating + a stub file for submodule * #7055: linkcheck: redirect is treated as an error +* #7088: HTML template: If ``navigation_with_keys`` option is activated, + modifier keys are ignored, which means the feature can interfere with browser + features +* #7090: std domain: Can't assign numfig-numbers for custom container nodes Testing -------- diff --git a/doc/man/sphinx-apidoc.rst b/doc/man/sphinx-apidoc.rst index 457e2e7f6..725d2f169 100644 --- a/doc/man/sphinx-apidoc.rst +++ b/doc/man/sphinx-apidoc.rst @@ -39,6 +39,11 @@ Options Directory to place the output files. If it does not exist, it is created. +.. option:: -q + + Do not output anything on standard output, only write warnings and errors to + standard error. + .. option:: -f, --force Force overwriting of any existing generated files. diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index e107acac1..9c69fe88f 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -195,6 +195,18 @@ The following directives are provided for module and class contents: as "defined constants." Class and object attributes are not documented using this environment. + .. rubric:: options + + .. rst:directive:option:: type: type of the variable + :type: text + + .. versionadded:: 2.4 + + .. rst:directive:option:: value: initial value of the variable + :type: text + + .. versionadded:: 2.4 + .. rst:directive:: .. py:exception:: name Describes an exception class. The signature can, but need not include @@ -229,6 +241,18 @@ The following directives are provided for module and class contents: information about the type of the data to be expected and whether it may be changed directly. + .. rubric:: options + + .. rst:directive:option:: type: type of the attribute + :type: text + + .. versionadded:: 2.4 + + .. rst:directive:option:: value: initial value of the attribute + :type: text + + .. versionadded:: 2.4 + .. rst:directive:: .. py:method:: name(parameters) Describes an object method. The parameters should not include the ``self`` diff --git a/doc/usage/restructuredtext/field-lists.rst b/doc/usage/restructuredtext/field-lists.rst index fcecfe708..b84d238ba 100644 --- a/doc/usage/restructuredtext/field-lists.rst +++ b/doc/usage/restructuredtext/field-lists.rst @@ -30,6 +30,12 @@ At the moment, these metadata fields are recognized: :tocdepth: 2 + .. note:: + + This metadata effects to the depth of local toctree. But it does not + effect to the depth of *global* toctree. So this would not be change + the sidebar of some themes which uses global one. + .. versionadded:: 0.4 ``nocomments`` diff --git a/doc/usage/theming.rst b/doc/usage/theming.rst index 3b42bc4cb..5ed9f1f1b 100644 --- a/doc/usage/theming.rst +++ b/doc/usage/theming.rst @@ -151,6 +151,10 @@ These themes are: dimension string such as '70em' or '50%'. Use 'none' if you don't want a width limit. Defaults may depend on the theme (often 800px). + - **navigation_with_keys** (true or false): Allow navigating to the + previous/next page using the keyboard's left and right arrows. Defaults to + ``False``. + **alabaster** `Alabaster theme`_ is a modified "Kr" Sphinx theme from @kennethreitz (especially as used in his Requests project), which was itself originally diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index bf7bd2784..87d769b41 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -145,6 +145,7 @@ class TocTree(SphinxDirective): # entries contains all entries (self references, external links etc.) if 'reversed' in self.options: toctree['entries'] = list(reversed(toctree['entries'])) + toctree['includefiles'] = list(reversed(toctree['includefiles'])) return ret diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 3c3d3d707..1b551c70b 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -437,6 +437,25 @@ class PyFunction(PyObject): class PyVariable(PyObject): """Description of a variable.""" + option_spec = PyObject.option_spec.copy() + option_spec.update({ + 'type': directives.unchanged, + 'value': directives.unchanged, + }) + + def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]: + fullname, prefix = super().handle_signature(sig, signode) + + typ = self.options.get('type') + if typ: + signode += addnodes.desc_annotation(typ, ': ' + typ) + + value = self.options.get('value') + if value: + signode += addnodes.desc_annotation(value, ' = ' + value) + + return fullname, prefix + def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: name, cls = name_cls if modname: @@ -629,6 +648,25 @@ class PyStaticMethod(PyMethod): class PyAttribute(PyObject): """Description of an attribute.""" + option_spec = PyObject.option_spec.copy() + option_spec.update({ + 'type': directives.unchanged, + 'value': directives.unchanged, + }) + + def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]: + fullname, prefix = super().handle_signature(sig, signode) + + typ = self.options.get('type') + if typ: + signode += addnodes.desc_annotation(typ, ': ' + typ) + + value = self.options.get('value') + if value: + signode += addnodes.desc_annotation(value, ' = ' + value) + + return fullname, prefix + def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: name, cls = name_cls try: @@ -1017,6 +1055,11 @@ class PythonDomain(Domain): searchmode = 1 if node.hasattr('refspecific') else 0 matches = self.find_obj(env, modname, clsname, target, type, searchmode) + + if not matches and type == 'attr': + # fallback to meth (for property) + matches = self.find_obj(env, modname, clsname, target, 'meth', searchmode) + if not matches: return None elif len(matches) > 1: diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index f545ec7d1..52633d194 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -946,11 +946,11 @@ class StandardDomain(Domain): if isinstance(node, nodes.section): return 'section' - elif isinstance(node, nodes.container): - if node.get('literal_block') and has_child(node, nodes.literal_block): - return 'code-block' - else: - return None + elif (isinstance(node, nodes.container) and + 'literal_block' in node and + has_child(node, nodes.literal_block)): + # given node is a code-block having caption + return 'code-block' else: figtype, _ = self.enumerable_nodes.get(node.__class__, (None, None)) return figtype diff --git a/sphinx/ext/apidoc.py b/sphinx/ext/apidoc.py index 0c70b4ec8..99cf67016 100644 --- a/sphinx/ext/apidoc.py +++ b/sphinx/ext/apidoc.py @@ -73,14 +73,19 @@ def module_join(*modnames: str) -> str: def write_file(name: str, text: str, opts: Any) -> None: """Write the output file for module/package <name>.""" + quiet = getattr(opts, 'quiet', None) + fname = path.join(opts.destdir, '%s.%s' % (name, opts.suffix)) if opts.dryrun: - print(__('Would create file %s.') % fname) + if not quiet: + print(__('Would create file %s.') % fname) return if not opts.force and path.isfile(fname): - print(__('File %s already exists, skipping.') % fname) + if not quiet: + print(__('File %s already exists, skipping.') % fname) else: - print(__('Creating file %s.') % fname) + if not quiet: + print(__('Creating file %s.') % fname) with FileAvoidWrite(fname) as f: f.write(text) @@ -324,6 +329,8 @@ Note: By default this script will not overwrite already created files.""")) parser.add_argument('-o', '--output-dir', action='store', dest='destdir', required=True, help=__('directory to place all output')) + parser.add_argument('-q', action='store_true', dest='quiet', + help=__('no output on stdout, just warnings on stderr')) parser.add_argument('-d', '--maxdepth', action='store', dest='maxdepth', type=int, default=4, help=__('maximum depth of submodules to show in the TOC ' @@ -451,6 +458,8 @@ def main(argv: List[str] = sys.argv[1:]) -> int: } if args.extensions: d['extensions'].extend(args.extensions) + if args.quiet: + d['quiet'] = True for ext in d['extensions'][:]: if ',' in ext: diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index fd5d2bd05..b9404804b 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -10,6 +10,7 @@ :license: BSD, see LICENSE for details. """ +import importlib import re import warnings from types import ModuleType @@ -31,6 +32,7 @@ from sphinx.util import logging from sphinx.util import rpartition from sphinx.util.docstrings import extract_metadata, prepare_docstring from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature +from sphinx.util.typing import stringify as stringify_typehint if False: # For type annotation @@ -1262,12 +1264,22 @@ class DataDocumenter(ModuleLevelDocumenter): super().add_directive_header(sig) sourcename = self.get_sourcename() if not self.options.annotation: + # obtain annotation for this data + annotations = getattr(self.parent, '__annotations__', {}) + 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: objrepr = object_description(self.object) + self.add_line(' :value: ' + objrepr, sourcename) except ValueError: pass - else: - self.add_line(' :annotation: = ' + objrepr, sourcename) elif self.options.annotation is SUPPRESS: pass else: @@ -1306,6 +1318,12 @@ class DataDeclarationDocumenter(DataDocumenter): """Never import anything.""" # disguise as a data self.objtype = 'data' + 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: @@ -1434,12 +1452,22 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): sourcename = self.get_sourcename() if not self.options.annotation: if not self._datadescriptor: + # obtain annotation for this attribute + annotations = getattr(self.parent, '__annotations__', {}) + 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: objrepr = object_description(self.object) + self.add_line(' :value: ' + objrepr, sourcename) except ValueError: pass - else: - self.add_line(' :annotation: = ' + objrepr, sourcename) elif self.options.annotation is SUPPRESS: pass else: diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 7c29d41e8..006dab815 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -299,7 +299,8 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, generate_autosummary_docs(new_files, output_dir=output_dir, suffix=suffix, warn=warn, info=info, base_path=base_path, builder=builder, - template_dir=template_dir, app=app, + template_dir=template_dir, + imported_members=imported_members, app=app, overwrite=overwrite) diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index 12bd8d9ef..55d5d2c1d 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -142,9 +142,10 @@ class ModuleAnalyzer: self.code = source.read() # will be filled by parse() - self.attr_docs = None # type: Dict[Tuple[str, str], List[str]] - self.tagorder = None # type: Dict[str, int] - self.tags = None # type: Dict[str, Tuple[str, int, int]] + self.annotations = None # type: Dict[Tuple[str, str], str] + self.attr_docs = None # type: Dict[Tuple[str, str], List[str]] + self.tagorder = None # type: Dict[str, int] + self.tags = None # type: Dict[str, Tuple[str, int, int]] def parse(self) -> None: """Parse the source code.""" @@ -159,6 +160,7 @@ class ModuleAnalyzer: else: self.attr_docs[scope] = [''] + self.annotations = parser.annotations self.tags = parser.definitions self.tagorder = parser.deforders except Exception as exc: diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py index 155ae86d5..22207b715 100644 --- a/sphinx/pycode/ast.py +++ b/sphinx/pycode/ast.py @@ -38,6 +38,8 @@ def unparse(node: ast.AST) -> str: """Unparse an AST to string.""" if node is None: return None + elif isinstance(node, str): + return node elif isinstance(node, ast.Attribute): return "%s.%s" % (unparse(node.value), node.attr) elif isinstance(node, ast.Bytes): diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index c14d5773b..cb3cf0cc1 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -7,7 +7,6 @@ :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -import ast import inspect import itertools import re @@ -17,6 +16,9 @@ from token import NAME, NEWLINE, INDENT, DEDENT, NUMBER, OP, STRING from tokenize import COMMENT, NL from typing import Any, Dict, List, Tuple +from sphinx.pycode.ast import ast # for py37 or older +from sphinx.pycode.ast import parse, unparse + comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$') indent_re = re.compile('^\\s*$') @@ -226,6 +228,7 @@ class VariableCommentPicker(ast.NodeVisitor): self.current_classes = [] # type: List[str] self.current_function = None # type: ast.FunctionDef self.comments = {} # type: Dict[Tuple[str, str], str] + self.annotations = {} # type: Dict[Tuple[str, str], str] self.previous = None # type: ast.AST self.deforders = {} # type: Dict[str, int] super().__init__() @@ -254,6 +257,18 @@ class VariableCommentPicker(ast.NodeVisitor): self.comments[(context, name)] = comment + def add_variable_annotation(self, name: str, annotation: ast.AST) -> None: + if self.current_function: + if self.current_classes and self.context[-1] == "__init__": + # store variable comments inside __init__ method of classes + context = ".".join(self.context[:-1]) + else: + return + else: + context = ".".join(self.context) + + self.annotations[(context, name)] = unparse(annotation) + def get_self(self) -> ast.arg: """Returns the name of first argument if in function.""" if self.current_function and self.current_function.args.args: @@ -295,6 +310,14 @@ class VariableCommentPicker(ast.NodeVisitor): except TypeError: return # this assignment is not new definition! + # record annotation + if hasattr(node, 'annotation') and node.annotation: # type: ignore + for varname in varnames: + self.add_variable_annotation(varname, node.annotation) # type: ignore + elif hasattr(node, 'type_comment') and node.type_comment: + for varname in varnames: + self.add_variable_annotation(varname, node.type_comment) # type: ignore + # check comments after assignment parser = AfterCommentParser([current_line[node.col_offset:]] + self.buffers[node.lineno:]) @@ -468,6 +491,7 @@ class Parser: def __init__(self, code: str, encoding: str = 'utf-8') -> None: self.code = filter_whitespace(code) self.encoding = encoding + self.annotations = {} # type: Dict[Tuple[str, str], str] self.comments = {} # type: Dict[Tuple[str, str], str] self.deforders = {} # type: Dict[str, int] self.definitions = {} # type: Dict[str, Tuple[str, int, int]] @@ -479,9 +503,10 @@ class Parser: def parse_comments(self) -> None: """Parse the code and pick up comments.""" - tree = ast.parse(self.code) + tree = parse(self.code) picker = VariableCommentPicker(self.code.splitlines(True), self.encoding) picker.visit(tree) + self.annotations = picker.annotations self.comments = picker.comments self.deforders = picker.deforders diff --git a/sphinx/themes/basic/static/doctools.js b/sphinx/themes/basic/static/doctools.js index ddca10ca3..daccd209d 100644 --- a/sphinx/themes/basic/static/doctools.js +++ b/sphinx/themes/basic/static/doctools.js @@ -283,10 +283,11 @@ var Documentation = { }, initOnKeyListeners: function() { - $(document).keyup(function(event) { + $(document).keydown(function(event) { var activeElementType = document.activeElement.tagName; // don't navigate when in search box or textarea - if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') { + if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' + && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) { switch (event.keyCode) { case 37: // left var prevHref = $('link[rel="prev"]').prop('href'); diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 16501472f..4da3a0cd8 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -1329,6 +1329,8 @@ class LaTeXTranslator(SphinxTranslator): length = self.latex_image_length(node['width']) elif isinstance(node[0], nodes.image) and 'width' in node[0]: length = self.latex_image_length(node[0]['width']) + self.body.append('\n\n') # Insert a blank line to prevent infinite loop + # https://github.com/sphinx-doc/sphinx/issues/7059 self.body.append('\\begin{wrapfigure}{%s}{%s}\n\\centering' % ('r' if node['align'] == 'right' else 'l', length or '0pt')) self.context.append('\\end{wrapfigure}\n') diff --git a/tests/roots/test-domain-py/module.rst b/tests/roots/test-domain-py/module.rst index 64601bc95..c01032b26 100644 --- a/tests/roots/test-domain-py/module.rst +++ b/tests/roots/test-domain-py/module.rst @@ -18,6 +18,12 @@ module * Link to :py:meth:`module_a.submodule.ModTopLevel.mod_child_1` +.. py:method:: ModTopLevel.prop + :property: + + * Link to :py:attr:`prop attribute <.prop>` + * Link to :py:meth:`prop method <.prop>` + .. py:currentmodule:: None .. py:class:: ModNoModule diff --git a/tests/roots/test-ext-autodoc/target/typed_vars.py b/tests/roots/test-ext-autodoc/target/typed_vars.py index 4a9a6f7b5..b0782787e 100644 --- a/tests/roots/test-ext-autodoc/target/typed_vars.py +++ b/tests/roots/test-ext-autodoc/target/typed_vars.py @@ -2,12 +2,17 @@ attr1: str = '' #: attr2 attr2: str +#: attr3 +attr3 = '' # type: str class Class: attr1: int = 0 attr2: int + attr3 = 0 # type: int def __init__(self): - self.attr3: int = 0 #: attr3 - self.attr4: int #: attr4 + self.attr4: int = 0 #: attr4 + self.attr5: int #: attr5 + self.attr6 = 0 # type: int + """attr6""" diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 6fb4db6ad..52e9bf5c5 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -930,7 +930,7 @@ def test_autodoc_module_scope(app): '', '.. py:attribute:: Class.mdocattr', ' :module: target', - ' :annotation: = <_io.StringIO object>', + ' :value: <_io.StringIO object>', '', ' should be documented as well - süß', ' ' @@ -946,7 +946,7 @@ def test_autodoc_class_scope(app): '', '.. py:attribute:: Class.mdocattr', ' :module: target', - ' :annotation: = <_io.StringIO object>', + ' :value: <_io.StringIO object>', '', ' should be documented as well - süß', ' ' @@ -966,12 +966,12 @@ def test_class_attributes(app): ' ', ' .. py:attribute:: AttCls.a1', ' :module: target', - ' :annotation: = hello world', + ' :value: hello world', ' ', ' ', ' .. py:attribute:: AttCls.a2', ' :module: target', - ' :annotation: = None', + ' :value: None', ' ' ] @@ -990,7 +990,7 @@ def test_instance_attributes(app): ' ', ' .. py:attribute:: InstAttCls.ca1', ' :module: target', - " :annotation: = 'a'", + " :value: 'a'", ' ', ' Doc comment for class attribute InstAttCls.ca1.', ' It can have multiple lines.', @@ -998,28 +998,28 @@ def test_instance_attributes(app): ' ', ' .. py:attribute:: InstAttCls.ca2', ' :module: target', - " :annotation: = 'b'", + " :value: 'b'", ' ', ' Doc comment for InstAttCls.ca2. One line only.', ' ', ' ', ' .. py:attribute:: InstAttCls.ca3', ' :module: target', - " :annotation: = 'c'", + " :value: 'c'", ' ', ' Docstring for class attribute InstAttCls.ca3.', ' ', ' ', ' .. py:attribute:: InstAttCls.ia1', ' :module: target', - ' :annotation: = None', + ' :value: None', ' ', ' Doc comment for instance attribute InstAttCls.ia1', ' ', ' ', ' .. py:attribute:: InstAttCls.ia2', ' :module: target', - ' :annotation: = None', + ' :value: None', ' ', ' Docstring for instance attribute InstAttCls.ia2.', ' ' @@ -1038,7 +1038,7 @@ def test_instance_attributes(app): ' ', ' .. py:attribute:: InstAttCls.ca1', ' :module: target', - " :annotation: = 'a'", + " :value: 'a'", ' ', ' Doc comment for class attribute InstAttCls.ca1.', ' It can have multiple lines.', @@ -1046,7 +1046,7 @@ def test_instance_attributes(app): ' ', ' .. py:attribute:: InstAttCls.ia1', ' :module: target', - ' :annotation: = None', + ' :value: None', ' ', ' Doc comment for instance attribute InstAttCls.ia1', ' ' @@ -1114,28 +1114,28 @@ def test_enum_class(app): ' ', ' .. py:attribute:: EnumCls.val1', ' :module: target.enum', - ' :annotation: = 12', + ' :value: 12', ' ', ' doc for val1', ' ', ' ', ' .. py:attribute:: EnumCls.val2', ' :module: target.enum', - ' :annotation: = 23', + ' :value: 23', ' ', ' doc for val2', ' ', ' ', ' .. py:attribute:: EnumCls.val3', ' :module: target.enum', - ' :annotation: = 34', + ' :value: 34', ' ', ' doc for val3', ' ', ' ', ' .. py:attribute:: EnumCls.val4', ' :module: target.enum', - ' :annotation: = 34', + ' :value: 34', ' ' ] @@ -1145,7 +1145,7 @@ def test_enum_class(app): '', '.. py:attribute:: EnumCls.val1', ' :module: target.enum', - ' :annotation: = 12', + ' :value: 12', '', ' doc for val1', ' ' @@ -1429,40 +1429,68 @@ def test_autodoc_typed_instance_variables(app): ' ', ' .. py:attribute:: Class.attr1', ' :module: target.typed_vars', - ' :annotation: = 0', + ' :type: int', + ' :value: 0', ' ', ' ', ' .. py:attribute:: Class.attr2', ' :module: target.typed_vars', - ' :annotation: = None', + ' :type: int', + ' :value: None', ' ', ' ', ' .. py:attribute:: Class.attr3', ' :module: target.typed_vars', - ' :annotation: = None', + ' :type: int', + ' :value: 0', ' ', - ' attr3', - ' ', ' ', ' .. py:attribute:: Class.attr4', ' :module: target.typed_vars', - ' :annotation: = None', + ' :type: int', + ' :value: None', ' ', ' attr4', ' ', + ' ', + ' .. py:attribute:: Class.attr5', + ' :module: target.typed_vars', + ' :type: int', + ' :value: None', + ' ', + ' attr5', + ' ', + ' ', + ' .. py:attribute:: Class.attr6', + ' :module: target.typed_vars', + ' :type: int', + ' :value: None', + ' ', + ' attr6', + ' ', '', '.. py:data:: attr1', ' :module: target.typed_vars', - " :annotation: = ''", + ' :type: str', + " :value: ''", '', ' attr1', ' ', '', '.. py:data:: attr2', ' :module: target.typed_vars', - " :annotation: = None", + ' :type: str', + ' :value: None', '', ' attr2', + ' ', + '', + '.. py:data:: attr3', + ' :module: target.typed_vars', + ' :type: str', + " :value: ''", + '', + ' attr3', ' ' ] @@ -1479,7 +1507,7 @@ def test_autodoc_for_egged_code(app): '', '.. py:data:: CONSTANT', ' :module: sample', - ' :annotation: = 1', + ' :value: 1', '', ' constant on sample.py', ' ', diff --git a/tests/test_directive_other.py b/tests/test_directive_other.py index bfdc1aac8..52e4a937c 100644 --- a/tests/test_directive_other.py +++ b/tests/test_directive_other.py @@ -127,6 +127,23 @@ def test_toctree_glob_and_url(app): @pytest.mark.sphinx(testroot='toctree-glob') +def test_reversed_toctree(app): + text = (".. toctree::\n" + " :reversed:\n" + "\n" + " foo\n" + " bar/index\n" + " baz\n") + + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'index') + assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) + assert_node(doctree[0][0], + entries=[(None, 'baz'), (None, 'bar/index'), (None, 'foo')], + includefiles=['baz', 'bar/index', 'foo']) + + +@pytest.mark.sphinx(testroot='toctree-glob') def test_toctree_twice(app): text = (".. toctree::\n" "\n" diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 3ff29cbb7..f78c1e9d8 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -105,19 +105,22 @@ def test_domain_py_xrefs(app, status, warning): 'mod_child_2', 'meth') assert_refnode(refnodes[4], 'module_a.submodule', 'ModTopLevel', 'module_a.submodule.ModTopLevel.mod_child_1', 'meth') - assert_refnode(refnodes[5], 'module_b.submodule', None, + assert_refnode(refnodes[5], 'module_a.submodule', 'ModTopLevel', + 'prop', 'attr') + assert_refnode(refnodes[6], 'module_a.submodule', 'ModTopLevel', + 'prop', 'meth') + assert_refnode(refnodes[7], 'module_b.submodule', None, 'ModTopLevel', 'class') - assert_refnode(refnodes[6], 'module_b.submodule', 'ModTopLevel', + assert_refnode(refnodes[8], 'module_b.submodule', 'ModTopLevel', 'ModNoModule', 'class') - assert_refnode(refnodes[7], False, False, 'int', 'class') - assert_refnode(refnodes[8], False, False, 'tuple', 'class') - assert_refnode(refnodes[9], False, False, 'str', 'class') - assert_refnode(refnodes[10], False, False, 'float', 'class') - assert_refnode(refnodes[11], False, False, 'list', 'class') - assert_refnode(refnodes[11], False, False, 'list', 'class') - assert_refnode(refnodes[12], False, False, 'ModTopLevel', 'class') - assert_refnode(refnodes[13], False, False, 'index', 'doc', domain='std') - assert len(refnodes) == 14 + assert_refnode(refnodes[9], False, False, 'int', 'class') + assert_refnode(refnodes[10], False, False, 'tuple', 'class') + assert_refnode(refnodes[11], False, False, 'str', 'class') + assert_refnode(refnodes[12], False, False, 'float', 'class') + assert_refnode(refnodes[13], False, False, 'list', 'class') + assert_refnode(refnodes[14], False, False, 'ModTopLevel', 'class') + assert_refnode(refnodes[15], False, False, 'index', 'doc', domain='std') + assert len(refnodes) == 16 doctree = app.env.get_doctree('module_option') refnodes = list(doctree.traverse(addnodes.pending_xref)) @@ -161,6 +164,21 @@ def test_domain_py_objects(app, status, warning): assert objects['NestedParentB.child_1'] == ('roles', 'method') +@pytest.mark.sphinx('html', testroot='domain-py') +def test_resolve_xref_for_properties(app, status, warning): + app.builder.build_all() + + content = (app.outdir / 'module.html').text() + assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"' + ' title="module_a.submodule.ModTopLevel.prop">' + '<code class="xref py py-attr docutils literal notranslate"><span class="pre">' + 'prop</span> <span class="pre">attribute</span></code></a>' in content) + assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"' + ' title="module_a.submodule.ModTopLevel.prop">' + '<code class="xref py py-meth docutils literal notranslate"><span class="pre">' + 'prop</span> <span class="pre">method</span></code></a>' in content) + + @pytest.mark.sphinx('dummy', testroot='domain-py') def test_domain_py_find_obj(app, status, warning): @@ -268,6 +286,20 @@ def test_exceptions_module_is_ignored(app): def test_pydata_signature(app): text = (".. py:data:: version\n" + " :type: int\n" + " :value: 1\n") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "version"], + [desc_annotation, ": int"], + [desc_annotation, " = 1"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="data", + domain="py", objtype="data", noindex=False) + + +def test_pydata_signature_old(app): + text = (".. py:data:: version\n" " :annotation: = 1\n") doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, @@ -463,7 +495,9 @@ def test_pystaticmethod(app): def test_pyattribute(app): text = (".. py:class:: Class\n" "\n" - " .. py:attribute:: attr\n") + " .. py:attribute:: attr\n" + " :type: str\n" + " :value: ''\n") domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, @@ -473,7 +507,9 @@ def test_pyattribute(app): desc)])])) assert_node(doctree[1][1][0], addnodes.index, entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)]) - assert_node(doctree[1][1][1], ([desc_signature, desc_name, "attr"], + assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"], + [desc_annotation, ": str"], + [desc_annotation, " = ''"])], [desc_content, ()])) assert 'Class.attr' in domain.objects assert domain.objects['Class.attr'] == ('index', 'attribute') diff --git a/tests/test_environment_toctree.py b/tests/test_environment_toctree.py index 6a23b384a..a8c7da62e 100644 --- a/tests/test_environment_toctree.py +++ b/tests/test_environment_toctree.py @@ -119,16 +119,14 @@ def test_glob(app): [bullet_list, addnodes.toctree])]) # [0][1][1][1][0] assert_node(toctree[0][1][1][1][0], addnodes.toctree, caption=None, glob=True, hidden=False, titlesonly=False, - maxdepth=-1, numbered=0, includefiles=includefiles, + maxdepth=-1, numbered=0, includefiles=list(reversed(includefiles)), entries=[(None, 'qux/index'), (None, 'baz'), (None, 'bar/bar_3'), (None, 'bar/bar_2'), (None, 'bar/bar_1'), (None, 'bar/index'), (None, 'foo')]) - includefiles = ['foo', 'bar/index', 'bar/bar_1', 'bar/bar_2', - 'bar/bar_3', 'baz', 'qux/index'] # other collections assert app.env.toc_num_entries['index'] == 3 - assert app.env.toctree_includes['index'] == includefiles + includefiles + assert app.env.toctree_includes['index'] == includefiles + list(reversed(includefiles)) for file in includefiles: assert 'index' in app.env.files_to_rebuild[file] assert 'index' in app.env.glob_toctrees diff --git a/tests/test_pycode_parser.py b/tests/test_pycode_parser.py index b8bece84e..0bf505a33 100644 --- a/tests/test_pycode_parser.py +++ b/tests/test_pycode_parser.py @@ -99,12 +99,19 @@ def test_annotated_assignment_py36(): source = ('a: str = "Sphinx" #: comment\n' 'b: int = 1\n' '"""string on next line"""\n' - 'c: int #: comment') + 'c: int #: comment\n' + 'd = 1 # type: int\n' + '"""string on next line"""\n') parser = Parser(source) parser.parse() assert parser.comments == {('', 'a'): 'comment', ('', 'b'): 'string on next line', - ('', 'c'): 'comment'} + ('', 'c'): 'comment', + ('', 'd'): 'string on next line'} + assert parser.annotations == {('', 'a'): 'str', + ('', 'b'): 'int', + ('', 'c'): 'int', + ('', 'd'): 'int'} assert parser.definitions == {} |