Merge branch 'master' into 1618-make-search-results-reader-friendly

This commit is contained in:
Timotheus Kampik
2018-08-29 19:01:13 +02:00
10 changed files with 165 additions and 78 deletions

View File

@@ -21,6 +21,8 @@ Features added
__ https://github.com/sphinx-contrib/sphinx-pretty-searchresults __ https://github.com/sphinx-contrib/sphinx-pretty-searchresults
* #4182: autodoc: Support :confval:`suppress_warnings`
Bugs fixed Bugs fixed
---------- ----------

View File

@@ -45,6 +45,10 @@ docstrings to correct reStructuredText before :mod:`autodoc` processes them.
.. _NumPy: .. _NumPy:
https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt
Directives
----------
:mod:`autodoc` provides several directives that are versions of the usual :mod:`autodoc` provides several directives that are versions of the usual
:rst:dir:`py:module`, :rst:dir:`py:class` and so forth. On parsing time, they :rst:dir:`py:module`, :rst:dir:`py:class` and so forth. On parsing time, they
import the corresponding module and extract the docstring of the given objects, import the corresponding module and extract the docstring of the given objects,
@@ -306,6 +310,9 @@ inserting them into the page source under a suitable :rst:dir:`py:module`,
well-behaved decorating functions. well-behaved decorating functions.
Configuration
-------------
There are also new config values that you can set: There are also new config values that you can set:
.. confval:: autoclass_content .. confval:: autoclass_content
@@ -432,6 +439,16 @@ There are also new config values that you can set:
.. versionadded:: 1.7 .. versionadded:: 1.7
.. confval:: suppress_warnings
:noindex:
:mod:`autodoc` supports to suppress warning messages via
:confval:`suppress_warnings`. It allows following warnings types in
addition:
* autodoc
* autodoc.import_object
Docstring preprocessing Docstring preprocessing
----------------------- -----------------------

View File

@@ -15,6 +15,7 @@ from __future__ import absolute_import
import gzip import gzip
import re import re
from os import path from os import path
from typing import Any
from docutils import nodes from docutils import nodes
@@ -23,6 +24,7 @@ from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.environment.adapters.indexentries import IndexEntries from sphinx.environment.adapters.indexentries import IndexEntries
from sphinx.locale import __ from sphinx.locale import __
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.nodes import NodeMatcher
from sphinx.util.osutil import make_filename from sphinx.util.osutil import make_filename
try: try:
@@ -32,7 +34,7 @@ except ImportError:
if False: if False:
# For type annotation # For type annotation
from typing import Any, Dict, List # NOQA from typing import Dict, List # NOQA
from sphinx.application import Sphinx # NOQA from sphinx.application import Sphinx # NOQA
@@ -100,12 +102,8 @@ class DevhelpBuilder(StandaloneHTMLBuilder):
parent.attrib['link'] = node['refuri'] parent.attrib['link'] = node['refuri']
parent.attrib['name'] = node.astext() parent.attrib['name'] = node.astext()
def istoctree(node): matcher = NodeMatcher(addnodes.compact_paragraph, toctree=Any)
# type: (nodes.Node) -> bool for node in tocdoc.traverse(matcher):
return isinstance(node, addnodes.compact_paragraph) and \
'toctree' in node
for node in tocdoc.traverse(istoctree):
write_toc(node, chapters) write_toc(node, chapters)
# Index # Index

View File

