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