diff options
Diffstat (limited to 'sphinx/util/nodes.py')
-rw-r--r-- | sphinx/util/nodes.py | 113 |
1 files changed, 82 insertions, 31 deletions
diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 03e06c416..ddf78185e 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ sphinx.util.nodes ~~~~~~~~~~~~~~~~~ @@ -8,12 +7,11 @@ :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from __future__ import absolute_import import re +from typing import Any, cast from docutils import nodes -from six import text_type from sphinx import addnodes from sphinx.locale import __ @@ -21,7 +19,9 @@ from sphinx.util import logging if False: # For type annotation - from typing import Any, Callable, Iterable, List, Set, Tuple, Optional # NOQA + from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Type # NOQA + from docutils.parsers.rst.states import Inliner # NOQA + from docutils.statemachine import StringList # NOQA from sphinx.builders import Builder # NOQA from sphinx.utils.tags import Tags # NOQA @@ -33,6 +33,61 @@ explicit_title_re = re.compile(r'^(.+?)\s*(?<!\x00)<(.*?)>$', re.DOTALL) caption_ref_re = explicit_title_re # b/w compat alias +class NodeMatcher: + """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: (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 + + if self.attrs: + if not isinstance(node, nodes.Element): + 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 + + 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): # type: (nodes.Node) -> str """ @@ -45,7 +100,7 @@ def get_full_module_name(node): def repr_domxml(node, length=80): - # type: (nodes.Node, Optional[int]) -> unicode + # type: (nodes.Node, Optional[int]) -> str """ return DOM XML representation of the specified node like: '<paragraph translatable="False"><inline classes="versionmodified">New in version...' @@ -59,14 +114,14 @@ def repr_domxml(node, length=80): try: text = node.asdom().toxml() except Exception: - text = text_type(node) + text = str(node) if length and len(text) > length: text = text[:length] + '...' return text def apply_source_workaround(node): - # type: (nodes.Node) -> None + # type: (nodes.Element) -> None # workaround: nodes.term have wrong rawsource if classifier is specified. # The behavior of docutils-0.11, 0.12 is: # * when ``term text : classifier1 : classifier2`` is specified, @@ -195,9 +250,9 @@ META_TYPE_NODES = ( def extract_messages(doctree): - # type: (nodes.Node) -> Iterable[Tuple[nodes.Node, unicode]] + # type: (nodes.Element) -> Iterable[Tuple[nodes.Element, str]] """Extract translatable messages from a document tree.""" - for node in doctree.traverse(is_translatable): + for node in doctree.traverse(is_translatable): # type: nodes.Element if isinstance(node, addnodes.translatable): for msg in node.extract_original_messages(): yield node, msg @@ -212,7 +267,7 @@ def extract_messages(doctree): msg += '\n :alt: %s' % node['alt'] elif isinstance(node, META_TYPE_NODES): msg = node.rawcontent - elif is_pending_meta(node): + elif isinstance(node, nodes.pending) and is_pending_meta(node): msg = node.details['nodes'][0].rawcontent else: msg = node.rawsource.replace('\n', ' ').strip() @@ -223,7 +278,7 @@ def extract_messages(doctree): def find_source_node(node): - # type: (nodes.Node) -> unicode + # type: (nodes.Element) -> str for pnode in traverse_parent(node): if pnode.source: return pnode.source @@ -231,7 +286,7 @@ def find_source_node(node): def traverse_parent(node, cls=None): - # type: (nodes.Node, Any) -> Iterable[nodes.Node] + # type: (nodes.Element, Any) -> Iterable[nodes.Element] while node: if cls is None or isinstance(node, cls): yield node @@ -239,13 +294,9 @@ def traverse_parent(node, cls=None): def traverse_translatable_index(doctree): - # type: (nodes.Node) -> Iterable[Tuple[nodes.Node, List[unicode]]] + # type: (nodes.Element) -> Iterable[Tuple[nodes.Element, List[str]]] """Traverse translatable index node from a document tree.""" - def is_block_index(node): - # type: (nodes.Node) -> bool - return isinstance(node, addnodes.index) and \ - node.get('inline') is False - for node in doctree.traverse(is_block_index): + for node in doctree.traverse(NodeMatcher(addnodes.index, inline=False)): # type: addnodes.index # NOQA if 'raw_entries' in node: entries = node['raw_entries'] else: @@ -254,7 +305,7 @@ def traverse_translatable_index(doctree): def nested_parse_with_titles(state, content, node): - # type: (Any, List[unicode], nodes.Node) -> unicode + # type: (Any, StringList, nodes.Node) -> str """Version of state.nested_parse() that allows titles and does not require titles to have the same decoration as the calling document. @@ -274,7 +325,7 @@ def nested_parse_with_titles(state, content, node): def clean_astext(node): - # type: (nodes.Node) -> unicode + # type: (nodes.Element) -> str """Like node.astext(), but ignore images.""" node = node.deepcopy() for img in node.traverse(nodes.image): @@ -285,7 +336,7 @@ def clean_astext(node): def split_explicit_title(text): - # type: (unicode) -> Tuple[bool, unicode, unicode] + # type: (str) -> Tuple[bool, str, str] """Split role content into title and target, if given.""" match = explicit_title_re.match(text) if match: @@ -299,10 +350,10 @@ indextypes = [ def process_index_entry(entry, targetid): - # type: (unicode, unicode) -> List[Tuple[unicode, unicode, unicode, unicode, unicode]] + # type: (str, str) -> List[Tuple[str, str, str, str, str]] from sphinx.domains.python import pairindextypes - indexentries = [] # type: List[Tuple[unicode, unicode, unicode, unicode, unicode]] + indexentries = [] # type: List[Tuple[str, str, str, str, str]] entry = entry.strip() oentry = entry main = '' @@ -338,15 +389,15 @@ def process_index_entry(entry, targetid): def inline_all_toctrees(builder, docnameset, docname, tree, colorfunc, traversed): - # type: (Builder, Set[unicode], unicode, nodes.Node, Callable, nodes.Node) -> nodes.Node + # type: (Builder, Set[str], str, nodes.document, Callable, List[str]) -> nodes.document """Inline all toctrees in the *tree*. Record all docnames in *docnameset*, and output docnames with *colorfunc*. """ - tree = tree.deepcopy() + tree = cast(nodes.document, tree.deepcopy()) for toctreenode in tree.traverse(addnodes.toctree): newnodes = [] - includefiles = map(text_type, toctreenode['includefiles']) + includefiles = map(str, toctreenode['includefiles']) for includefile in includefiles: if includefile not in traversed: try: @@ -371,7 +422,7 @@ def inline_all_toctrees(builder, docnameset, docname, tree, colorfunc, traversed def make_refnode(builder, fromdocname, todocname, targetid, child, title=None): - # type: (Builder, unicode, unicode, unicode, nodes.Node, unicode) -> nodes.reference + # type: (Builder, str, str, str, nodes.Node, str) -> nodes.reference """Shortcut to create a reference node.""" node = nodes.reference('', '', internal=True) if fromdocname == todocname and targetid: @@ -395,8 +446,8 @@ def set_source_info(directive, node): def set_role_source_info(inliner, lineno, node): - # type: (Any, unicode, nodes.Node) -> None - node.source, node.line = inliner.reporter.get_source_and_line(lineno) + # type: (Inliner, int, nodes.Node) -> None + node.source, node.line = inliner.reporter.get_source_and_line(lineno) # type: ignore NON_SMARTQUOTABLE_PARENT_NODES = ( @@ -447,7 +498,7 @@ def process_only_nodes(document, tags): # monkey-patch Element.copy to copy the rawsource and line def _new_copy(self): - # type: (nodes.Node) -> nodes.Node + # type: (nodes.Element) -> nodes.Element newnode = self.__class__(self.rawsource, **self.attributes) if isinstance(self, nodes.Element): newnode.source = self.source @@ -455,4 +506,4 @@ def _new_copy(self): return newnode -nodes.Element.copy = _new_copy +nodes.Element.copy = _new_copy # type: ignore |