@@ -16,6 +16,7 @@ from sphinx.builders.latex.nodes import (
captioned_literal_block, footnotemark, footnotetext, math_reference, thebibliography captioned_literal_block, footnotemark, footnotetext, math_reference, thebibliography
) )
from sphinx.transforms import SphinxTransform from sphinx.transforms import SphinxTransform
from sphinx.util.nodes import NodeMatcher
if False: if False:
# For type annotation # For type annotation
@@ -30,7 +31,7 @@ class FootnoteDocnameUpdater(SphinxTransform):
TARGET_NODES = (nodes.footnote, nodes.footnote_reference) TARGET_NODES = (nodes.footnote, nodes.footnote_reference)
def apply(self): def apply(self):
for node in self.document.traverse(lambda n: isinstance(n, self.TARGET_NODES)): for node in self.document.traverse(NodeMatcher(*self.TARGET_NODES)):
node['docname'] = self.env.docname node['docname'] = self.env.docname
@@ -536,9 +537,9 @@ class CitationReferenceTransform(SphinxTransform):
if self.app.builder.name != 'latex': if self.app.builder.name != 'latex':
return return
matcher = NodeMatcher(addnodes.pending_xref, refdomain='std', reftype='citation')
citations = self.env.get_domain('std').data['citations'] citations = self.env.get_domain('std').data['citations']
for node in self.document.traverse(addnodes.pending_xref): for node in self.document.traverse(matcher):
if node['refdomain'] == 'std' and node['reftype'] == 'citation':
docname, labelid, _ = citations.get(node['reftarget'], ('', '', 0)) docname, labelid, _ = citations.get(node['reftarget'], ('', '', 0))
if docname: if docname:
citation_ref = nodes.citation_reference('', *node.children, citation_ref = nodes.citation_reference('', *node.children,
@@ -577,8 +578,8 @@ class LiteralBlockTransform(SphinxTransform):
if self.app.builder.name != 'latex': if self.app.builder.name != 'latex':
return return
for node in self.document.traverse(nodes.container): matcher = NodeMatcher(nodes.container, literal_block=True)
if node.get('literal_block') is True: for node in self.document.traverse(matcher):
newnode = captioned_literal_block('', *node.children, **node.attributes) newnode = captioned_literal_block('', *node.children, **node.attributes)
node.replace_self(newnode) node.replace_self(newnode)

View File

@@ -336,9 +336,6 @@ class BuildEnvironment(object):
if docname in other.reread_always: if docname in other.reread_always:
self.reread_always.add(docname) self.reread_always.add(docname)
for docname in other.included:
self.included.add(docname)
for version, changes in other.versionchanges.items(): for version, changes in other.versionchanges.items():
self.versionchanges.setdefault(version, []).extend( self.versionchanges.setdefault(version, []).extend(
change for change in changes if change[1] in docnames) change for change in changes if change[1] in docnames)

View File

@@ -342,7 +342,8 @@ class Documenter(object):
explicit_modname, path, base, args, retann = \ explicit_modname, path, base, args, retann = \
py_ext_sig_re.match(self.name).groups() # type: ignore py_ext_sig_re.match(self.name).groups() # type: ignore
except AttributeError: except AttributeError:
logger.warning(__('invalid signature for auto%s (%r)') % (self.objtype, self.name)) logger.warning(__('invalid signature for auto%s (%r)') % (self.objtype, self.name),
type='autodoc')
return False return False
# support explicit module and class name separation via :: # support explicit module and class name separation via ::
@@ -379,7 +380,7 @@ class Documenter(object):
self.module, self.parent, self.object_name, self.object = ret self.module, self.parent, self.object_name, self.object = ret
return True return True
except ImportError as exc: except ImportError as exc:
logger.warning(exc.args[0]) logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread() self.env.note_reread()
return False return False
@@ -442,7 +443,7 @@ class Documenter(object):
args = self.format_args() args = self.format_args()
except Exception as err: except Exception as err:
logger.warning(__('error while formatting arguments for %s: %s') % logger.warning(__('error while formatting arguments for %s: %s') %
(self.fullname, err)) (self.fullname, err), type='autodoc')
args = None args = None
retann = self.retann retann = self.retann
@@ -564,7 +565,7 @@ class Documenter(object):
selected.append((name, members[name].value)) selected.append((name, members[name].value))
else: else:
logger.warning(__('missing attribute %s in object %s') % logger.warning(__('missing attribute %s in object %s') %
(name, self.fullname)) (name, self.fullname), type='autodoc')
return False, sorted(selected) return False, sorted(selected)
elif self.options.inherited_members: elif self.options.inherited_members:
return False, sorted((m.name, m.value) for m in itervalues(members)) return False, sorted((m.name, m.value) for m in itervalues(members))
@@ -653,7 +654,7 @@ class Documenter(object):
except Exception as exc: except Exception as exc:
logger.warning(__('autodoc: failed to determine %r to be documented.' logger.warning(__('autodoc: failed to determine %r to be documented.'
'the following exception was raised:\n%s'), 'the following exception was raised:\n%s'),
member, exc) member, exc, type='autodoc')
keep = False keep = False
if keep: if keep:
@@ -746,7 +747,7 @@ class Documenter(object):
__('don\'t know which module to import for autodocumenting ' __('don\'t know which module to import for autodocumenting '
'%r (try placing a "module" or "currentmodule" directive ' '%r (try placing a "module" or "currentmodule" directive '
'in the document, or giving an explicit module name)') % 'in the document, or giving an explicit module name)') %
self.name) self.name, type='autodoc')
return return
# now, import the module and get object to document # now, import the module and get object to document
@@ -832,7 +833,8 @@ class ModuleDocumenter(Documenter):
def resolve_name(self, modname, parents, path, base): def resolve_name(self, modname, parents, path, base):
# type: (str, Any, str, Any) -> Tuple[str, List[unicode]] # type: (str, Any, str, Any) -> Tuple[str, List[unicode]]
if modname is not None: if modname is not None:
logger.warning(__('"::" in automodule name doesn\'t make sense')) logger.warning(__('"::" in automodule name doesn\'t make sense'),
type='autodoc')
return (path or '') + base, [] return (path or '') + base, []
def parse_name(self): def parse_name(self):
@@ -840,7 +842,8 @@ class ModuleDocumenter(Documenter):
ret = Documenter.parse_name(self) ret = Documenter.parse_name(self)
if self.args or self.retann: if self.args or self.retann:
logger.warning(__('signature arguments or return annotation ' logger.warning(__('signature arguments or return annotation '
'given for automodule %s') % self.fullname) 'given for automodule %s') % self.fullname,
type='autodoc')
return ret return ret
def add_directive_header(self, sig): def add_directive_header(self, sig):
@@ -875,7 +878,9 @@ class ModuleDocumenter(Documenter):
logger.warning( logger.warning(
__('__all__ should be a list of strings, not %r ' __('__all__ should be a list of strings, not %r '
'(in module %s) -- ignoring __all__') % '(in module %s) -- ignoring __all__') %
(memberlist, self.fullname)) (memberlist, self.fullname),
type='autodoc'
)
# fall back to all members # fall back to all members
return True, safe_getmembers(self.object) return True, safe_getmembers(self.object)
else: else:
@@ -888,7 +893,9 @@ class ModuleDocumenter(Documenter):
logger.warning( logger.warning(
__('missing attribute mentioned in :members: or __all__: ' __('missing attribute mentioned in :members: or __all__: '
'module %s, attribute %s') % 'module %s, attribute %s') %
(safe_getattr(self.object, '__name__', '???'), mname)) (safe_getattr(self.object, '__name__', '???'), mname),
type='autodoc'
)
return False, ret return False, ret
@@ -1539,7 +1546,8 @@ def merge_autodoc_default_flags(app, config):
return return
logger.warning(__('autodoc_default_flags is now deprecated. ' logger.warning(__('autodoc_default_flags is now deprecated. '
'Please use autodoc_default_options instead.')) 'Please use autodoc_default_options instead.'),
type='autodoc')
for option in config.autodoc_default_flags: for option in config.autodoc_default_flags:
if isinstance(option, string_types): if isinstance(option, string_types):
@@ -1547,7 +1555,7 @@ def merge_autodoc_default_flags(app, config):
else: else:
logger.warning( logger.warning(
__("Ignoring invalid option in autodoc_default_flags: %r"), __("Ignoring invalid option in autodoc_default_flags: %r"),
option option, type='autodoc'
) )

