diff options
| -rw-r--r-- | CHANGES | 7 | ||||
| -rw-r--r-- | doc/extdev/deprecated.rst | 5 | ||||
| -rw-r--r-- | doc/usage/restructuredtext/directives.rst | 6 | ||||
| -rw-r--r-- | setup.py | 1 | ||||
| -rw-r--r-- | sphinx/domains/python.py | 59 | ||||
| -rw-r--r-- | sphinx/ext/autodoc/__init__.py | 11 | ||||
| -rw-r--r-- | sphinx/ext/autosummary/__init__.py | 5 | ||||
| -rw-r--r-- | sphinx/ext/napoleon/docstring.py | 8 | ||||
| -rw-r--r-- | sphinx/project.py | 9 | ||||
| -rw-r--r-- | sphinx/testing/util.py | 2 | ||||
| -rw-r--r-- | sphinx/util/inspect.py | 11 | ||||
| -rw-r--r-- | sphinx/util/logging.py | 35 | ||||
| -rw-r--r-- | sphinx/writers/html.py | 19 | ||||
| -rw-r--r-- | sphinx/writers/html5.py | 19 | ||||
| -rw-r--r-- | tests/roots/test-ext-autodoc/target/cython.pyx | 12 | ||||
| -rw-r--r-- | tests/test_autodoc.py | 358 | ||||
| -rw-r--r-- | tests/test_domain_py.py | 57 | ||||
| -rw-r--r-- | tests/test_ext_autodoc_configs.py | 166 | ||||
| -rw-r--r-- | tests/test_ext_autodoc_events.py | 6 | ||||
| -rw-r--r-- | tests/test_ext_autodoc_private_members.py | 4 | ||||
| -rw-r--r-- | tests/test_ext_napoleon_docstring.py | 28 | ||||
| -rw-r--r-- | tests/test_util_logging.py | 11 |
22 files changed, 552 insertions, 287 deletions
@@ -54,6 +54,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()`` @@ -83,6 +84,7 @@ Features added * #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 @@ -114,6 +116,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 ---------- @@ -128,9 +132,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`` @@ -141,6 +147,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 +* #6240: napoleon: Attributes and Methods sections ignore :noindex: option Testing -------- diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index dc8f9e7b2..9551f8613 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -51,6 +51,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:: @@ -52,6 +52,7 @@ extras_require = { 'pytest-cov', 'html5lib', 'typed_ast', # for py35-37 + 'cython', ], } diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 262347b55..fae1991c7 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 @@ -67,6 +68,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) @@ -93,9 +146,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(' ') @@ -354,7 +408,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: diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 0832aeb3e..7b6b3e4aa 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -270,7 +270,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]]: @@ -1008,7 +1011,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: @@ -1427,7 +1431,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 f79b7feb4..9c550c622 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 @@ -110,6 +110,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 = {} @@ -766,7 +768,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 7f6ebe478..820de6ee4 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -583,7 +583,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: @@ -641,6 +645,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 d63af1fcb..f4afdadad 100644 --- a/sphinx/project.py +++ b/sphinx/project.py @@ -9,6 +9,7 @@ """ import os +from glob import glob from sphinx.locale import __ from sphinx.util import get_matching_files @@ -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/inspect.py b/sphinx/util/inspect.py index d22df7656..855b11d83 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 a2dee807d..fbf161ec0 100644 --- a/sphinx/util/logging.py +++ b/sphinx/util/logging.py @@ -118,6 +118,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): @@ -131,16 +132,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 @@ -446,6 +440,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 @@ -563,6 +577,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/writers/html.py b/sphinx/writers/html.py index e74c0334f..85eeb4376 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 cast @@ -38,6 +39,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. @@ -597,11 +611,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 0c00a1fa4..f94dd60a3 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 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 HTML5Translator(SphinxTranslator, BaseTranslator): """ Our custom HTML translator. @@ -538,11 +552,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_domain_py.py b/tests/test_domain_py.py index 3a0ce1c68..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]) @@ -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) 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_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"] |
