diff --git a/CHANGES b/CHANGES index 1618cbeb4..a91860e08 100644 --- a/CHANGES +++ b/CHANGES @@ -29,8 +29,29 @@ Bugs fixed Testing -------- -Release 3.0.0 (in development) -============================== +Release 3.0.0 beta2 (in development) +==================================== + +Dependencies +------------ + +Incompatible changes +-------------------- + +Deprecated +---------- + +Features added +-------------- + +Bugs fixed +---------- + +Testing +-------- + +Release 3.0.0 beta1 (released Mar 23, 2020) +=========================================== Dependencies ------------ @@ -85,6 +106,7 @@ Deprecated * ``sphinx.domains.std.StandardDomain.add_object()`` * ``sphinx.domains.python.PyDecoratorMixin`` * ``sphinx.ext.autodoc.get_documenters()`` +* ``sphinx.ext.autosummary.process_autosummary_toc()`` * ``sphinx.parsers.Parser.app`` * ``sphinx.testing.path.Path.text()`` * ``sphinx.testing.path.Path.bytes()`` @@ -112,6 +134,9 @@ Features added * #6895: py domain: Do not emit nitpicky warnings for built-in types * py domain: Support lambda functions in function signature * #6417: py domain: Allow to make a style for arguments of functions and methods +* #7238, #7239: py domain: Emit a warning on describing a python object if the + entry is already added as the same name +* #7341: py domain: type annotations in singature are converted to cross refs * Support priority of event handlers. For more detail, see :py:meth:`.Sphinx.connect()` * #3077: Implement the scoping for :rst:dir:`productionlist` as indicated @@ -143,6 +168,8 @@ Features added * Added ``SphinxDirective.get_source_info()`` and ``SphinxRole.get_source_info()``. +* #7324: sphinx-build: Emit a warning if multiple files having different file + extensions for same document found Bugs fixed ---------- @@ -157,9 +184,11 @@ Bugs fixed * #7267: autodoc: error message for invalid directive options has wrong location * #7329: autodoc: info-field-list is wrongly generated from type hints into the class description even if ``autoclass_content='class'`` set +* #7331: autodoc: a cython-function is not recognized as a function * #5637: inheritance_diagram: Incorrect handling of nested class names * #7139: ``code-block:: guess`` does not work * #7325: html: source_suffix containing dot leads to wrong source link +* #7357: html: Resizing SVG image fails with ValueError * #7278: html search: Fix use of ``html_file_suffix`` instead of ``html_link_suffix`` in search results * #7297: html theme: ``bizstyle`` does not support ``sidebarwidth`` @@ -170,9 +199,7 @@ Bugs fixed * #2377: C, parse function pointers even in complex types. * #7345: sphinx-build: Sphinx crashes if output directory exists as a file * #7290: sphinx-build: Ignore bdb.BdbQuit when handling exceptions - -Testing --------- +* #6240: napoleon: Attributes and Methods sections ignore :noindex: option Release 2.4.5 (in development) ============================== @@ -192,6 +219,8 @@ Features added Bugs fixed ---------- +* #7343: Sphinx builds has been slower since 2.4.0 on debug mode + Testing -------- diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 7a90e24de..ba21649b3 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -81,6 +81,11 @@ The following is a list of deprecated interfaces. - 5.0 - ``sphinx.registry.documenters`` + * - ``sphinx.ext.autosummary.process_autosummary_toc()`` + - 3.0 + - 5.0 + - N/A + * - ``sphinx.parsers.Parser.app`` - 3.0 - 5.0 diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst index b5b0a2980..7b9bd2f80 100644 --- a/doc/usage/restructuredtext/directives.rst +++ b/doc/usage/restructuredtext/directives.rst @@ -48,6 +48,12 @@ tables of contents. The ``toctree`` directive is the central element. to the source directory. A numeric ``maxdepth`` option may be given to indicate the depth of the tree; by default, all levels are included. [#]_ + The representation of "TOC tree" is changed in each output format. The + builders that output multiple files (ex. HTML) treat it as a collection of + hyperlinks. On the other hand, the builders that output a single file (ex. + LaTeX, man page, etc.) replace it with the content of the documents on the + TOC tree. + Consider this example (taken from the Python docs' library reference index):: .. toctree:: diff --git a/setup.py b/setup.py index 82a602cdc..ba4484f22 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ extras_require = { 'pytest-cov', 'html5lib', 'typed_ast', # for py35-37 + 'cython', ], } diff --git a/sphinx/cmd/build.py b/sphinx/cmd/build.py index 263794a5a..cf50f1730 100644 --- a/sphinx/cmd/build.py +++ b/sphinx/cmd/build.py @@ -9,9 +9,11 @@ """ import argparse +import bdb import locale import multiprocessing import os +import pdb import sys import traceback from typing import Any, IO, List @@ -29,13 +31,10 @@ from sphinx.util.docutils import docutils_namespace, patch_docutils def handle_exception(app: Sphinx, args: Any, exception: BaseException, stderr: IO = sys.stderr) -> None: # NOQA - import bdb - if isinstance(exception, bdb.BdbQuit): return if args.pdb: - import pdb print(red(__('Exception occurred while building, starting debugger:')), file=stderr) traceback.print_exc() diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 1a2464211..93eae2f0b 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -30,6 +30,7 @@ from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType, Index, IndexEntry from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ +from sphinx.pycode.ast import ast, parse as ast_parse from sphinx.roles import XRefRole from sphinx.util import logging from sphinx.util.docfields import Field, GroupedField, TypedField @@ -63,6 +64,58 @@ pairindextypes = { } +def _parse_annotation(annotation: str) -> List[Node]: + """Parse type annotation.""" + def make_xref(text: str) -> addnodes.pending_xref: + return pending_xref('', nodes.Text(text), + refdomain='py', reftype='class', reftarget=text) + + def unparse(node: ast.AST) -> List[Node]: + if isinstance(node, ast.Attribute): + return [nodes.Text("%s.%s" % (unparse(node.value)[0], node.attr))] + elif isinstance(node, ast.Expr): + return unparse(node.value) + elif isinstance(node, ast.Index): + return unparse(node.value) + elif isinstance(node, ast.List): + result = [addnodes.desc_sig_punctuation('', '[')] # type: List[Node] + for elem in node.elts: + result.extend(unparse(elem)) + result.append(addnodes.desc_sig_punctuation('', ', ')) + result.pop() + result.append(addnodes.desc_sig_punctuation('', ']')) + return result + elif isinstance(node, ast.Module): + return sum((unparse(e) for e in node.body), []) + elif isinstance(node, ast.Name): + return [nodes.Text(node.id)] + elif isinstance(node, ast.Subscript): + result = unparse(node.value) + result.append(addnodes.desc_sig_punctuation('', '[')) + result.extend(unparse(node.slice)) + result.append(addnodes.desc_sig_punctuation('', ']')) + return result + elif isinstance(node, ast.Tuple): + result = [] + for elem in node.elts: + result.extend(unparse(elem)) + result.append(addnodes.desc_sig_punctuation('', ', ')) + result.pop() + return result + else: + raise SyntaxError # unsupported syntax + + try: + tree = ast_parse(annotation) + result = unparse(tree) + for i, node in enumerate(result): + if isinstance(node, nodes.Text): + result[i] = make_xref(str(node)) + return result + except SyntaxError: + return [make_xref(annotation)] + + def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist: """Parse a list of arguments using AST parser""" params = addnodes.desc_parameterlist(arglist) @@ -89,9 +142,10 @@ def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist: node += addnodes.desc_sig_name('', param.name) if param.annotation is not param.empty: + children = _parse_annotation(param.annotation) node += addnodes.desc_sig_punctuation('', ':') node += nodes.Text(' ') - node += addnodes.desc_sig_name('', param.annotation) + node += addnodes.desc_sig_name('', '', *children) # type: ignore if param.default is not param.empty: if param.annotation is not param.empty: node += nodes.Text(' ') @@ -350,7 +404,8 @@ class PyObject(ObjectDescription): signode += addnodes.desc_parameterlist() if retann: - signode += addnodes.desc_returns(retann, retann) + children = _parse_annotation(retann) + signode += addnodes.desc_returns(retann, '', *children) anno = self.options.get('annotation') if anno: @@ -366,7 +421,7 @@ class PyObject(ObjectDescription): signode: desc_signature) -> None: modname = self.options.get('module', self.env.ref_context.get('py:module')) fullname = (modname + '.' if modname else '') + name_cls[0] - node_id = make_id(self.env, self.state.document, modname or '', name_cls[0]) + node_id = make_id(self.env, self.state.document, '', fullname) signode['ids'].append(node_id) # Assign old styled node_id(fullname) not to break old hyperlinks (if possible) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 7c7979982..5031de82a 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -269,7 +269,10 @@ class Documenter: def add_line(self, line: str, source: str, *lineno: int) -> None: """Append one line of generated reST to the output.""" - self.directive.result.append(self.indent + line, source, *lineno) + if line.strip(): # not a blank line + self.directive.result.append(self.indent + line, source, *lineno) + else: + self.directive.result.append('', source, *lineno) def resolve_name(self, modname: str, parents: Any, path: str, base: Any ) -> Tuple[str, List[str]]: @@ -1007,7 +1010,8 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ if self.env.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) - if inspect.isbuiltin(self.object) or inspect.ismethoddescriptor(self.object): + if ((inspect.isbuiltin(self.object) or inspect.ismethoddescriptor(self.object)) and + not inspect.is_cython_function_or_method(self.object)): # cannot introspect arguments of a C function or method return None try: @@ -1426,7 +1430,8 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: if self.env.config.autodoc_typehints == 'none': kwargs.setdefault('show_annotation', False) - if inspect.isbuiltin(self.object) or inspect.ismethoddescriptor(self.object): + if ((inspect.isbuiltin(self.object) or inspect.ismethoddescriptor(self.object)) and + not inspect.is_cython_function_or_method(self.object)): # can never get arguments of a C function or method return None if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index c5de26198..52e1c47d2 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -72,7 +72,7 @@ from docutils.statemachine import StringList import sphinx from sphinx import addnodes from sphinx.application import Sphinx -from sphinx.deprecation import RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.environment import BuildEnvironment from sphinx.environment.adapters.toctree import TocTree from sphinx.ext.autodoc import Documenter @@ -106,6 +106,8 @@ def process_autosummary_toc(app: Sphinx, doctree: nodes.document) -> None: """Insert items described in autosummary:: to the TOC tree, but do not generate the toctree:: list. """ + warnings.warn('process_autosummary_toc() is deprecated', + RemovedInSphinx50Warning, stacklevel=2) env = app.builder.env crawled = {} @@ -762,7 +764,6 @@ def setup(app: Sphinx) -> Dict[str, Any]: texinfo=(autosummary_noop, autosummary_noop)) app.add_directive('autosummary', Autosummary) app.add_role('autolink', AutoLink()) - app.connect('doctree-read', process_autosummary_toc) app.connect('builder-inited', process_generate_options) app.add_config_value('autosummary_generate', [], True, [bool]) app.add_config_value('autosummary_generate_overwrite', True, False) diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index c490670be..f39a4a105 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -579,7 +579,11 @@ class GoogleDocstring: if _type: lines.append(':vartype %s: %s' % (_name, _type)) else: - lines.extend(['.. attribute:: ' + _name, '']) + lines.append('.. attribute:: ' + _name) + if self._opt and 'noindex' in self._opt: + lines.append(' :noindex:') + lines.append('') + fields = self._format_field('', '', _desc) lines.extend(self._indent(fields, 3)) if _type: @@ -637,6 +641,8 @@ class GoogleDocstring: lines = [] # type: List[str] for _name, _type, _desc in self._consume_fields(parse_type=False): lines.append('.. method:: %s' % _name) + if self._opt and 'noindex' in self._opt: + lines.append(' :noindex:') if _desc: lines.extend([''] + self._indent(_desc, 3)) lines.append('') diff --git a/sphinx/project.py b/sphinx/project.py index 409916608..1909659b1 100644 --- a/sphinx/project.py +++ b/sphinx/project.py @@ -9,6 +9,7 @@ """ import os +from glob import glob from typing import TYPE_CHECKING from sphinx.locale import __ @@ -55,7 +56,13 @@ class Project: for filename in get_matching_files(self.srcdir, excludes): # type: ignore docname = self.path2doc(filename) if docname: - if os.access(os.path.join(self.srcdir, filename), os.R_OK): + if docname in self.docnames: + pattern = os.path.join(self.srcdir, docname) + '.*' + files = [relpath(f, self.srcdir) for f in glob(pattern)] + logger.warning(__('multiple files found for the document "%s": %r\n' + 'Use %r for the build.'), + docname, files, self.doc2path(docname), once=True) + elif os.access(os.path.join(self.srcdir, filename), os.R_OK): self.docnames.add(docname) else: logger.warning(__("document not readable. Ignored."), location=docname) diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index 75cd0f411..450241f55 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -63,7 +63,7 @@ def assert_node(node: Node, cls: Any = None, xpath: str = "", **kwargs: Any) -> 'The node%s has %d child nodes, not one' % (xpath, len(node)) assert_node(node[0], cls[1:], xpath=xpath + "[0]", **kwargs) elif isinstance(cls, tuple): - assert isinstance(node, nodes.Element), \ + assert isinstance(node, (list, nodes.Element)), \ 'The node%s does not have any items' % xpath assert len(node) == len(cls), \ 'The node%s has %d child nodes, not %r' % (xpath, len(node), len(cls)) diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index c9252c8b5..a7e0b27d9 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -459,8 +459,6 @@ class SphinxTranslator(nodes.NodeVisitor): for node_class in node.__class__.__mro__: method = getattr(self, 'visit_%s' % (node_class.__name__), None) if method: - logger.debug('SphinxTranslator.dispatch_visit calling %s for %s', - method.__name__, node) method(node) break else: @@ -478,8 +476,6 @@ class SphinxTranslator(nodes.NodeVisitor): for node_class in node.__class__.__mro__: method = getattr(self, 'depart_%s' % (node_class.__name__), None) if method: - logger.debug('SphinxTranslator.dispatch_departure calling %s for %s', - method.__name__, node) method(node) break else: diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 98ddc5fa6..951d400a0 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -197,6 +197,14 @@ def isabstractmethod(obj: Any) -> bool: return safe_getattr(obj, '__isabstractmethod__', False) is True +def is_cython_function_or_method(obj: Any) -> bool: + """Check if the object is a function or method in cython.""" + try: + return obj.__class__.__name__ == 'cython_function_or_method' + except AttributeError: + return False + + def isattributedescriptor(obj: Any) -> bool: """Check if the object is an attribute like descriptor.""" if inspect.isdatadescriptor(object): @@ -207,6 +215,9 @@ def isattributedescriptor(obj: Any) -> bool: if isfunction(obj) or isbuiltin(obj) or inspect.ismethod(obj): # attribute must not be either function, builtin and method return False + elif is_cython_function_or_method(obj): + # attribute must not be either function and method (for cython) + return False elif inspect.isclass(obj): # attribute must not be a class return False diff --git a/sphinx/util/logging.py b/sphinx/util/logging.py index e9e367e36..1074d6c78 100644 --- a/sphinx/util/logging.py +++ b/sphinx/util/logging.py @@ -117,6 +117,7 @@ class SphinxWarningLogRecord(SphinxLogRecord): class SphinxLoggerAdapter(logging.LoggerAdapter): """LoggerAdapter allowing ``type`` and ``subtype`` keywords.""" + KEYWORDS = ['type', 'subtype', 'location', 'nonl', 'color', 'once'] def log(self, level: Union[int, str], msg: str, *args: Any, **kwargs: Any) -> None: if isinstance(level, int): @@ -130,16 +131,9 @@ class SphinxLoggerAdapter(logging.LoggerAdapter): def process(self, msg: str, kwargs: Dict) -> Tuple[str, Dict]: # type: ignore extra = kwargs.setdefault('extra', {}) - if 'type' in kwargs: - extra['type'] = kwargs.pop('type') - if 'subtype' in kwargs: - extra['subtype'] = kwargs.pop('subtype') - if 'location' in kwargs: - extra['location'] = kwargs.pop('location') - if 'nonl' in kwargs: - extra['nonl'] = kwargs.pop('nonl') - if 'color' in kwargs: - extra['color'] = kwargs.pop('color') + for keyword in self.KEYWORDS: + if keyword in kwargs: + extra[keyword] = kwargs.pop(keyword) return msg, kwargs @@ -445,6 +439,26 @@ class MessagePrefixFilter(logging.Filter): return True +class OnceFilter(logging.Filter): + """Show the message only once.""" + + def __init__(self, name: str = '') -> None: + super().__init__(name) + self.messages = {} # type: Dict[str, List] + + def filter(self, record: logging.LogRecord) -> bool: + once = getattr(record, 'once', '') + if not once: + return True + else: + params = self.messages.setdefault(record.msg, []) + if record.args in params: + return False + + params.append(record.args) + return True + + class SphinxLogRecordTranslator(logging.Filter): """Converts a log record to one Sphinx expects @@ -562,6 +576,7 @@ def setup(app: "Sphinx", status: IO, warning: IO) -> None: warning_handler.addFilter(WarningSuppressor(app)) warning_handler.addFilter(WarningLogRecordTranslator(app)) warning_handler.addFilter(WarningIsErrorFilter(app)) + warning_handler.addFilter(OnceFilter()) warning_handler.setLevel(logging.WARNING) warning_handler.setFormatter(ColorizeFormatter()) diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index f1b26f611..5261c097d 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -9,6 +9,7 @@ """ import re +import unicodedata import warnings from typing import Any, Callable, Iterable, List, Set, Tuple, Type from typing import TYPE_CHECKING, cast @@ -434,6 +435,79 @@ def inline_all_toctrees(builder: "Builder", docnameset: Set[str], docname: str, return tree +def _make_id(string: str) -> str: + """Convert `string` into an identifier and return it. + + This function is a modified version of ``docutils.nodes.make_id()`` of + docutils-0.16. + + Changes: + + * Allow to use dots (".") and underscores ("_") for an identifier + without a leading character. + + # Author: David Goodger + # Maintainer: docutils-develop@lists.sourceforge.net + # Copyright: This module has been placed in the public domain. + """ + id = string.lower() + id = id.translate(_non_id_translate_digraphs) + id = id.translate(_non_id_translate) + # get rid of non-ascii characters. + # 'ascii' lowercase to prevent problems with turkish locale. + id = unicodedata.normalize('NFKD', id).encode('ascii', 'ignore').decode('ascii') + # shrink runs of whitespace and replace by hyphen + id = _non_id_chars.sub('-', ' '.join(id.split())) + id = _non_id_at_ends.sub('', id) + return str(id) + + +_non_id_chars = re.compile('[^a-z0-9._]+') +_non_id_at_ends = re.compile('^[-0-9._]+|-+$') +_non_id_translate = { + 0x00f8: u'o', # o with stroke + 0x0111: u'd', # d with stroke + 0x0127: u'h', # h with stroke + 0x0131: u'i', # dotless i + 0x0142: u'l', # l with stroke + 0x0167: u't', # t with stroke + 0x0180: u'b', # b with stroke + 0x0183: u'b', # b with topbar + 0x0188: u'c', # c with hook + 0x018c: u'd', # d with topbar + 0x0192: u'f', # f with hook + 0x0199: u'k', # k with hook + 0x019a: u'l', # l with bar + 0x019e: u'n', # n with long right leg + 0x01a5: u'p', # p with hook + 0x01ab: u't', # t with palatal hook + 0x01ad: u't', # t with hook + 0x01b4: u'y', # y with hook + 0x01b6: u'z', # z with stroke + 0x01e5: u'g', # g with stroke + 0x0225: u'z', # z with hook + 0x0234: u'l', # l with curl + 0x0235: u'n', # n with curl + 0x0236: u't', # t with curl + 0x0237: u'j', # dotless j + 0x023c: u'c', # c with stroke + 0x023f: u's', # s with swash tail + 0x0240: u'z', # z with swash tail + 0x0247: u'e', # e with stroke + 0x0249: u'j', # j with stroke + 0x024b: u'q', # q with hook tail + 0x024d: u'r', # r with stroke + 0x024f: u'y', # y with stroke +} +_non_id_translate_digraphs = { + 0x00df: u'sz', # ligature sz + 0x00e6: u'ae', # ae + 0x0153: u'oe', # ligature oe + 0x0238: u'db', # db digraph + 0x0239: u'qp', # qp digraph +} + + def make_id(env: "BuildEnvironment", document: nodes.document, prefix: str = '', term: str = None) -> str: """Generate an appropriate node_id for given *prefix* and *term*.""" @@ -445,12 +519,12 @@ def make_id(env: "BuildEnvironment", document: nodes.document, # try to generate node_id by *term* if prefix and term: - node_id = nodes.make_id(idformat % term) + node_id = _make_id(idformat % term) if node_id == prefix: # *term* is not good to generate a node_id. node_id = None elif term: - node_id = nodes.make_id(term) + node_id = _make_id(term) if node_id == '': node_id = None # fallback to None diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index 508e28c22..07c4a59b8 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -11,6 +11,7 @@ import copy import os import posixpath +import re import warnings from typing import Any, Iterable, Tuple from typing import TYPE_CHECKING, cast @@ -37,6 +38,19 @@ logger = logging.getLogger(__name__) # http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html +def multiply_length(length: str, scale: int) -> str: + """Multiply *length* (width or height) by *scale*.""" + matched = re.match(r'^(\d*\.?\d*)\s*(\S*)$', length) + if not matched: + return length + elif scale == 100: + return length + else: + amount, unit = matched.groups() + result = float(amount) * scale / 100 + return "%s%s" % (int(result), unit) + + class HTMLWriter(Writer): # override embed-stylesheet default value to 0. @@ -596,11 +610,10 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): if 'height' in node: atts['height'] = node['height'] if 'scale' in node: - scale = node['scale'] / 100.0 if 'width' in atts: - atts['width'] = int(atts['width']) * scale + atts['width'] = multiply_length(atts['width'], node['scale']) if 'height' in atts: - atts['height'] = int(atts['height']) * scale + atts['height'] = multiply_length(atts['height'], node['scale']) atts['alt'] = node.get('alt', uri) if 'align' in node: atts['class'] = 'align-%s' % node['align'] diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index 164ec3659..0f2750270 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -10,6 +10,7 @@ import os import posixpath +import re import warnings from typing import Any, Iterable, Tuple from typing import TYPE_CHECKING, cast @@ -36,6 +37,19 @@ logger = logging.getLogger(__name__) # http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html +def multiply_length(length: str, scale: int) -> str: + """Multiply *length* (width or height) by *scale*.""" + matched = re.match(r'^(\d*\.?\d*)\s*(\S*)$', length) + if not matched: + return length + elif scale == 100: + return length + else: + amount, unit = matched.groups() + result = float(amount) * scale / 100 + return "%s%s" % (int(result), unit) + + class HTML5Translator(SphinxTranslator, BaseTranslator): """ Our custom HTML translator. @@ -537,11 +551,10 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): if 'height' in node: atts['height'] = node['height'] if 'scale' in node: - scale = node['scale'] / 100.0 if 'width' in atts: - atts['width'] = int(atts['width']) * scale + atts['width'] = multiply_length(atts['width'], node['scale']) if 'height' in atts: - atts['height'] = int(atts['height']) * scale + atts['height'] = multiply_length(atts['height'], node['scale']) atts['alt'] = node.get('alt', uri) if 'align' in node: atts['class'] = 'align-%s' % node['align'] diff --git a/tests/roots/test-ext-autodoc/target/cython.pyx b/tests/roots/test-ext-autodoc/target/cython.pyx new file mode 100644 index 000000000..1457db3c9 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/cython.pyx @@ -0,0 +1,12 @@ +# cython: binding=True + +def foo(*args, **kwargs): + """Docstring.""" + + +class Class: + """Docstring.""" + + def meth(self, name: str, age: int = 0) -> None: + """Docstring.""" + pass diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index b6323a46f..741c4bb60 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -22,6 +22,13 @@ from sphinx.testing.util import SphinxTestApp, Struct # NOQA from sphinx.util import logging from sphinx.util.docutils import LoggingReporter +try: + # Enable pyximport to test cython module + import pyximport + pyximport.install() +except ImportError: + pyximport = None + app = None @@ -361,7 +368,7 @@ def test_new_documenter(app): ' :module: target', '', ' documentation for the integer', - ' ' + '', ] @@ -420,7 +427,7 @@ def test_py_module(app, warning): ' :module: target', '', ' Function.', - ' ' + '', ] assert ("don't know which module to import for autodocumenting 'Class.meth'" not in warning.getvalue()) @@ -435,7 +442,7 @@ def test_autodoc_decorator(app): ' :module: target.decorator', '', ' docstring for deco1', - ' ' + '', ] actual = do_autodoc(app, 'decorator', 'target.decorator.deco2') @@ -445,7 +452,7 @@ def test_autodoc_decorator(app): ' :module: target.decorator', '', ' docstring for deco2', - ' ' + '', ] @@ -458,7 +465,7 @@ def test_autodoc_exception(app): ' :module: target', '', ' My custom exception.', - ' ' + '', ] @@ -726,7 +733,7 @@ def test_autodoc_subclass_of_builtin_class(app): ' :module: target', '', ' Docstring.', - ' ' + '', ] @@ -740,23 +747,23 @@ def test_autodoc_inner_class(app): ' :module: target', '', ' Foo', - ' ', - ' ', + '', + '', ' .. py:class:: Outer.Inner', ' :module: target', - ' ', + '', ' Foo', - ' ', - ' ', + '', + '', ' .. py:method:: Outer.Inner.meth()', ' :module: target', - ' ', + '', ' Foo', - ' ', - ' ', + '', + '', ' .. py:attribute:: Outer.factory', ' :module: target', - ' ', + '', ' alias of :class:`builtins.dict`' ] @@ -767,13 +774,13 @@ def test_autodoc_inner_class(app): ' :module: target.Outer', '', ' Foo', - ' ', - ' ', + '', + '', ' .. py:method:: Inner.meth()', ' :module: target.Outer', - ' ', + '', ' Foo', - ' ', + '', ] options['show-inheritance'] = True @@ -785,7 +792,7 @@ def test_autodoc_inner_class(app): ' Bases: :class:`target.Outer.Inner`', '', ' InnerChild docstring', - ' ' + '', ] @@ -799,7 +806,7 @@ def test_autodoc_classmethod(app): ' :classmethod:', '', ' Inherited class method.', - ' ' + '', ] @@ -813,7 +820,7 @@ def test_autodoc_staticmethod(app): ' :staticmethod:', '', ' Inherited static method.', - ' ' + '', ] @@ -827,19 +834,19 @@ def test_autodoc_descriptor(app): '.. py:class:: Class', ' :module: target.descriptor', '', - ' ', + '', ' .. py:attribute:: Class.descr', ' :module: target.descriptor', - ' ', + '', ' Descriptor instance docstring.', - ' ', - ' ', + '', + '', ' .. py:method:: Class.prop', ' :module: target.descriptor', ' :property:', - ' ', + '', ' Property.', - ' ' + '' ] @@ -854,7 +861,7 @@ def test_autodoc_c_module(app): " Convert a time tuple to a string, e.g. 'Sat Jun 06 16:26:11 1998'.", ' When the time tuple is not present, current time as returned by localtime()', ' is used.', - ' ' + '', ] @@ -946,7 +953,7 @@ def test_autodoc_module_scope(app): ' :value: <_io.StringIO object>', '', ' should be documented as well - süß', - ' ' + '', ] @@ -962,7 +969,7 @@ def test_autodoc_class_scope(app): ' :value: <_io.StringIO object>', '', ' should be documented as well - süß', - ' ' + '', ] @@ -976,16 +983,16 @@ def test_class_attributes(app): '.. py:class:: AttCls', ' :module: target', '', - ' ', + '', ' .. py:attribute:: AttCls.a1', ' :module: target', ' :value: hello world', - ' ', - ' ', + '', + '', ' .. py:attribute:: AttCls.a2', ' :module: target', ' :value: None', - ' ' + '' ] @@ -999,43 +1006,43 @@ def test_instance_attributes(app): ' :module: target', '', ' Class with documented class and instance attributes.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ca1', ' :module: target', " :value: 'a'", - ' ', + '', ' Doc comment for class attribute InstAttCls.ca1.', ' It can have multiple lines.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ca2', ' :module: target', " :value: 'b'", - ' ', + '', ' Doc comment for InstAttCls.ca2. One line only.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ca3', ' :module: target', " :value: 'c'", - ' ', + '', ' Docstring for class attribute InstAttCls.ca3.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ia1', ' :module: target', ' :value: None', - ' ', + '', ' Doc comment for instance attribute InstAttCls.ia1', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ia2', ' :module: target', ' :value: None', - ' ', + '', ' Docstring for instance attribute InstAttCls.ia2.', - ' ' + '' ] # pick up arbitrary attributes @@ -1047,22 +1054,22 @@ def test_instance_attributes(app): ' :module: target', '', ' Class with documented class and instance attributes.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ca1', ' :module: target', " :value: 'a'", - ' ', + '', ' Doc comment for class attribute InstAttCls.ca1.', ' It can have multiple lines.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ia1', ' :module: target', ' :value: None', - ' ', + '', ' Doc comment for instance attribute InstAttCls.ia1', - ' ' + '' ] @@ -1079,30 +1086,30 @@ def test_slots(app): '.. py:class:: Bar()', ' :module: target.slots', '', - ' ', + '', ' .. py:attribute:: Bar.attr1', ' :module: target.slots', - ' ', + '', ' docstring of attr1', - ' ', - ' ', + '', + '', ' .. py:attribute:: Bar.attr2', ' :module: target.slots', - ' ', + '', ' docstring of instance attr2', - ' ', - ' ', + '', + '', ' .. py:attribute:: Bar.attr3', ' :module: target.slots', - ' ', + '', '', '.. py:class:: Foo', ' :module: target.slots', '', - ' ', + '', ' .. py:attribute:: Foo.attr', ' :module: target.slots', - ' ', + '', ] @@ -1117,39 +1124,39 @@ def test_enum_class(app): ' :module: target.enum', '', ' this is enum class', - ' ', - ' ', + '', + '', ' .. py:method:: EnumCls.say_hello()', ' :module: target.enum', - ' ', + '', ' a method says hello to you.', - ' ', - ' ', + '', + '', ' .. py:attribute:: EnumCls.val1', ' :module: target.enum', ' :value: 12', - ' ', + '', ' doc for val1', - ' ', - ' ', + '', + '', ' .. py:attribute:: EnumCls.val2', ' :module: target.enum', ' :value: 23', - ' ', + '', ' doc for val2', - ' ', - ' ', + '', + '', ' .. py:attribute:: EnumCls.val3', ' :module: target.enum', ' :value: 34', - ' ', + '', ' doc for val3', - ' ', - ' ', + '', + '', ' .. py:attribute:: EnumCls.val4', ' :module: target.enum', ' :value: 34', - ' ' + '' ] # checks for an attribute of EnumClass @@ -1161,7 +1168,7 @@ def test_enum_class(app): ' :value: 12', '', ' doc for val1', - ' ' + '' ] @@ -1178,19 +1185,19 @@ def test_descriptor_class(app): ' :module: target.descriptor', '', ' Descriptor class docstring.', - ' ', - ' ', + '', + '', ' .. py:method:: CustomDataDescriptor.meth()', ' :module: target.descriptor', - ' ', + '', ' Function.', - ' ', + '', '', '.. py:class:: CustomDataDescriptor2(doc)', ' :module: target.descriptor', '', ' Descriptor class with custom metaclass docstring.', - ' ' + '', ] @@ -1203,7 +1210,7 @@ def test_autofunction_for_callable(app): ' :module: target.callable', '', ' A callable object that behaves like a function.', - ' ' + '', ] @@ -1216,7 +1223,7 @@ def test_autofunction_for_method(app): ' :module: target.callable', '', ' docstring of Callable.method().', - ' ' + '', ] @@ -1233,39 +1240,39 @@ def test_abstractmethods(): '.. py:class:: Base', ' :module: target.abstractmethods', '', - ' ', + '', ' .. py:method:: Base.abstractmeth()', ' :module: target.abstractmethods', ' :abstractmethod:', - ' ', - ' ', + '', + '', ' .. py:method:: Base.classmeth()', ' :module: target.abstractmethods', ' :abstractmethod:', ' :classmethod:', - ' ', - ' ', + '', + '', ' .. py:method:: Base.coroutinemeth()', ' :module: target.abstractmethods', ' :abstractmethod:', ' :async:', - ' ', - ' ', + '', + '', ' .. py:method:: Base.meth()', ' :module: target.abstractmethods', - ' ', - ' ', + '', + '', ' .. py:method:: Base.prop', ' :module: target.abstractmethods', ' :abstractmethod:', ' :property:', - ' ', - ' ', + '', + '', ' .. py:method:: Base.staticmeth()', ' :module: target.abstractmethods', ' :abstractmethod:', ' :staticmethod:', - ' ' + '', ] @@ -1282,25 +1289,25 @@ def test_partialfunction(): ' :module: target.partialfunction', '', ' docstring of func1', - ' ', + '', '', '.. py:function:: func2(b, c)', ' :module: target.partialfunction', '', ' docstring of func1', - ' ', + '', '', '.. py:function:: func3(c)', ' :module: target.partialfunction', '', ' docstring of func3', - ' ', + '', '', '.. py:function:: func4()', ' :module: target.partialfunction', '', ' docstring of func3', - ' ' + '', ] @@ -1328,7 +1335,7 @@ def test_bound_method(): ' :module: target.bound_method', '', ' Method docstring', - ' ', + '', ] @@ -1350,29 +1357,29 @@ def test_coroutine(): '.. py:class:: AsyncClass', ' :module: target.coroutine', '', - ' ', + '', ' .. py:method:: AsyncClass.do_coroutine()', ' :module: target.coroutine', ' :async:', - ' ', + '', ' A documented coroutine function', - ' ', - ' ', + '', + '', ' .. py:method:: AsyncClass.do_coroutine2()', ' :module: target.coroutine', ' :async:', ' :classmethod:', - ' ', + '', ' A documented coroutine classmethod', - ' ', - ' ', + '', + '', ' .. py:method:: AsyncClass.do_coroutine3()', ' :module: target.coroutine', ' :async:', ' :staticmethod:', - ' ', + '', ' A documented coroutine staticmethod', - ' ', + '', ] @@ -1384,21 +1391,21 @@ def test_partialmethod(app): ' :module: target.partialmethod', '', ' An example for partialmethod.', - ' ', + '', ' refs: https://docs.python.jp/3/library/functools.html#functools.partialmethod', - ' ', - ' ', + '', + '', ' .. py:method:: Cell.set_alive()', ' :module: target.partialmethod', - ' ', + '', ' Make a cell alive.', - ' ', - ' ', + '', + '', ' .. py:method:: Cell.set_state(state)', ' :module: target.partialmethod', - ' ', + '', ' Update state of cell to *state*.', - ' ', + '', ] options = {"members": None} @@ -1414,25 +1421,25 @@ def test_partialmethod_undoc_members(app): ' :module: target.partialmethod', '', ' An example for partialmethod.', - ' ', + '', ' refs: https://docs.python.jp/3/library/functools.html#functools.partialmethod', - ' ', - ' ', + '', + '', ' .. py:method:: Cell.set_alive()', ' :module: target.partialmethod', - ' ', + '', ' Make a cell alive.', - ' ', - ' ', + '', + '', ' .. py:method:: Cell.set_dead()', ' :module: target.partialmethod', - ' ', - ' ', + '', + '', ' .. py:method:: Cell.set_state(state)', ' :module: target.partialmethod', - ' ', + '', ' Update state of cell to *state*.', - ' ', + '', ] options = {"members": None, @@ -1455,48 +1462,48 @@ def test_autodoc_typed_instance_variables(app): '.. py:class:: Class()', ' :module: target.typed_vars', '', - ' ', + '', ' .. py:attribute:: Class.attr1', ' :module: target.typed_vars', ' :type: int', ' :value: 0', - ' ', - ' ', + '', + '', ' .. py:attribute:: Class.attr2', ' :module: target.typed_vars', ' :type: int', ' :value: None', - ' ', - ' ', + '', + '', ' .. py:attribute:: Class.attr3', ' :module: target.typed_vars', ' :type: int', ' :value: 0', - ' ', - ' ', + '', + '', ' .. py:attribute:: Class.attr4', ' :module: target.typed_vars', ' :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', @@ -1504,7 +1511,7 @@ def test_autodoc_typed_instance_variables(app): " :value: ''", '', ' attr1', - ' ', + '', '', '.. py:data:: attr2', ' :module: target.typed_vars', @@ -1512,7 +1519,7 @@ def test_autodoc_typed_instance_variables(app): ' :value: None', '', ' attr2', - ' ', + '', '', '.. py:data:: attr3', ' :module: target.typed_vars', @@ -1520,7 +1527,7 @@ def test_autodoc_typed_instance_variables(app): " :value: ''", '', ' attr3', - ' ' + '', ] @@ -1538,7 +1545,7 @@ def test_autodoc_Annotated(app): ' :module: target.annotated', '', ' docstring', - ' ' + '', ] @@ -1557,7 +1564,7 @@ def test_autodoc_for_egged_code(app): ' :value: 1', '', ' constant on sample.py', - ' ', + '', '', '.. py:function:: hello(s)', ' :module: sample', @@ -1580,7 +1587,7 @@ def test_singledispatch(): ' :module: target.singledispatch', '', ' A function for general use.', - ' ' + '', ] @@ -1599,13 +1606,44 @@ def test_singledispatchmethod(): ' :module: target.singledispatchmethod', '', ' docstring', - ' ', - ' ', + '', + '', ' .. py:method:: Foo.meth(arg, kwarg=None)', ' Foo.meth(arg: int, kwarg=None)', ' Foo.meth(arg: str, kwarg=None)', ' :module: target.singledispatchmethod', - ' ', + '', ' A method for general use.', - ' ' + '', + ] + + +@pytest.mark.usefixtures('setup_test') +@pytest.mark.skipif(pyximport is None, reason='cython is not installed') +def test_cython(): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.cython', options) + assert list(actual) == [ + '', + '.. py:module:: target.cython', + '', + '', + '.. py:class:: Class', + ' :module: target.cython', + '', + ' Docstring.', + '', + '', + ' .. py:method:: Class.meth(name: str, age: int = 0) -> None', + ' :module: target.cython', + '', + ' Docstring.', + '', + '', + '.. py:function:: foo(*args, **kwargs)', + ' :module: target.cython', + '', + ' Docstring.', + '', ] diff --git a/tests/test_build_epub.py b/tests/test_build_epub.py index cb60e79ad..379fbacc7 100644 --- a/tests/test_build_epub.py +++ b/tests/test_build_epub.py @@ -318,13 +318,13 @@ def test_epub_anchor_id(app): app.build() html = (app.outdir / 'index.xhtml').read_text() - assert ('