View File

@@ -10,6 +10,7 @@
""" """
from os import path from os import path
from typing import Any
from docutils import nodes from docutils import nodes
from docutils.io import StringInput from docutils.io import StringInput
@@ -22,14 +23,14 @@ from sphinx.transforms import SphinxTransform
from sphinx.util import split_index_msg, logging from sphinx.util import split_index_msg, logging
from sphinx.util.i18n import find_catalog from sphinx.util.i18n import find_catalog
from sphinx.util.nodes import ( from sphinx.util.nodes import (
LITERAL_TYPE_NODES, IMAGE_TYPE_NODES, LITERAL_TYPE_NODES, IMAGE_TYPE_NODES, NodeMatcher,
extract_messages, is_pending_meta, traverse_translatable_index, extract_messages, is_pending_meta, traverse_translatable_index,
) )
from sphinx.util.pycompat import indent from sphinx.util.pycompat import indent
if False: if False:
# For type annotation # For type annotation
from typing import Any, Dict, List, Tuple # NOQA from typing import Dict, List, Tuple # NOQA
from sphinx.application import Sphinx # NOQA from sphinx.application import Sphinx # NOQA
from sphinx.config import Config # NOQA from sphinx.config import Config # NOQA
@@ -183,11 +184,8 @@ class Locale(SphinxTransform):
self.document.note_implicit_target(section_node) self.document.note_implicit_target(section_node)
# replace target's refname to new target name # replace target's refname to new target name
def is_named_target(node): matcher = NodeMatcher(nodes.target, refname=old_name)
# type: (nodes.Node) -> bool for old_target in self.document.traverse(matcher):
return isinstance(node, nodes.target) and \
node.get('refname') == old_name
for old_target in self.document.traverse(is_named_target):
old_target['refname'] = new_name old_target['refname'] = new_name
processed = True processed = True
@@ -276,16 +274,14 @@ class Locale(SphinxTransform):
continue # skip continue # skip
# auto-numbered foot note reference should use original 'ids'. # auto-numbered foot note reference should use original 'ids'.
def is_autofootnote_ref(node):
# type: (nodes.Node) -> bool
return isinstance(node, nodes.footnote_reference) and node.get('auto')
def list_replace_or_append(lst, old, new): def list_replace_or_append(lst, old, new):
# type: (List, Any, Any) -> None # type: (List, Any, Any) -> None
if old in lst: if old in lst:
lst[lst.index(old)] = new lst[lst.index(old)] = new
else: else:
lst.append(new) lst.append(new)
is_autofootnote_ref = NodeMatcher(nodes.footnote_reference, auto=Any)
old_foot_refs = node.traverse(is_autofootnote_ref) old_foot_refs = node.traverse(is_autofootnote_ref)
new_foot_refs = patch.traverse(is_autofootnote_ref) new_foot_refs = patch.traverse(is_autofootnote_ref)
if len(old_foot_refs) != len(new_foot_refs): if len(old_foot_refs) != len(new_foot_refs):
@@ -328,10 +324,7 @@ class Locale(SphinxTransform):
# * reference target ".. _Python: ..." is not translatable. # * reference target ".. _Python: ..." is not translatable.
# * use translated refname for section refname. # * use translated refname for section refname.
# * inline reference "`Python <...>`_" has no 'refname'. # * inline reference "`Python <...>`_" has no 'refname'.
def is_refnamed_ref(node): is_refnamed_ref = NodeMatcher(nodes.reference, refname=Any)
# type: (nodes.Node) -> bool
return isinstance(node, nodes.reference) and \
'refname' in node
old_refs = node.traverse(is_refnamed_ref) old_refs = node.traverse(is_refnamed_ref)
new_refs = patch.traverse(is_refnamed_ref) new_refs = patch.traverse(is_refnamed_ref)
if len(old_refs) != len(new_refs): if len(old_refs) != len(new_refs):
@@ -358,10 +351,7 @@ class Locale(SphinxTransform):
self.document.note_refname(new) self.document.note_refname(new)
# refnamed footnote should use original 'ids'. # refnamed footnote should use original 'ids'.
def is_refnamed_footnote_ref(node): is_refnamed_footnote_ref = NodeMatcher(nodes.footnote_reference, refname=Any)
# type: (nodes.Node) -> bool
return isinstance(node, nodes.footnote_reference) and \
'refname' in node
old_foot_refs = node.traverse(is_refnamed_footnote_ref) old_foot_refs = node.traverse(is_refnamed_footnote_ref)
new_foot_refs = patch.traverse(is_refnamed_footnote_ref) new_foot_refs = patch.traverse(is_refnamed_footnote_ref)
refname_ids_map = {} refname_ids_map = {}
@@ -380,10 +370,7 @@ class Locale(SphinxTransform):
new["ids"] = refname_ids_map[refname] new["ids"] = refname_ids_map[refname]
# citation should use original 'ids'. # citation should use original 'ids'.
def is_citation_ref(node): is_citation_ref = NodeMatcher(nodes.citation_reference, refname=Any)
# type: (nodes.Node) -> bool
return isinstance(node, nodes.citation_reference) and \
'refname' in node
old_cite_refs = node.traverse(is_citation_ref) old_cite_refs = node.traverse(is_citation_ref)
new_cite_refs = patch.traverse(is_citation_ref) new_cite_refs = patch.traverse(is_citation_ref)
refname_ids_map = {} refname_ids_map = {}
@@ -474,10 +461,7 @@ class Locale(SphinxTransform):
node['entries'] = new_entries node['entries'] = new_entries
# remove translated attribute that is used for avoiding double translation. # remove translated attribute that is used for avoiding double translation.
def has_translatable(node): for node in self.document.traverse(NodeMatcher(translated=Any)):
# type: (nodes.Node) -> bool
return isinstance(node, nodes.Element) and 'translated' in node
for node in self.document.traverse(has_translatable):
node.delattr('translated') node.delattr('translated')
@@ -492,7 +476,8 @@ class RemoveTranslatableInline(SphinxTransform):
from sphinx.builders.gettext import MessageCatalogBuilder from sphinx.builders.gettext import MessageCatalogBuilder
if isinstance(self.app.builder, MessageCatalogBuilder): if isinstance(self.app.builder, MessageCatalogBuilder):
return return
for inline in self.document.traverse(nodes.inline):
if 'translatable' in inline: matcher = NodeMatcher(nodes.inline, translatable=Any)
for inline in self.document.traverse(matcher):
inline.parent.remove(inline) inline.parent.remove(inline)
inline.parent += inline.children inline.parent += inline.children

