summaryrefslogtreecommitdiff
path: root/sphinx/util/nodes.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/util/nodes.py')
-rw-r--r--sphinx/util/nodes.py113
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