diff options
Diffstat (limited to 'sphinx/util/docutils.py')
-rw-r--r-- | sphinx/util/docutils.py | 121 |
1 files changed, 105 insertions, 16 deletions
diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index 38f275824..b6262d793 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -10,19 +10,22 @@ """ from __future__ import absolute_import +import os import re import types +import warnings from contextlib import contextmanager from copy import copy from distutils.version import LooseVersion +from os import path import docutils from docutils import nodes -from docutils.languages import get_language -from docutils.parsers.rst import directives, roles, convert_directive_function +from docutils.parsers.rst import Directive, directives, roles, convert_directive_function from docutils.statemachine import StateMachine from docutils.utils import Reporter +from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.errors import ExtensionError from sphinx.locale import __ from sphinx.util import logging @@ -32,18 +35,20 @@ report_re = re.compile('^(.+?:(?:\\d+)?): \\((DEBUG|INFO|WARNING|ERROR|SEVERE)/( if False: # For type annotation - from typing import Any, Callable, Generator, Iterator, List, Tuple # NOQA + from typing import Any, Callable, Generator, List, Set, Tuple # NOQA from docutils.statemachine import State, ViewList # NOQA + from sphinx.config import Config # NOQA from sphinx.environment import BuildEnvironment # NOQA from sphinx.io import SphinxFileInput # NOQA __version_info__ = tuple(LooseVersion(docutils.__version__).version) +additional_nodes = set() # type: Set[nodes.Node] @contextmanager def docutils_namespace(): - # type: () -> Iterator[None] + # type: () -> Generator[None, None, None] """Create namespace for reST parsers.""" try: _directives = copy(directives._directives) @@ -54,30 +59,89 @@ def docutils_namespace(): directives._directives = _directives roles._roles = _roles + for node in list(additional_nodes): + unregister_node(node) + additional_nodes.discard(node) -def patched_get_language(language_code, reporter=None): - # type: (unicode, Reporter) -> Any - """A wrapper for docutils.languages.get_language(). + +def is_node_registered(node): + # type: (nodes.Node) -> bool + """Check the *node* is already registered.""" + return hasattr(nodes.GenericNodeVisitor, 'visit_' + node.__name__) + + +def register_node(node): + # type: (nodes.Node) -> None + """Register a node to docutils. + + This modifies global state of some visitors. So it is better to use this + inside ``docutils_namespace()`` to prevent side-effects. + """ + if not hasattr(nodes.GenericNodeVisitor, 'visit_' + node.__name__): + nodes._add_node_class_names([node.__name__]) + additional_nodes.add(node) + + +def unregister_node(node): + # type: (nodes.Node) -> None + """Unregister a node from docutils. + + This is inverse of ``nodes._add_nodes_class_names()``. + """ + if hasattr(nodes.GenericNodeVisitor, 'visit_' + node.__name__): + delattr(nodes.GenericNodeVisitor, "visit_" + node.__name__) + delattr(nodes.GenericNodeVisitor, "depart_" + node.__name__) + delattr(nodes.SparseNodeVisitor, 'visit_' + node.__name__) + delattr(nodes.SparseNodeVisitor, 'depart_' + node.__name__) + + +@contextmanager +def patched_get_language(): + # type: () -> Generator[None, None, None] + """Patch docutils.languages.get_language() temporarily. This ignores the second argument ``reporter`` to suppress warnings. refs: https://github.com/sphinx-doc/sphinx/issues/3788 """ - return get_language(language_code) + from docutils.languages import get_language + def patched_get_language(language_code, reporter=None): + # type: (unicode, Reporter) -> Any + return get_language(language_code) -@contextmanager -def patch_docutils(): - # type: () -> Iterator[None] - """Patch to docutils temporarily.""" try: docutils.languages.get_language = patched_get_language - yield finally: # restore original implementations docutils.languages.get_language = get_language +@contextmanager +def using_user_docutils_conf(confdir): + # type: (unicode) -> Generator[None, None, None] + """Let docutils know the location of ``docutils.conf`` for Sphinx.""" + try: + docutilsconfig = os.environ.get('DOCUTILSCONFIG', None) + if confdir: + os.environ['DOCUTILSCONFIG'] = path.join(path.abspath(confdir), 'docutils.conf') # type: ignore # NOQA + + yield + finally: + if docutilsconfig is None: + os.environ.pop('DOCUTILSCONFIG') + else: + os.environ['DOCUTILSCONFIG'] = docutilsconfig + + +@contextmanager +def patch_docutils(confdir=None): + # type: (unicode) -> Generator[None, None, None] + """Patch to docutils temporarily.""" + with patched_get_language(), using_user_docutils_conf(confdir): + yield + + class ElementLookupError(Exception): pass @@ -177,8 +241,9 @@ class LoggingReporter(Reporter): return cls(reporter.source, reporter.report_level, reporter.halt_level, reporter.debug_flag, reporter.error_handler) - def __init__(self, source, report_level, halt_level, - debug=False, error_handler='backslashreplace'): + def __init__(self, source, report_level=Reporter.WARNING_LEVEL, + halt_level=Reporter.SEVERE_LEVEL, debug=False, + error_handler='backslashreplace'): # type: (unicode, int, int, bool, unicode) -> None stream = WarningStream() Reporter.__init__(self, source, report_level, halt_level, @@ -200,6 +265,10 @@ def is_html5_writer_available(): def directive_helper(obj, has_content=None, argument_spec=None, **option_spec): # type: (Any, bool, Tuple[int, int, bool], Any) -> Any + warnings.warn('function based directive support is now deprecated. ' + 'Use class based directive instead.', + RemovedInSphinx30Warning) + if isinstance(obj, (types.FunctionType, types.MethodType)): obj.content = has_content # type: ignore obj.arguments = argument_spec or (0, 0, False) # type: ignore @@ -214,7 +283,7 @@ def directive_helper(obj, has_content=None, argument_spec=None, **option_spec): @contextmanager def switch_source_input(state, content): - # type: (State, ViewList) -> Generator + # type: (State, ViewList) -> Generator[None, None, None] """Switch current source input of state temporarily.""" try: # remember the original ``get_source_and_line()`` method @@ -231,6 +300,26 @@ def switch_source_input(state, content): state.memo.reporter.get_source_and_line = get_source_and_line +class SphinxDirective(Directive): + """A base class for Directives. + + Compared with ``docutils.parsers.rst.Directive``, this class improves + accessibility to Sphinx APIs. + """ + + @property + def env(self): + # type: () -> BuildEnvironment + """Reference to the :class:`.BuildEnvironment` object.""" + return self.state.document.settings.env + + @property + def config(self): + # type: () -> Config + """Reference to the :class:`.Config` object.""" + return self.env.config + + # cache a vanilla instance of nodes.document # Used in new_document() function __document_cache__ = None # type: nodes.document |