View File

@@ -11,6 +11,7 @@
from __future__ import absolute_import from __future__ import absolute_import
import re import re
from typing import Any
from docutils import nodes from docutils import nodes
from six import text_type from six import text_type
@@ -33,6 +34,57 @@ explicit_title_re = re.compile(r'^(.+?)\s*(?<!\x00)<(.*?)>$', re.DOTALL)
caption_ref_re = explicit_title_re # b/w compat alias caption_ref_re = explicit_title_re # b/w compat alias
class NodeMatcher(object):
"""A helper class for Node.traverse().
It checks that given node is an instance of specified node-classes and it has
specified node-attributes.
For example, following example searches ``reference`` node having ``refdomain``
and ``reftype`` attributes::
matcher = NodeMatcher(nodes.reference, refdomain='std', reftype='citation')
doctree.traverse(matcher)
# => [<reference ...>, <reference ...>, ...]
A special value ``typing.Any`` matches any kind of node-attributes. For example,
following example searches ``reference`` node having ``refdomain`` attributes::
from typing import Any
matcher = NodeMatcher(nodes.reference, refdomain=Any)
doctree.traverse(matcher)
# => [<reference ...>, <reference ...>, ...]
"""
def __init__(self, *classes, **attrs):
# type: (nodes.Node, Any) -> None
self.classes = classes
self.attrs = attrs
def match(self, node):
# type: (nodes.Node) -> bool
try:
if self.classes and not isinstance(node, self.classes):
return False
for key, value in self.attrs.items():
if key not in node:
return False
elif value is Any:
continue
elif node.get(key) != value:
return False
else:
return True
except Exception:
# for non-Element nodes
return False
def __call__(self, node):
# type: (nodes.Node) -> bool
return self.match(node)
def get_full_module_name(node): def get_full_module_name(node):
# type: (nodes.Node) -> str # type: (nodes.Node) -> str
""" """
@@ -241,11 +293,7 @@ def traverse_parent(node, cls=None):
def traverse_translatable_index(doctree): def traverse_translatable_index(doctree):
# type: (nodes.Node) -> Iterable[Tuple[nodes.Node, List[unicode]]] # type: (nodes.Node) -> Iterable[Tuple[nodes.Node, List[unicode]]]
"""Traverse translatable index node from a document tree.""" """Traverse translatable index node from a document tree."""
def is_block_index(node): for node in doctree.traverse(NodeMatcher(addnodes.index, inline=False)):
# type: (nodes.Node) -> bool
return isinstance(node, addnodes.index) and \
node.get('inline') is False
for node in doctree.traverse(is_block_index):
if 'raw_entries' in node: if 'raw_entries' in node:
entries = node['raw_entries'] entries = node['raw_entries']
else: else:

View File

@@ -21,6 +21,7 @@ from sphinx import addnodes
from sphinx.locale import admonitionlabels, _ from sphinx.locale import admonitionlabels, _
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.i18n import format_date from sphinx.util.i18n import format_date
from sphinx.util.nodes import NodeMatcher
if False: if False:
# For type annotation # For type annotation
@@ -63,16 +64,13 @@ class NestedInlineTransform(object):
def apply(self): def apply(self):
# type: () -> None # type: () -> None
def is_inline(node): matcher = NodeMatcher(nodes.literal, nodes.emphasis, nodes.strong)
# type: (nodes.Node) -> bool for node in self.document.traverse(matcher):
return isinstance(node, (nodes.literal, nodes.emphasis, nodes.strong)) if any(matcher(subnode) for subnode in node):
for node in self.document.traverse(is_inline):
if any(is_inline(subnode) for subnode in node):
pos = node.parent.index(node) pos = node.parent.index(node)
for subnode in reversed(node[1:]): for subnode in reversed(node[1:]):
node.remove(subnode) node.remove(subnode)
if is_inline(subnode): if matcher(subnode):
node.parent.insert(pos + 1, subnode) node.parent.insert(pos + 1, subnode)
else: else:
newnode = node.__class__('', subnode, **node.attributes) newnode = node.__class__('', subnode, **node.attributes)