' + assert ('

' '' 'blah blah blah

' in html) - assert ('' + assert ('' '' '

blah blah blah

' in html) - assert 'see ' in html + assert 'see ' in html @pytest.mark.sphinx('epub', testroot='html_assets') diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 015ac3e33..3860f7da5 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -175,9 +175,9 @@ def test_html4_output(app, status, warning): r'-| |-'), ], 'autodoc.html': [ - (".//dl[@class='py class']/dt[@id='autodoc-target-class']", ''), - (".//dl[@class='py function']/dt[@id='autodoc-target-function']/em/span", r'\*\*'), - (".//dl[@class='py function']/dt[@id='autodoc-target-function']/em/span", r'kwds'), + (".//dl[@class='py class']/dt[@id='autodoc_target.class']", ''), + (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span", r'\*\*'), + (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span", r'kwds'), (".//dd/p", r'Return spam\.'), ], 'extapi.html': [ @@ -222,7 +222,7 @@ def test_html4_output(app, status, warning): "[@class='reference internal']/code/span[@class='pre']", 'HOME'), (".//a[@href='#with']" "[@class='reference internal']/code/span[@class='pre']", '^with$'), - (".//a[@href='#grammar-token-try-stmt']" + (".//a[@href='#grammar-token-try_stmt']" "[@class='reference internal']/code/span", '^statement$'), (".//a[@href='#some-label'][@class='reference internal']/span", '^here$'), (".//a[@href='#some-label'][@class='reference internal']/span", '^there$'), @@ -254,7 +254,7 @@ def test_html4_output(app, status, warning): (".//dl/dt[@id='term-boson']", 'boson'), # a production list (".//pre/strong", 'try_stmt'), - (".//pre/a[@href='#grammar-token-try1-stmt']/code/span", 'try1_stmt'), + (".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'), # tests for ``only`` directive (".//p", 'A global substitution.'), (".//p", 'In HTML.'), @@ -262,7 +262,7 @@ def test_html4_output(app, status, warning): (".//p", 'Always present'), # tests for ``any`` role (".//a[@href='#with']/span", 'headings'), - (".//a[@href='objects.html#func-without-body']/code/span", 'objects'), + (".//a[@href='objects.html#func_without_body']/code/span", 'objects'), # tests for numeric labels (".//a[@href='#id1'][@class='reference internal']/span", 'Testing various markup'), # tests for smartypants @@ -274,18 +274,18 @@ def test_html4_output(app, status, warning): (".//p", 'Il dit : « C’est “super” ! »'), ], 'objects.html': [ - (".//dt[@id='mod-cls-meth1']", ''), - (".//dt[@id='errmod-error']", ''), + (".//dt[@id='mod.cls.meth1']", ''), + (".//dt[@id='errmod.error']", ''), (".//dt/code", r'long\(parameter,\s* list\)'), (".//dt/code", 'another one'), - (".//a[@href='#mod-cls'][@class='reference internal']", ''), + (".//a[@href='#mod.cls'][@class='reference internal']", ''), (".//dl[@class='std userdesc']", ''), (".//dt[@id='userdesc-myobj']", ''), (".//a[@href='#userdesc-myobj'][@class='reference internal']", ''), # docfields (".//a[@class='reference internal'][@href='#timeint']/em", 'TimeInt'), (".//a[@class='reference internal'][@href='#time']", 'Time'), - (".//a[@class='reference internal'][@href='#errmod-error']/strong", 'Error'), + (".//a[@class='reference internal'][@href='#errmod.error']/strong", 'Error'), # C references (".//span[@class='pre']", 'CFunction()'), (".//a[@href='#c.Sphinx_DoSomething']", ''), @@ -324,7 +324,7 @@ def test_html4_output(app, status, warning): '\\+p'), (".//a[@class='reference internal'][@href='#cmdoption-perl-objc']/code/span", '--ObjC\\+\\+'), - (".//a[@class='reference internal'][@href='#cmdoption-perl-plugin-option']/code/span", + (".//a[@class='reference internal'][@href='#cmdoption-perl-plugin.option']/code/span", '--plugin.option'), (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-create-auth-token']" "/code/span", diff --git a/tests/test_domain_js.py b/tests/test_domain_js.py index 2c2e2b7cc..f7bacb90e 100644 --- a/tests/test_domain_js.py +++ b/tests/test_domain_js.py @@ -123,25 +123,25 @@ def test_domain_js_find_obj(app, status, warning): ('NestedParentA', ('roles', 'nestedparenta', 'class'))) assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') == ('NestedParentA.NestedChildA', - ('roles', 'nestedparenta-nestedchilda', 'class'))) + ('roles', 'nestedparenta.nestedchilda', 'class'))) assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') == ('NestedParentA.NestedChildA', - ('roles', 'nestedparenta-nestedchilda', 'class'))) + ('roles', 'nestedparenta.nestedchilda', 'class'))) assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'func') == ('NestedParentA.NestedChildA.subchild_1', - ('roles', 'nestedparenta-nestedchilda-subchild-1', 'function'))) + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'function'))) assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'func') == ('NestedParentA.NestedChildA.subchild_1', - ('roles', 'nestedparenta-nestedchilda-subchild-1', 'function'))) + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'function'))) assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'func') == ('NestedParentA.NestedChildA.subchild_1', - ('roles', 'nestedparenta-nestedchilda-subchild-1', 'function'))) + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'function'))) assert (find_obj('module_a.submodule', 'ModTopLevel', 'mod_child_2', 'meth') == ('module_a.submodule.ModTopLevel.mod_child_2', - ('module', 'module-a-submodule-modtoplevel-mod-child-2', 'method'))) + ('module', 'module_a.submodule.modtoplevel.mod_child_2', 'method'))) assert (find_obj('module_b.submodule', 'ModTopLevel', 'module_a.submodule', 'mod') == ('module_a.submodule', - ('module', 'module-module-a-submodule', 'module'))) + ('module', 'module-module_a.submodule', 'module'))) def test_get_full_qualified_name(): diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index b3a510a8b..e4bc17004 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -18,11 +18,11 @@ from sphinx import addnodes from sphinx.addnodes import ( desc, desc_addname, desc_annotation, desc_content, desc_name, desc_optional, desc_parameter, desc_parameterlist, desc_returns, desc_signature, - desc_sig_name, desc_sig_operator, desc_sig_punctuation, + desc_sig_name, desc_sig_operator, desc_sig_punctuation, pending_xref, ) from sphinx.domains import IndexEntry from sphinx.domains.python import ( - py_sig_re, _pseudo_parse_arglist, PythonDomain, PythonModuleIndex + py_sig_re, _parse_annotation, _pseudo_parse_arglist, PythonDomain, PythonModuleIndex ) from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node @@ -78,7 +78,7 @@ def test_domain_py_xrefs(app, status, warning): assert_node(node, **attributes) doctree = app.env.get_doctree('roles') - refnodes = list(doctree.traverse(addnodes.pending_xref)) + refnodes = list(doctree.traverse(pending_xref)) assert_refnode(refnodes[0], None, None, 'TopLevel', 'class') assert_refnode(refnodes[1], None, None, 'top_level', 'meth') assert_refnode(refnodes[2], None, 'NestedParentA', 'child_1', 'meth') @@ -96,7 +96,7 @@ def test_domain_py_xrefs(app, status, warning): assert len(refnodes) == 13 doctree = app.env.get_doctree('module') - refnodes = list(doctree.traverse(addnodes.pending_xref)) + refnodes = list(doctree.traverse(pending_xref)) assert_refnode(refnodes[0], 'module_a.submodule', None, 'ModTopLevel', 'class') assert_refnode(refnodes[1], 'module_a.submodule', 'ModTopLevel', @@ -125,7 +125,7 @@ def test_domain_py_xrefs(app, status, warning): assert len(refnodes) == 16 doctree = app.env.get_doctree('module_option') - refnodes = list(doctree.traverse(addnodes.pending_xref)) + refnodes = list(doctree.traverse(pending_xref)) print(refnodes) print(refnodes[0]) print(refnodes[1]) @@ -171,11 +171,11 @@ def test_resolve_xref_for_properties(app, status, warning): app.builder.build_all() content = (app.outdir / 'module.html').read_text() - assert ('Link to ' '' 'prop attribute' in content) - assert ('Link to ' '' 'prop method' in content) @@ -194,18 +194,18 @@ def test_domain_py_find_obj(app, status, warning): assert (find_obj(None, None, 'NestedParentA', 'class') == [('NestedParentA', ('roles', 'nestedparenta', 'class'))]) assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') == - [('NestedParentA.NestedChildA', ('roles', 'nestedparenta-nestedchilda', 'class'))]) + [('NestedParentA.NestedChildA', ('roles', 'nestedparenta.nestedchilda', 'class'))]) assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') == - [('NestedParentA.NestedChildA', ('roles', 'nestedparenta-nestedchilda', 'class'))]) + [('NestedParentA.NestedChildA', ('roles', 'nestedparenta.nestedchilda', 'class'))]) assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'meth') == [('NestedParentA.NestedChildA.subchild_1', - ('roles', 'nestedparenta-nestedchilda-subchild-1', 'method'))]) + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'method'))]) assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') == [('NestedParentA.NestedChildA.subchild_1', - ('roles', 'nestedparenta-nestedchilda-subchild-1', 'method'))]) + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'method'))]) assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'meth') == [('NestedParentA.NestedChildA.subchild_1', - ('roles', 'nestedparenta-nestedchilda-subchild-1', 'method'))]) + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'method'))]) def test_get_full_qualified_name(): @@ -236,13 +236,44 @@ def test_get_full_qualified_name(): assert domain.get_full_qualified_name(node) == 'module1.Class.func' +def test_parse_annotation(): + doctree = _parse_annotation("int") + assert_node(doctree, ([pending_xref, "int"],)) + + doctree = _parse_annotation("List[int]") + assert_node(doctree, ([pending_xref, "List"], + [desc_sig_punctuation, "["], + [pending_xref, "int"], + [desc_sig_punctuation, "]"])) + + doctree = _parse_annotation("Tuple[int, int]") + assert_node(doctree, ([pending_xref, "Tuple"], + [desc_sig_punctuation, "["], + [pending_xref, "int"], + [desc_sig_punctuation, ", "], + [pending_xref, "int"], + [desc_sig_punctuation, "]"])) + + doctree = _parse_annotation("Callable[[int, int], int]") + assert_node(doctree, ([pending_xref, "Callable"], + [desc_sig_punctuation, "["], + [desc_sig_punctuation, "["], + [pending_xref, "int"], + [desc_sig_punctuation, ", "], + [pending_xref, "int"], + [desc_sig_punctuation, "]"], + [desc_sig_punctuation, ", "], + [pending_xref, "int"], + [desc_sig_punctuation, "]"])) + + def test_pyfunction_signature(app): text = ".. py:function:: hello(name: str) -> str" doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, [desc, ([desc_signature, ([desc_name, "hello"], desc_parameterlist, - [desc_returns, "str"])], + [desc_returns, pending_xref, "str"])], desc_content)])) assert_node(doctree[1], addnodes.desc, desctype="function", domain="py", objtype="function", noindex=False) @@ -250,7 +281,7 @@ def test_pyfunction_signature(app): [desc_parameterlist, desc_parameter, ([desc_sig_name, "name"], [desc_sig_punctuation, ":"], " ", - [nodes.inline, "str"])]) + [nodes.inline, pending_xref, "str"])]) def test_pyfunction_signature_full(app): @@ -260,7 +291,7 @@ def test_pyfunction_signature_full(app): assert_node(doctree, (addnodes.index, [desc, ([desc_signature, ([desc_name, "hello"], desc_parameterlist, - [desc_returns, "str"])], + [desc_returns, pending_xref, "str"])], desc_content)])) assert_node(doctree[1], addnodes.desc, desctype="function", domain="py", objtype="function", noindex=False) @@ -268,7 +299,7 @@ def test_pyfunction_signature_full(app): [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "a"], [desc_sig_punctuation, ":"], " ", - [desc_sig_name, "str"])], + [desc_sig_name, pending_xref, "str"])], [desc_parameter, ([desc_sig_name, "b"], [desc_sig_operator, "="], [nodes.inline, "1"])], @@ -276,11 +307,11 @@ def test_pyfunction_signature_full(app): [desc_sig_name, "args"], [desc_sig_punctuation, ":"], " ", - [desc_sig_name, "str"])], + [desc_sig_name, pending_xref, "str"])], [desc_parameter, ([desc_sig_name, "c"], [desc_sig_punctuation, ":"], " ", - [desc_sig_name, "bool"], + [desc_sig_name, pending_xref, "bool"], " ", [desc_sig_operator, "="], " ", @@ -289,7 +320,7 @@ def test_pyfunction_signature_full(app): [desc_sig_name, "kwargs"], [desc_sig_punctuation, ":"], " ", - [desc_sig_name, "str"])])]) + [desc_sig_name, pending_xref, "str"])])]) @pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') @@ -340,7 +371,7 @@ def test_optional_pyfunction_signature(app): assert_node(doctree, (addnodes.index, [desc, ([desc_signature, ([desc_name, "compile"], desc_parameterlist, - [desc_returns, "ast object"])], + [desc_returns, pending_xref, "ast object"])], desc_content)])) assert_node(doctree[1], addnodes.desc, desctype="function", domain="py", objtype="function", noindex=False) @@ -483,61 +514,61 @@ def test_pymethod_options(app): # method assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth1() (Class method)', 'class-meth1', '', None)]) + entries=[('single', 'meth1() (Class method)', 'class.meth1', '', None)]) assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "meth1"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth1' in domain.objects - assert domain.objects['Class.meth1'] == ('index', 'class-meth1', 'method') + assert domain.objects['Class.meth1'] == ('index', 'class.meth1', 'method') # :classmethod: assert_node(doctree[1][1][2], addnodes.index, - entries=[('single', 'meth2() (Class class method)', 'class-meth2', '', None)]) + entries=[('single', 'meth2() (Class class method)', 'class.meth2', '', None)]) assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, "classmethod "], [desc_name, "meth2"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth2' in domain.objects - assert domain.objects['Class.meth2'] == ('index', 'class-meth2', 'method') + assert domain.objects['Class.meth2'] == ('index', 'class.meth2', 'method') # :staticmethod: assert_node(doctree[1][1][4], addnodes.index, - entries=[('single', 'meth3() (Class static method)', 'class-meth3', '', None)]) + entries=[('single', 'meth3() (Class static method)', 'class.meth3', '', None)]) assert_node(doctree[1][1][5], ([desc_signature, ([desc_annotation, "static "], [desc_name, "meth3"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth3' in domain.objects - assert domain.objects['Class.meth3'] == ('index', 'class-meth3', 'method') + assert domain.objects['Class.meth3'] == ('index', 'class.meth3', 'method') # :async: assert_node(doctree[1][1][6], addnodes.index, - entries=[('single', 'meth4() (Class method)', 'class-meth4', '', None)]) + entries=[('single', 'meth4() (Class method)', 'class.meth4', '', None)]) assert_node(doctree[1][1][7], ([desc_signature, ([desc_annotation, "async "], [desc_name, "meth4"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth4' in domain.objects - assert domain.objects['Class.meth4'] == ('index', 'class-meth4', 'method') + assert domain.objects['Class.meth4'] == ('index', 'class.meth4', 'method') # :property: assert_node(doctree[1][1][8], addnodes.index, - entries=[('single', 'meth5() (Class property)', 'class-meth5', '', None)]) + entries=[('single', 'meth5() (Class property)', 'class.meth5', '', None)]) assert_node(doctree[1][1][9], ([desc_signature, ([desc_annotation, "property "], [desc_name, "meth5"])], [desc_content, ()])) assert 'Class.meth5' in domain.objects - assert domain.objects['Class.meth5'] == ('index', 'class-meth5', 'method') + assert domain.objects['Class.meth5'] == ('index', 'class.meth5', 'method') # :abstractmethod: assert_node(doctree[1][1][10], addnodes.index, - entries=[('single', 'meth6() (Class method)', 'class-meth6', '', None)]) + entries=[('single', 'meth6() (Class method)', 'class.meth6', '', None)]) assert_node(doctree[1][1][11], ([desc_signature, ([desc_annotation, "abstract "], [desc_name, "meth6"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth6' in domain.objects - assert domain.objects['Class.meth6'] == ('index', 'class-meth6', 'method') + assert domain.objects['Class.meth6'] == ('index', 'class.meth6', 'method') def test_pyclassmethod(app): @@ -552,13 +583,13 @@ def test_pyclassmethod(app): [desc_content, (addnodes.index, desc)])])) assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth() (Class class method)', 'class-meth', '', None)]) + entries=[('single', 'meth() (Class class method)', 'class.meth', '', None)]) assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "classmethod "], [desc_name, "meth"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth' in domain.objects - assert domain.objects['Class.meth'] == ('index', 'class-meth', 'method') + assert domain.objects['Class.meth'] == ('index', 'class.meth', 'method') def test_pystaticmethod(app): @@ -573,13 +604,13 @@ def test_pystaticmethod(app): [desc_content, (addnodes.index, desc)])])) assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth() (Class static method)', 'class-meth', '', None)]) + entries=[('single', 'meth() (Class static method)', 'class.meth', '', None)]) assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "static "], [desc_name, "meth"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth' in domain.objects - assert domain.objects['Class.meth'] == ('index', 'class-meth', 'method') + assert domain.objects['Class.meth'] == ('index', 'class.meth', 'method') def test_pyattribute(app): @@ -596,13 +627,13 @@ def test_pyattribute(app): [desc_content, (addnodes.index, desc)])])) assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'attr (Class attribute)', 'class-attr', '', None)]) + entries=[('single', 'attr (Class attribute)', 'class.attr', '', None)]) 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', 'class-attr', 'attribute') + assert domain.objects['Class.attr'] == ('index', 'class.attr', 'attribute') def test_pydecorator_signature(app): @@ -648,10 +679,10 @@ def test_module_index(app): assert index.generate() == ( [('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), ('s', [IndexEntry('sphinx', 1, 'index', 'module-sphinx', '', '', ''), - IndexEntry('sphinx.builders', 2, 'index', 'module-sphinx-builders', '', '', ''), # NOQA - IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx-builders-html', '', '', ''), # NOQA - IndexEntry('sphinx.config', 2, 'index', 'module-sphinx-config', '', '', ''), - IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx-intl', '', '', '')])], + IndexEntry('sphinx.builders', 2, 'index', 'module-sphinx.builders', '', '', ''), # NOQA + IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', ''), # NOQA + IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', ''), + IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])], False ) @@ -663,7 +694,7 @@ def test_module_index_submodule(app): index = PythonModuleIndex(app.env.get_domain('py')) assert index.generate() == ( [('s', [IndexEntry('sphinx', 1, '', '', '', '', ''), - IndexEntry('sphinx.config', 2, 'index', 'module-sphinx-config', '', '', '')])], + IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', '')])], False ) @@ -692,12 +723,12 @@ def test_modindex_common_prefix(app): restructuredtext.parse(app, text) index = PythonModuleIndex(app.env.get_domain('py')) assert index.generate() == ( - [('b', [IndexEntry('sphinx.builders', 1, 'index', 'module-sphinx-builders', '', '', ''), # NOQA - IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx-builders-html', '', '', '')]), # NOQA - ('c', [IndexEntry('sphinx.config', 0, 'index', 'module-sphinx-config', '', '', '')]), + [('b', [IndexEntry('sphinx.builders', 1, 'index', 'module-sphinx.builders', '', '', ''), # NOQA + IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', '')]), # NOQA + ('c', [IndexEntry('sphinx.config', 0, 'index', 'module-sphinx.config', '', '', '')]), ('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', ''), - IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx-intl', '', '', '')])], + IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])], True ) diff --git a/tests/test_environment.py b/tests/test_environment.py index 4b1f8e77e..7290eb6a0 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -84,7 +84,7 @@ def test_object_inventory(app): refs = app.env.domaindata['py']['objects'] assert 'func_without_module' in refs - assert refs['func_without_module'] == ('objects', 'func-without-module', 'function') + assert refs['func_without_module'] == ('objects', 'func_without_module', 'function') assert 'func_without_module2' in refs assert 'mod.func_in_module' in refs assert 'mod.Cls' in refs diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index e250b21b3..f351d0e4b 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -31,49 +31,49 @@ def test_autoclass_content_class(app): ' :module: target.autoclass_content', '', ' A class having no __init__, no __new__', - ' ', + '', '', '.. py:class:: B()', ' :module: target.autoclass_content', '', ' A class having __init__(no docstring), no __new__', - ' ', + '', '', '.. py:class:: C()', ' :module: target.autoclass_content', '', ' A class having __init__, no __new__', - ' ', + '', '', '.. py:class:: D', ' :module: target.autoclass_content', '', ' A class having no __init__, __new__(no docstring)', - ' ', + '', '', '.. py:class:: E', ' :module: target.autoclass_content', '', ' A class having no __init__, __new__', - ' ', + '', '', '.. py:class:: F()', ' :module: target.autoclass_content', '', ' A class having both __init__ and __new__', - ' ', + '', '', '.. py:class:: G()', ' :module: target.autoclass_content', '', ' A class inherits __init__ without docstring.', - ' ', + '', '', '.. py:class:: H()', ' :module: target.autoclass_content', '', ' A class inherits __new__ without docstring.', - ' ' + '', ] @@ -91,49 +91,49 @@ def test_autoclass_content_init(app): ' :module: target.autoclass_content', '', ' A class having no __init__, no __new__', - ' ', + '', '', '.. py:class:: B()', ' :module: target.autoclass_content', '', ' A class having __init__(no docstring), no __new__', - ' ', + '', '', '.. py:class:: C()', ' :module: target.autoclass_content', '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: D', ' :module: target.autoclass_content', '', ' A class having no __init__, __new__(no docstring)', - ' ', + '', '', '.. py:class:: E', ' :module: target.autoclass_content', '', ' __new__ docstring', - ' ', + '', '', '.. py:class:: F()', ' :module: target.autoclass_content', '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: G()', ' :module: target.autoclass_content', '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: H()', ' :module: target.autoclass_content', '', ' __new__ docstring', - ' ' + '', ] @@ -151,59 +151,59 @@ def test_autoclass_content_both(app): ' :module: target.autoclass_content', '', ' A class having no __init__, no __new__', - ' ', + '', '', '.. py:class:: B()', ' :module: target.autoclass_content', '', ' A class having __init__(no docstring), no __new__', - ' ', + '', '', '.. py:class:: C()', ' :module: target.autoclass_content', '', ' A class having __init__, no __new__', - ' ', + '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: D', ' :module: target.autoclass_content', '', ' A class having no __init__, __new__(no docstring)', - ' ', + '', '', '.. py:class:: E', ' :module: target.autoclass_content', '', ' A class having no __init__, __new__', - ' ', + '', ' __new__ docstring', - ' ', + '', '', '.. py:class:: F()', ' :module: target.autoclass_content', '', ' A class having both __init__ and __new__', - ' ', + '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: G()', ' :module: target.autoclass_content', '', ' A class inherits __init__ without docstring.', - ' ', + '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: H()', ' :module: target.autoclass_content', '', ' A class inherits __new__ without docstring.', - ' ', + '', ' __new__ docstring', - ' ' + '', ] @@ -217,7 +217,7 @@ def test_autodoc_inherit_docstrings(app): ' :module: target.inheritance', '', ' Inherited function.', - ' ' + '', ] # disable autodoc_inherit_docstrings @@ -240,38 +240,38 @@ def test_autodoc_docstring_signature(app): '.. py:class:: DocstringSig', ' :module: target', '', - ' ', + '', ' .. py:method:: DocstringSig.meth(FOO, BAR=1) -> BAZ', ' :module: target', - ' ', + '', ' First line of docstring', - ' ', + '', ' rest of docstring', - ' ', - ' ', + '', + '', ' .. py:method:: DocstringSig.meth2()', ' :module: target', - ' ', + '', ' First line, no signature', ' Second line followed by indentation::', - ' ', + '', ' indented line', - ' ', - ' ', + '', + '', ' .. py:method:: DocstringSig.prop1', ' :module: target', ' :property:', - ' ', + '', ' First line of docstring', - ' ', - ' ', + '', + '', ' .. py:method:: DocstringSig.prop2', ' :module: target', ' :property:', - ' ', + '', ' First line of docstring', ' Second line of docstring', - ' ' + '', ] # disable autodoc_docstring_signature @@ -282,41 +282,41 @@ def test_autodoc_docstring_signature(app): '.. py:class:: DocstringSig', ' :module: target', '', - ' ', + '', ' .. py:method:: DocstringSig.meth()', ' :module: target', - ' ', + '', ' meth(FOO, BAR=1) -> BAZ', ' First line of docstring', - ' ', + '', ' rest of docstring', - ' ', - ' ', - ' ', + '', + '', + '', ' .. py:method:: DocstringSig.meth2()', ' :module: target', - ' ', + '', ' First line, no signature', ' Second line followed by indentation::', - ' ', + '', ' indented line', - ' ', - ' ', + '', + '', ' .. py:method:: DocstringSig.prop1', ' :module: target', ' :property:', - ' ', + '', ' DocstringSig.prop1(self)', ' First line of docstring', - ' ', - ' ', + '', + '', ' .. py:method:: DocstringSig.prop2', ' :module: target', ' :property:', - ' ', + '', ' First line of docstring', ' Second line of docstring', - ' ' + '', ] @@ -397,13 +397,13 @@ def test_autoclass_content_and_docstring_signature_both(app): ' :module: target.docstring_signature', '', ' B(foo, bar, baz)', - ' ', + '', '', '.. py:class:: C(foo, bar)', ' :module: target.docstring_signature', '', ' C(foo, bar, baz)', - ' ', + '', '', '.. py:class:: D(foo, bar, baz)', ' :module: target.docstring_signature', @@ -439,25 +439,25 @@ def test_mocked_module_imports(app, warning): ' :module: target.need_mocks', '', ' TestAutodoc docstring.', - ' ', - ' ', + '', + '', ' .. py:method:: TestAutodoc.decoratedMethod()', ' :module: target.need_mocks', - ' ', + '', ' TestAutodoc::decoratedMethod docstring', - ' ', + '', '', '.. py:function:: decoratedFunction()', ' :module: target.need_mocks', '', ' decoratedFunction docstring', - ' ', + '', '', '.. py:function:: func(arg: missing_module.Class)', ' :module: target.need_mocks', '', ' a function takes mocked object as an argument', - ' ' + '', ] assert warning.getvalue() == '' @@ -476,22 +476,22 @@ def test_autodoc_typehints_signature(app): '.. py:class:: Math(s: str, o: object = None)', ' :module: target.typehints', '', - ' ', + '', ' .. py:method:: Math.decr(a: int, b: int = 1) -> int', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.horse(a: str, b: int) -> None', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.incr(a: int, b: int = 1) -> int', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.nothing() -> None', ' :module: target.typehints', - ' ', + '', '', '.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, ' 'Union[str, Unknown]] = None, *args: str, **kwargs: str) -> None', @@ -526,22 +526,22 @@ def test_autodoc_typehints_none(app): '.. py:class:: Math(s, o=None)', ' :module: target.typehints', '', - ' ', + '', ' .. py:method:: Math.decr(a, b=1)', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.horse(a, b)', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.incr(a, b=1)', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.nothing()', ' :module: target.typehints', - ' ', + '', '', '.. py:function:: complex_func(arg1, arg2, arg3=None, *args, **kwargs)', ' :module: target.typehints', diff --git a/tests/test_ext_autodoc_events.py b/tests/test_ext_autodoc_events.py index 91fc19630..106c5793a 100644 --- a/tests/test_ext_autodoc_events.py +++ b/tests/test_ext_autodoc_events.py @@ -44,7 +44,7 @@ def test_cut_lines(app): ' :module: target.process_docstring', '', ' second line', - ' ' + '', ] @@ -60,7 +60,7 @@ def test_between(app): ' :module: target.process_docstring', '', ' second line', - ' ' + '', ] @@ -77,5 +77,5 @@ def test_between_exclude(app): '', ' first line', ' third line', - ' ' + '', ] diff --git a/tests/test_ext_autodoc_private_members.py b/tests/test_ext_autodoc_private_members.py index e8f3e53ef..2d9208b41 100644 --- a/tests/test_ext_autodoc_private_members.py +++ b/tests/test_ext_autodoc_private_members.py @@ -40,7 +40,7 @@ def test_private_field_and_private_members(app): ' :module: target.private', '', ' private_function is a docstring().', - ' ', + '', ' :meta private:', - ' ' + '', ] diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index 2ce754eff..160079a50 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -1020,6 +1020,34 @@ Sooper Warning: actual = str(GoogleDocstring(docstring, testConfig)) self.assertEqual(expected, actual) + def test_noindex(self): + docstring = """ +Attributes: + arg + description + +Methods: + func(i, j) + description +""" + + expected = """ +.. attribute:: arg + :noindex: + + description + +.. method:: func(i, j) + :noindex: + + + description +""" + config = Config() + actual = str(GoogleDocstring(docstring, config=config, app=None, what='module', + options={'noindex': True})) + self.assertEqual(expected, actual) + class NumpyDocstringTest(BaseDocstringTest): docstrings = [( diff --git a/tests/test_intl.py b/tests/test_intl.py index ee96490a4..0e7dd4f62 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -870,7 +870,7 @@ def test_xml_refs_in_python_domain(app): assert_elem( para0[0], ['SEE THIS DECORATOR:', 'sensitive_variables()', '.'], - ['sensitive-sensitive-variables']) + ['sensitive.sensitive_variables']) @sphinx_intl diff --git a/tests/test_util_logging.py b/tests/test_util_logging.py index 85646112d..337782d87 100644 --- a/tests/test_util_logging.py +++ b/tests/test_util_logging.py @@ -103,6 +103,17 @@ def test_nonl_info_log(app, status, warning): assert 'message1message2\nmessage3' in status.getvalue() +def test_once_warning_log(app, status, warning): + logging.setup(app, status, warning) + logger = logging.getLogger(__name__) + + logger.warning('message: %d', 1, once=True) + logger.warning('message: %d', 1, once=True) + logger.warning('message: %d', 2, once=True) + + assert 'WARNING: message: 1\nWARNING: message: 2\n' in strip_escseq(warning.getvalue()) + + def test_is_suppressed_warning(): suppress_warnings = ["ref", "files.*", "rest.duplicated_labels"] diff --git a/tests/test_util_nodes.py b/tests/test_util_nodes.py index 1833414b0..01c5d2e3f 100644 --- a/tests/test_util_nodes.py +++ b/tests/test_util_nodes.py @@ -189,9 +189,9 @@ def test_clean_astext(): ('', '', 'id0'), ('term', '', 'term-0'), ('term', 'Sphinx', 'term-sphinx'), - ('', 'io.StringIO', 'io-stringio'), # contains a dot - ('', 'sphinx.setup_command', 'sphinx-setup-command'), # contains a dot - ('', '_io.StringIO', 'io-stringio'), # starts with underscore + ('', 'io.StringIO', 'io.stringio'), # contains a dot + ('', 'sphinx.setup_command', 'sphinx.setup_command'), # contains a dot & underscore + ('', '_io.StringIO', 'io.stringio'), # starts with underscore ('', 'sphinx', 'sphinx'), # alphabets in unicode fullwidth characters ('', '悠好', 'id0'), # multibytes text (in Chinese) ('', 'Hello=悠好=こんにちは', 'hello'), # alphabets and multibytes text