View File

@@ -9,6 +9,7 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from textwrap import dedent from textwrap import dedent
from typing import Any
import pytest import pytest
from docutils import frontend from docutils import frontend
@@ -17,7 +18,7 @@ from docutils.parsers import rst
from docutils.utils import new_document from docutils.utils import new_document
from sphinx.transforms import ApplySourceWorkaround from sphinx.transforms import ApplySourceWorkaround
from sphinx.util.nodes import extract_messages, clean_astext from sphinx.util.nodes import NodeMatcher, extract_messages, clean_astext
def _transform(doctree): def _transform(doctree):
@@ -50,6 +51,38 @@ def assert_node_count(messages, node_type, expect_count):
% (node_type, node_list, count, expect_count)) % (node_type, node_list, count, expect_count))
def test_NodeMatcher():
doctree = nodes.document(None, None)
doctree += nodes.paragraph('', 'Hello')
doctree += nodes.paragraph('', 'Sphinx', block=1)
doctree += nodes.paragraph('', 'World', block=2)
doctree += nodes.literal_block('', 'blah blah blah', block=3)
# search by node class
matcher = NodeMatcher(nodes.paragraph)
assert len(doctree.traverse(matcher)) == 3
# search by multiple node classes
matcher = NodeMatcher(nodes.paragraph, nodes.literal_block)
assert len(doctree.traverse(matcher)) == 4
# search by node attribute
matcher = NodeMatcher(block=1)
assert len(doctree.traverse(matcher)) == 1
# search by node attribute (Any)
matcher = NodeMatcher(block=Any)
assert len(doctree.traverse(matcher)) == 3
# search by both class and attribute
matcher = NodeMatcher(nodes.paragraph, block=Any)
assert len(doctree.traverse(matcher)) == 2
# mismatched
matcher = NodeMatcher(nodes.title)
assert len(doctree.traverse(matcher)) == 0
@pytest.mark.parametrize( @pytest.mark.parametrize(
'rst,node_cls,count', 'rst,node_cls,count',
[ [