summaryrefslogtreecommitdiffstats
path: root/sphinx/application.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/application.py')
-rw-r--r--sphinx/application.py1371
1 files changed, 1371 insertions, 0 deletions
diff --git a/sphinx/application.py b/sphinx/application.py
new file mode 100644
index 0000000..d5fbaa9
--- /dev/null
+++ b/sphinx/application.py
@@ -0,0 +1,1371 @@
+"""Sphinx application class and extensibility interface.
+
+Gracefully adapted from the TextPress system by Armin.
+"""
+
+from __future__ import annotations
+
+import contextlib
+import os
+import pickle
+import sys
+from collections import deque
+from collections.abc import Sequence # NoQA: TCH003
+from io import StringIO
+from os import path
+from typing import IO, TYPE_CHECKING, Any, Callable
+
+from docutils.nodes import TextElement # NoQA: TCH002
+from docutils.parsers.rst import Directive, roles
+from docutils.transforms import Transform # NoQA: TCH002
+from pygments.lexer import Lexer # NoQA: TCH002
+
+import sphinx
+from sphinx import locale, package_dir
+from sphinx.config import Config
+from sphinx.environment import BuildEnvironment
+from sphinx.errors import ApplicationError, ConfigError, VersionRequirementError
+from sphinx.events import EventManager
+from sphinx.highlighting import lexer_classes
+from sphinx.locale import __
+from sphinx.project import Project
+from sphinx.registry import SphinxComponentRegistry
+from sphinx.util import docutils, logging
+from sphinx.util._pathlib import _StrPath
+from sphinx.util.build_phase import BuildPhase
+from sphinx.util.console import bold # type: ignore[attr-defined]
+from sphinx.util.display import progress_message
+from sphinx.util.i18n import CatalogRepository
+from sphinx.util.logging import prefixed_warnings
+from sphinx.util.osutil import ensuredir, relpath
+from sphinx.util.tags import Tags
+
+if TYPE_CHECKING:
+ from docutils import nodes
+ from docutils.nodes import Element
+ from docutils.parsers import Parser
+
+ from sphinx.builders import Builder
+ from sphinx.domains import Domain, Index
+ from sphinx.environment.collectors import EnvironmentCollector
+ from sphinx.extension import Extension
+ from sphinx.roles import XRefRole
+ from sphinx.theming import Theme
+ from sphinx.util.typing import RoleFunction, TitleGetter
+
+
+builtin_extensions: tuple[str, ...] = (
+ 'sphinx.addnodes',
+ 'sphinx.builders.changes',
+ 'sphinx.builders.epub3',
+ 'sphinx.builders.dirhtml',
+ 'sphinx.builders.dummy',
+ 'sphinx.builders.gettext',
+ 'sphinx.builders.html',
+ 'sphinx.builders.latex',
+ 'sphinx.builders.linkcheck',
+ 'sphinx.builders.manpage',
+ 'sphinx.builders.singlehtml',
+ 'sphinx.builders.texinfo',
+ 'sphinx.builders.text',
+ 'sphinx.builders.xml',
+ 'sphinx.config',
+ 'sphinx.domains.c',
+ 'sphinx.domains.changeset',
+ 'sphinx.domains.citation',
+ 'sphinx.domains.cpp',
+ 'sphinx.domains.index',
+ 'sphinx.domains.javascript',
+ 'sphinx.domains.math',
+ 'sphinx.domains.python',
+ 'sphinx.domains.rst',
+ 'sphinx.domains.std',
+ 'sphinx.directives',
+ 'sphinx.directives.code',
+ 'sphinx.directives.other',
+ 'sphinx.directives.patches',
+ 'sphinx.extension',
+ 'sphinx.parsers',
+ 'sphinx.registry',
+ 'sphinx.roles',
+ 'sphinx.transforms',
+ 'sphinx.transforms.compact_bullet_list',
+ 'sphinx.transforms.i18n',
+ 'sphinx.transforms.references',
+ 'sphinx.transforms.post_transforms',
+ 'sphinx.transforms.post_transforms.code',
+ 'sphinx.transforms.post_transforms.images',
+ 'sphinx.versioning',
+ # collectors should be loaded by specific order
+ 'sphinx.environment.collectors.dependencies',
+ 'sphinx.environment.collectors.asset',
+ 'sphinx.environment.collectors.metadata',
+ 'sphinx.environment.collectors.title',
+ 'sphinx.environment.collectors.toctree',
+)
+_first_party_extensions = (
+ # 1st party extensions
+ 'sphinxcontrib.applehelp',
+ 'sphinxcontrib.devhelp',
+ 'sphinxcontrib.htmlhelp',
+ 'sphinxcontrib.serializinghtml',
+ 'sphinxcontrib.qthelp',
+)
+_first_party_themes = (
+ # Alabaster is loaded automatically to be used as the default theme
+ 'alabaster',
+)
+builtin_extensions += _first_party_themes
+builtin_extensions += _first_party_extensions
+
+ENV_PICKLE_FILENAME = 'environment.pickle'
+
+logger = logging.getLogger(__name__)
+
+
+class Sphinx:
+ """The main application class and extensibility interface.
+
+ :ivar srcdir: Directory containing source.
+ :ivar confdir: Directory containing ``conf.py``.
+ :ivar doctreedir: Directory for storing pickled doctrees.
+ :ivar outdir: Directory for storing build documents.
+ """
+
+ warningiserror: bool
+ _warncount: int
+
+ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[str] | None,
+ outdir: str | os.PathLike[str], doctreedir: str | os.PathLike[str],
+ buildername: str, confoverrides: dict | None = None,
+ status: IO | None = sys.stdout, warning: IO | None = sys.stderr,
+ freshenv: bool = False, warningiserror: bool = False,
+ tags: list[str] | None = None,
+ verbosity: int = 0, parallel: int = 0, keep_going: bool = False,
+ pdb: bool = False) -> None:
+ self.phase = BuildPhase.INITIALIZATION
+ self.verbosity = verbosity
+ self.extensions: dict[str, Extension] = {}
+ self.registry = SphinxComponentRegistry()
+
+ # validate provided directories
+ self.srcdir = _StrPath(srcdir).resolve()
+ self.outdir = _StrPath(outdir).resolve()
+ self.doctreedir = _StrPath(doctreedir).resolve()
+
+ if not path.isdir(self.srcdir):
+ raise ApplicationError(__('Cannot find source directory (%s)') %
+ self.srcdir)
+
+ if path.exists(self.outdir) and not path.isdir(self.outdir):
+ raise ApplicationError(__('Output directory (%s) is not a directory') %
+ self.outdir)
+
+ if self.srcdir == self.outdir:
+ raise ApplicationError(__('Source directory and destination '
+ 'directory cannot be identical'))
+
+ self.parallel = parallel
+
+ if status is None:
+ self._status: IO = StringIO()
+ self.quiet: bool = True
+ else:
+ self._status = status
+ self.quiet = False
+
+ if warning is None:
+ self._warning: IO = StringIO()
+ else:
+ self._warning = warning
+ self._warncount = 0
+ self.keep_going = warningiserror and keep_going
+ if self.keep_going:
+ self.warningiserror = False
+ else:
+ self.warningiserror = warningiserror
+ self.pdb = pdb
+ logging.setup(self, self._status, self._warning)
+
+ self.events = EventManager(self)
+
+ # keep last few messages for traceback
+ # This will be filled by sphinx.util.logging.LastMessagesWriter
+ self.messagelog: deque = deque(maxlen=10)
+
+ # say hello to the world
+ logger.info(bold(__('Running Sphinx v%s') % sphinx.__display_version__))
+
+ # status code for command-line application
+ self.statuscode = 0
+
+ # read config
+ self.tags = Tags(tags)
+ if confdir is None:
+ # set confdir to srcdir if -C given (!= no confdir); a few pieces
+ # of code expect a confdir to be set
+ self.confdir = self.srcdir
+ self.config = Config({}, confoverrides or {})
+ else:
+ self.confdir = _StrPath(confdir).resolve()
+ self.config = Config.read(self.confdir, confoverrides or {}, self.tags)
+
+ # initialize some limited config variables before initialize i18n and loading
+ # extensions
+ self.config.pre_init_values()
+
+ # set up translation infrastructure
+ self._init_i18n()
+
+ # check the Sphinx version if requested
+ if self.config.needs_sphinx and self.config.needs_sphinx > sphinx.__display_version__:
+ raise VersionRequirementError(
+ __('This project needs at least Sphinx v%s and therefore cannot '
+ 'be built with this version.') % self.config.needs_sphinx)
+
+ # load all built-in extension modules, first-party extension modules,
+ # and first-party themes
+ for extension in builtin_extensions:
+ self.setup_extension(extension)
+
+ # load all user-given extension modules
+ for extension in self.config.extensions:
+ self.setup_extension(extension)
+
+ # preload builder module (before init config values)
+ self.preload_builder(buildername)
+
+ if not path.isdir(outdir):
+ with progress_message(__('making output directory')):
+ ensuredir(outdir)
+
+ # the config file itself can be an extension
+ if self.config.setup:
+ prefix = __('while setting up extension %s:') % "conf.py"
+ with prefixed_warnings(prefix):
+ if callable(self.config.setup):
+ self.config.setup(self)
+ else:
+ raise ConfigError(
+ __("'setup' as currently defined in conf.py isn't a Python callable. "
+ "Please modify its definition to make it a callable function. "
+ "This is needed for conf.py to behave as a Sphinx extension."),
+ )
+
+ # now that we know all config values, collect them from conf.py
+ self.config.init_values()
+ self.events.emit('config-inited', self.config)
+
+ # create the project
+ self.project = Project(self.srcdir, self.config.source_suffix)
+
+ # set up the build environment
+ self.env = self._init_env(freshenv)
+
+ # create the builder
+ self.builder = self.create_builder(buildername)
+
+ # build environment post-initialisation, after creating the builder
+ self._post_init_env()
+
+ # set up the builder
+ self._init_builder()
+
+ def _init_i18n(self) -> None:
+ """Load translated strings from the configured localedirs if enabled in
+ the configuration.
+ """
+ if self.config.language == 'en':
+ self.translator, _ = locale.init([], None)
+ else:
+ logger.info(bold(__('loading translations [%s]... ') % self.config.language),
+ nonl=True)
+
+ # compile mo files if sphinx.po file in user locale directories are updated
+ repo = CatalogRepository(self.srcdir, self.config.locale_dirs,
+ self.config.language, self.config.source_encoding)
+ for catalog in repo.catalogs:
+ if catalog.domain == 'sphinx' and catalog.is_outdated():
+ catalog.write_mo(self.config.language,
+ self.config.gettext_allow_fuzzy_translations)
+
+ locale_dirs: list[str | None] = list(repo.locale_dirs)
+ locale_dirs += [None]
+ locale_dirs += [path.join(package_dir, 'locale')]
+
+ self.translator, has_translation = locale.init(locale_dirs, self.config.language)
+ if has_translation:
+ logger.info(__('done'))
+ else:
+ logger.info(__('not available for built-in messages'))
+
+ def _init_env(self, freshenv: bool) -> BuildEnvironment:
+ filename = path.join(self.doctreedir, ENV_PICKLE_FILENAME)
+ if freshenv or not os.path.exists(filename):
+ return self._create_fresh_env()
+ else:
+ return self._load_existing_env(filename)
+
+ def _create_fresh_env(self) -> BuildEnvironment:
+ env = BuildEnvironment(self)
+ self._fresh_env_used = True
+ return env
+
+ @progress_message(__('loading pickled environment'))
+ def _load_existing_env(self, filename: str) -> BuildEnvironment:
+ try:
+ with open(filename, 'rb') as f:
+ env = pickle.load(f)
+ env.setup(self)
+ self._fresh_env_used = False
+ except Exception as err:
+ logger.info(__('failed: %s'), err)
+ env = self._create_fresh_env()
+ return env
+
+ def _post_init_env(self) -> None:
+ if self._fresh_env_used:
+ self.env.find_files(self.config, self.builder)
+ del self._fresh_env_used
+
+ def preload_builder(self, name: str) -> None:
+ self.registry.preload_builder(self, name)
+
+ def create_builder(self, name: str) -> Builder:
+ if name is None:
+ logger.info(__('No builder selected, using default: html'))
+ name = 'html'
+
+ return self.registry.create_builder(self, name, self.env)
+
+ def _init_builder(self) -> None:
+ self.builder.init()
+ self.events.emit('builder-inited')
+
+ # ---- main "build" method -------------------------------------------------
+
+ def build(self, force_all: bool = False, filenames: list[str] | None = None) -> None:
+ self.phase = BuildPhase.READING
+ try:
+ if force_all:
+ self.builder.build_all()
+ elif filenames:
+ self.builder.build_specific(filenames)
+ else:
+ self.builder.build_update()
+
+ self.events.emit('build-finished', None)
+ except Exception as err:
+ # delete the saved env to force a fresh build next time
+ envfile = path.join(self.doctreedir, ENV_PICKLE_FILENAME)
+ if path.isfile(envfile):
+ os.unlink(envfile)
+ self.events.emit('build-finished', err)
+ raise
+
+ if self._warncount and self.keep_going:
+ self.statuscode = 1
+
+ status = (__('succeeded') if self.statuscode == 0
+ else __('finished with problems'))
+ if self._warncount:
+ if self.warningiserror:
+ if self._warncount == 1:
+ msg = __('build %s, %s warning (with warnings treated as errors).')
+ else:
+ msg = __('build %s, %s warnings (with warnings treated as errors).')
+ else:
+ if self._warncount == 1:
+ msg = __('build %s, %s warning.')
+ else:
+ msg = __('build %s, %s warnings.')
+
+ logger.info(bold(msg % (status, self._warncount)))
+ else:
+ logger.info(bold(__('build %s.') % status))
+
+ if self.statuscode == 0 and self.builder.epilog:
+ logger.info('')
+ logger.info(self.builder.epilog % {
+ 'outdir': relpath(self.outdir),
+ 'project': self.config.project,
+ })
+
+ self.builder.cleanup()
+
+ # ---- general extensibility interface -------------------------------------
+
+ def setup_extension(self, extname: str) -> None:
+ """Import and setup a Sphinx extension module.
+
+ Load the extension given by the module *name*. Use this if your
+ extension needs the features provided by another extension. No-op if
+ called twice.
+ """
+ logger.debug('[app] setting up extension: %r', extname)
+ self.registry.load_extension(self, extname)
+
+ @staticmethod
+ def require_sphinx(version: tuple[int, int] | str) -> None:
+ """Check the Sphinx version if requested.
+
+ Compare *version* with the version of the running Sphinx, and abort the
+ build when it is too old.
+
+ :param version: The required version in the form of ``major.minor`` or
+ ``(major, minor)``.
+
+ .. versionadded:: 1.0
+ .. versionchanged:: 7.1
+ Type of *version* now allows ``(major, minor)`` form.
+ """
+ if isinstance(version, tuple):
+ major, minor = version
+ else:
+ major, minor = map(int, version.split('.')[:2])
+ if (major, minor) > sphinx.version_info[:2]:
+ req = f'{major}.{minor}'
+ raise VersionRequirementError(req)
+
+ # event interface
+ def connect(self, event: str, callback: Callable, priority: int = 500) -> int:
+ """Register *callback* to be called when *event* is emitted.
+
+ For details on available core events and the arguments of callback
+ functions, please see :ref:`events`.
+
+ :param event: The name of target event
+ :param callback: Callback function for the event
+ :param priority: The priority of the callback. The callbacks will be invoked
+ in order of *priority* (ascending).
+ :return: A listener ID. It can be used for :meth:`disconnect`.
+
+ .. versionchanged:: 3.0
+
+ Support *priority*
+ """
+ listener_id = self.events.connect(event, callback, priority)
+ logger.debug('[app] connecting event %r (%d): %r [id=%s]',
+ event, priority, callback, listener_id)
+ return listener_id
+
+ def disconnect(self, listener_id: int) -> None:
+ """Unregister callback by *listener_id*.
+
+ :param listener_id: A listener_id that :meth:`connect` returns
+ """
+ logger.debug('[app] disconnecting event: [id=%s]', listener_id)
+ self.events.disconnect(listener_id)
+
+ def emit(self, event: str, *args: Any,
+ allowed_exceptions: tuple[type[Exception], ...] = ()) -> list:
+ """Emit *event* and pass *arguments* to the callback functions.
+
+ Return the return values of all callbacks as a list. Do not emit core
+ Sphinx events in extensions!
+
+ :param event: The name of event that will be emitted
+ :param args: The arguments for the event
+ :param allowed_exceptions: The list of exceptions that are allowed in the callbacks
+
+ .. versionchanged:: 3.1
+
+ Added *allowed_exceptions* to specify path-through exceptions
+ """
+ return self.events.emit(event, *args, allowed_exceptions=allowed_exceptions)
+
+ def emit_firstresult(self, event: str, *args: Any,
+ allowed_exceptions: tuple[type[Exception], ...] = ()) -> Any:
+ """Emit *event* and pass *arguments* to the callback functions.
+
+ Return the result of the first callback that doesn't return ``None``.
+
+ :param event: The name of event that will be emitted
+ :param args: The arguments for the event
+ :param allowed_exceptions: The list of exceptions that are allowed in the callbacks
+
+ .. versionadded:: 0.5
+ .. versionchanged:: 3.1
+
+ Added *allowed_exceptions* to specify path-through exceptions
+ """
+ return self.events.emit_firstresult(event, *args,
+ allowed_exceptions=allowed_exceptions)
+
+ # registering addon parts
+
+ def add_builder(self, builder: type[Builder], override: bool = False) -> None:
+ """Register a new builder.
+
+ :param builder: A builder class
+ :param override: If true, install the builder forcedly even if another builder
+ is already installed as the same name
+
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ self.registry.add_builder(builder, override=override)
+
+ # TODO(stephenfin): Describe 'types' parameter
+ def add_config_value(self, name: str, default: Any, rebuild: bool | str,
+ types: Any = ()) -> None:
+ """Register a configuration value.
+
+ This is necessary for Sphinx to recognize new values and set default
+ values accordingly.
+
+
+ :param name: The name of the configuration value. It is recommended to be prefixed
+ with the extension name (ex. ``html_logo``, ``epub_title``)
+ :param default: The default value of the configuration.
+ :param rebuild: The condition of rebuild. It must be one of those values:
+
+ * ``'env'`` if a change in the setting only takes effect when a
+ document is parsed -- this means that the whole environment must be
+ rebuilt.
+ * ``'html'`` if a change in the setting needs a full rebuild of HTML
+ documents.
+ * ``''`` if a change in the setting will not need any special rebuild.
+ :param types: The type of configuration value. A list of types can be specified. For
+ example, ``[str]`` is used to describe a configuration that takes string
+ value.
+
+ .. versionchanged:: 0.4
+ If the *default* value is a callable, it will be called with the
+ config object as its argument in order to get the default value.
+ This can be used to implement config values whose default depends on
+ other values.
+
+ .. versionchanged:: 0.6
+ Changed *rebuild* from a simple boolean (equivalent to ``''`` or
+ ``'env'``) to a string. However, booleans are still accepted and
+ converted internally.
+ """
+ logger.debug('[app] adding config value: %r', (name, default, rebuild, types))
+ if rebuild in (False, True):
+ rebuild = 'env' if rebuild else ''
+ self.config.add(name, default, rebuild, types)
+
+ def add_event(self, name: str) -> None:
+ """Register an event called *name*.
+
+ This is needed to be able to emit it.
+
+ :param name: The name of the event
+ """
+ logger.debug('[app] adding event: %r', name)
+ self.events.add(name)
+
+ def set_translator(self, name: str, translator_class: type[nodes.NodeVisitor],
+ override: bool = False) -> None:
+ """Register or override a Docutils translator class.
+
+ This is used to register a custom output translator or to replace a
+ builtin translator. This allows extensions to use a custom translator
+ and define custom nodes for the translator (see :meth:`add_node`).
+
+ :param name: The name of the builder for the translator
+ :param translator_class: A translator class
+ :param override: If true, install the translator forcedly even if another translator
+ is already installed as the same name
+
+ .. versionadded:: 1.3
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ self.registry.add_translator(name, translator_class, override=override)
+
+ def add_node(self, node: type[Element], override: bool = False,
+ **kwargs: tuple[Callable, Callable | None]) -> None:
+ """Register a Docutils node class.
+
+ This is necessary for Docutils internals. It may also be used in the
+ future to validate nodes in the parsed documents.
+
+ :param node: A node class
+ :param kwargs: Visitor functions for each builder (see below)
+ :param override: If true, install the node forcedly even if another node is already
+ installed as the same name
+
+ Node visitor functions for the Sphinx HTML, LaTeX, text and manpage
+ writers can be given as keyword arguments: the keyword should be one or
+ more of ``'html'``, ``'latex'``, ``'text'``, ``'man'``, ``'texinfo'``
+ or any other supported translators, the value a 2-tuple of ``(visit,
+ depart)`` methods. ``depart`` can be ``None`` if the ``visit``
+ function raises :exc:`docutils.nodes.SkipNode`. Example:
+
+ .. code-block:: python
+
+ class math(docutils.nodes.Element): pass
+
+ def visit_math_html(self, node):
+ self.body.append(self.starttag(node, 'math'))
+ def depart_math_html(self, node):
+ self.body.append('</math>')
+
+ app.add_node(math, html=(visit_math_html, depart_math_html))
+
+ Obviously, translators for which you don't specify visitor methods will
+ choke on the node when encountered in a document to translate.
+
+ .. versionchanged:: 0.5
+ Added the support for keyword arguments giving visit functions.
+ """
+ logger.debug('[app] adding node: %r', (node, kwargs))
+ if not override and docutils.is_node_registered(node):
+ logger.warning(__('node class %r is already registered, '
+ 'its visitors will be overridden'),
+ node.__name__, type='app', subtype='add_node')
+ docutils.register_node(node)
+ self.registry.add_translation_handlers(node, **kwargs)
+
+ def add_enumerable_node(self, node: type[Element], figtype: str,
+ title_getter: TitleGetter | None = None, override: bool = False,
+ **kwargs: tuple[Callable, Callable]) -> None:
+ """Register a Docutils node class as a numfig target.
+
+ Sphinx numbers the node automatically. And then the users can refer it
+ using :rst:role:`numref`.
+
+ :param node: A node class
+ :param figtype: The type of enumerable nodes. Each figtype has individual numbering
+ sequences. As system figtypes, ``figure``, ``table`` and
+ ``code-block`` are defined. It is possible to add custom nodes to
+ these default figtypes. It is also possible to define new custom
+ figtype if a new figtype is given.
+ :param title_getter: A getter function to obtain the title of node. It takes an
+ instance of the enumerable node, and it must return its title as
+ string. The title is used to the default title of references for
+ :rst:role:`ref`. By default, Sphinx searches
+ ``docutils.nodes.caption`` or ``docutils.nodes.title`` from the
+ node as a title.
+ :param kwargs: Visitor functions for each builder (same as :meth:`add_node`)
+ :param override: If true, install the node forcedly even if another node is already
+ installed as the same name
+
+ .. versionadded:: 1.4
+ """
+ self.registry.add_enumerable_node(node, figtype, title_getter, override=override)
+ self.add_node(node, override=override, **kwargs)
+
+ def add_directive(self, name: str, cls: type[Directive], override: bool = False) -> None:
+ """Register a Docutils directive.
+
+ :param name: The name of the directive
+ :param cls: A directive class
+ :param override: If false, do not install it if another directive
+ is already installed as the same name
+ If true, unconditionally install the directive.
+
+ For example, a custom directive named ``my-directive`` would be added
+ like this:
+
+ .. code-block:: python
+
+ from docutils.parsers.rst import Directive, directives
+
+ class MyDirective(Directive):
+ has_content = True
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec = {
+ 'class': directives.class_option,
+ 'name': directives.unchanged,
+ }
+
+ def run(self):
+ ...
+
+ def setup(app):
+ app.add_directive('my-directive', MyDirective)
+
+ For more details, see `the Docutils docs
+ <https://docutils.sourceforge.io/docs/howto/rst-directives.html>`__ .
+
+ .. versionchanged:: 0.6
+ Docutils 0.5-style directive classes are now supported.
+ .. deprecated:: 1.8
+ Docutils 0.4-style (function based) directives support is deprecated.
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ logger.debug('[app] adding directive: %r', (name, cls))
+ if not override and docutils.is_directive_registered(name):
+ logger.warning(__('directive %r is already registered, it will be overridden'),
+ name, type='app', subtype='add_directive')
+
+ docutils.register_directive(name, cls)
+
+ def add_role(self, name: str, role: Any, override: bool = False) -> None:
+ """Register a Docutils role.
+
+ :param name: The name of role
+ :param role: A role function
+ :param override: If false, do not install it if another role
+ is already installed as the same name
+ If true, unconditionally install the role.
+
+ For more details about role functions, see `the Docutils docs
+ <https://docutils.sourceforge.io/docs/howto/rst-roles.html>`__ .
+
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ logger.debug('[app] adding role: %r', (name, role))
+ if not override and docutils.is_role_registered(name):
+ logger.warning(__('role %r is already registered, it will be overridden'),
+ name, type='app', subtype='add_role')
+ docutils.register_role(name, role)
+
+ def add_generic_role(self, name: str, nodeclass: Any, override: bool = False) -> None:
+ """Register a generic Docutils role.
+
+ Register a Docutils role that does nothing but wrap its contents in the
+ node given by *nodeclass*.
+
+ :param override: If false, do not install it if another role
+ is already installed as the same name
+ If true, unconditionally install the role.
+
+ .. versionadded:: 0.6
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ # Don't use ``roles.register_generic_role`` because it uses
+ # ``register_canonical_role``.
+ logger.debug('[app] adding generic role: %r', (name, nodeclass))
+ if not override and docutils.is_role_registered(name):
+ logger.warning(__('role %r is already registered, it will be overridden'),
+ name, type='app', subtype='add_generic_role')
+ role = roles.GenericRole(name, nodeclass)
+ docutils.register_role(name, role) # type: ignore[arg-type]
+
+ def add_domain(self, domain: type[Domain], override: bool = False) -> None:
+ """Register a domain.
+
+ :param domain: A domain class
+ :param override: If false, do not install it if another domain
+ is already installed as the same name
+ If true, unconditionally install the domain.
+
+ .. versionadded:: 1.0
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ self.registry.add_domain(domain, override=override)
+
+ def add_directive_to_domain(self, domain: str, name: str,
+ cls: type[Directive], override: bool = False) -> None:
+ """Register a Docutils directive in a domain.
+
+ Like :meth:`add_directive`, but the directive is added to the domain
+ named *domain*.
+
+ :param domain: The name of target domain
+ :param name: A name of directive
+ :param cls: A directive class
+ :param override: If false, do not install it if another directive
+ is already installed as the same name
+ If true, unconditionally install the directive.
+
+ .. versionadded:: 1.0
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ self.registry.add_directive_to_domain(domain, name, cls, override=override)
+
+ def add_role_to_domain(self, domain: str, name: str, role: RoleFunction | XRefRole,
+ override: bool = False) -> None:
+ """Register a Docutils role in a domain.
+
+ Like :meth:`add_role`, but the role is added to the domain named
+ *domain*.
+
+ :param domain: The name of the target domain
+ :param name: The name of the role
+ :param role: The role function
+ :param override: If false, do not install it if another role
+ is already installed as the same name
+ If true, unconditionally install the role.
+
+ .. versionadded:: 1.0
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ self.registry.add_role_to_domain(domain, name, role, override=override)
+
+ def add_index_to_domain(self, domain: str, index: type[Index], override: bool = False,
+ ) -> None:
+ """Register a custom index for a domain.
+
+ Add a custom *index* class to the domain named *domain*.
+
+ :param domain: The name of the target domain
+ :param index: The index class
+ :param override: If false, do not install it if another index
+ is already installed as the same name
+ If true, unconditionally install the index.
+
+ .. versionadded:: 1.0
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ self.registry.add_index_to_domain(domain, index)
+
+ def add_object_type(self, directivename: str, rolename: str, indextemplate: str = '',
+ parse_node: Callable | None = None,
+ ref_nodeclass: type[TextElement] | None = None,
+ objname: str = '', doc_field_types: Sequence = (),
+ override: bool = False,
+ ) -> None:
+ """Register a new object type.
+
+ This method is a very convenient way to add a new :term:`object` type
+ that can be cross-referenced. It will do this:
+
+ - Create a new directive (called *directivename*) for documenting an
+ object. It will automatically add index entries if *indextemplate*
+ is nonempty; if given, it must contain exactly one instance of
+ ``%s``. See the example below for how the template will be
+ interpreted.
+ - Create a new role (called *rolename*) to cross-reference to these
+ object descriptions.
+ - If you provide *parse_node*, it must be a function that takes a
+ string and a docutils node, and it must populate the node with
+ children parsed from the string. It must then return the name of the
+ item to be used in cross-referencing and index entries. See the
+ :file:`conf.py` file in the source for this documentation for an
+ example.
+ - The *objname* (if not given, will default to *directivename*) names
+ the type of object. It is used when listing objects, e.g. in search
+ results.
+
+ For example, if you have this call in a custom Sphinx extension::
+
+ app.add_object_type('directive', 'dir', 'pair: %s; directive')
+
+ you can use this markup in your documents::
+
+ .. rst:directive:: function
+
+ Document a function.
+
+ <...>
+
+ See also the :rst:dir:`function` directive.
+
+ For the directive, an index entry will be generated as if you had prepended ::
+
+ .. index:: pair: function; directive
+
+ The reference node will be of class ``literal`` (so it will be rendered
+ in a proportional font, as appropriate for code) unless you give the
+ *ref_nodeclass* argument, which must be a docutils node class. Most
+ useful are ``docutils.nodes.emphasis`` or ``docutils.nodes.strong`` --
+ you can also use ``docutils.nodes.generated`` if you want no further
+ text decoration. If the text should be treated as literal (e.g. no
+ smart quote replacement), but not have typewriter styling, use
+ ``sphinx.addnodes.literal_emphasis`` or
+ ``sphinx.addnodes.literal_strong``.
+
+ For the role content, you have the same syntactical possibilities as
+ for standard Sphinx roles (see :ref:`xref-syntax`).
+
+ If *override* is True, the given object_type is forcedly installed even if
+ an object_type having the same name is already installed.
+
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ self.registry.add_object_type(directivename, rolename, indextemplate, parse_node,
+ ref_nodeclass, objname, doc_field_types,
+ override=override)
+
+ def add_crossref_type(self, directivename: str, rolename: str, indextemplate: str = '',
+ ref_nodeclass: type[TextElement] | None = None, objname: str = '',
+ override: bool = False) -> None:
+ """Register a new crossref object type.
+
+ This method is very similar to :meth:`~Sphinx.add_object_type` except that the
+ directive it generates must be empty, and will produce no output.
+
+ That means that you can add semantic targets to your sources, and refer
+ to them using custom roles instead of generic ones (like
+ :rst:role:`ref`). Example call::
+
+ app.add_crossref_type('topic', 'topic', 'single: %s',
+ docutils.nodes.emphasis)
+
+ Example usage::
+
+ .. topic:: application API
+
+ The application API
+ -------------------
+
+ Some random text here.
+
+ See also :topic:`this section <application API>`.
+
+ (Of course, the element following the ``topic`` directive needn't be a
+ section.)
+
+
+ :param override: If false, do not install it if another cross-reference type
+ is already installed as the same name
+ If true, unconditionally install the cross-reference type.
+
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ self.registry.add_crossref_type(directivename, rolename,
+ indextemplate, ref_nodeclass, objname,
+ override=override)
+
+ def add_transform(self, transform: type[Transform]) -> None:
+ """Register a Docutils transform to be applied after parsing.
+
+ Add the standard docutils :class:`~docutils.transforms.Transform`
+ subclass *transform* to the list of transforms that are applied after
+ Sphinx parses a reST document.
+
+ :param transform: A transform class
+
+ .. list-table:: priority range categories for Sphinx transforms
+ :widths: 20,80
+
+ * - Priority
+ - Main purpose in Sphinx
+ * - 0-99
+ - Fix invalid nodes by docutils. Translate a doctree.
+ * - 100-299
+ - Preparation
+ * - 300-399
+ - early
+ * - 400-699
+ - main
+ * - 700-799
+ - Post processing. Deadline to modify text and referencing.
+ * - 800-899
+ - Collect referencing and referenced nodes. Domain processing.
+ * - 900-999
+ - Finalize and clean up.
+
+ refs: `Transform Priority Range Categories`__
+
+ __ https://docutils.sourceforge.io/docs/ref/transforms.html#transform-priority-range-categories
+ """ # NoQA: E501,RUF100 # Flake8 thinks the URL is too long, Ruff special cases URLs.
+ self.registry.add_transform(transform)
+
+ def add_post_transform(self, transform: type[Transform]) -> None:
+ """Register a Docutils transform to be applied before writing.
+
+ Add the standard docutils :class:`~docutils.transforms.Transform`
+ subclass *transform* to the list of transforms that are applied before
+ Sphinx writes a document.
+
+ :param transform: A transform class
+ """
+ self.registry.add_post_transform(transform)
+
+ def add_js_file(self, filename: str | None, priority: int = 500,
+ loading_method: str | None = None, **kwargs: Any) -> None:
+ """Register a JavaScript file to include in the HTML output.
+
+ :param filename: The name of a JavaScript file that the default HTML
+ template will include. It must be relative to the HTML
+ static path, or a full URI with scheme, or ``None`` .
+ The ``None`` value is used to create an inline
+ ``<script>`` tag. See the description of *kwargs*
+ below.
+ :param priority: Files are included in ascending order of priority. If
+ multiple JavaScript files have the same priority,
+ those files will be included in order of registration.
+ See list of "priority range for JavaScript files" below.
+ :param loading_method: The loading method for the JavaScript file.
+ Either ``'async'`` or ``'defer'`` are allowed.
+ :param kwargs: Extra keyword arguments are included as attributes of the
+ ``<script>`` tag. If the special keyword argument
+ ``body`` is given, its value will be added as the content
+ of the ``<script>`` tag.
+
+ Example::
+
+ app.add_js_file('example.js')
+ # => <script src="_static/example.js"></script>
+
+ app.add_js_file('example.js', loading_method="async")
+ # => <script src="_static/example.js" async="async"></script>
+
+ app.add_js_file(None, body="var myVariable = 'foo';")
+ # => <script>var myVariable = 'foo';</script>
+
+ .. list-table:: priority range for JavaScript files
+ :widths: 20,80
+
+ * - Priority
+ - Main purpose in Sphinx
+ * - 200
+ - default priority for built-in JavaScript files
+ * - 500
+ - default priority for extensions
+ * - 800
+ - default priority for :confval:`html_js_files`
+
+ A JavaScript file can be added to the specific HTML page when an extension
+ calls this method on :event:`html-page-context` event.
+
+ .. versionadded:: 0.5
+
+ .. versionchanged:: 1.8
+ Renamed from ``app.add_javascript()``.
+ And it allows keyword arguments as attributes of script tag.
+
+ .. versionchanged:: 3.5
+ Take priority argument. Allow to add a JavaScript file to the specific page.
+ .. versionchanged:: 4.4
+ Take loading_method argument. Allow to change the loading method of the
+ JavaScript file.
+ """
+ if loading_method == 'async':
+ kwargs['async'] = 'async'
+ elif loading_method == 'defer':
+ kwargs['defer'] = 'defer'
+
+ self.registry.add_js_file(filename, priority=priority, **kwargs)
+ with contextlib.suppress(AttributeError):
+ self.builder.add_js_file( # type: ignore[attr-defined]
+ filename, priority=priority, **kwargs,
+ )
+
+ def add_css_file(self, filename: str, priority: int = 500, **kwargs: Any) -> None:
+ """Register a stylesheet to include in the HTML output.
+
+ :param filename: The name of a CSS file that the default HTML
+ template will include. It must be relative to the HTML
+ static path, or a full URI with scheme.
+ :param priority: Files are included in ascending order of priority. If
+ multiple CSS files have the same priority,
+ those files will be included in order of registration.
+ See list of "priority range for CSS files" below.
+ :param kwargs: Extra keyword arguments are included as attributes of the
+ ``<link>`` tag.
+
+ Example::
+
+ app.add_css_file('custom.css')
+ # => <link rel="stylesheet" href="_static/custom.css" type="text/css" />
+
+ app.add_css_file('print.css', media='print')
+ # => <link rel="stylesheet" href="_static/print.css"
+ # type="text/css" media="print" />
+
+ app.add_css_file('fancy.css', rel='alternate stylesheet', title='fancy')
+ # => <link rel="alternate stylesheet" href="_static/fancy.css"
+ # type="text/css" title="fancy" />
+
+ .. list-table:: priority range for CSS files
+ :widths: 20,80
+
+ * - Priority
+ - Main purpose in Sphinx
+ * - 200
+ - default priority for built-in CSS files
+ * - 500
+ - default priority for extensions
+ * - 800
+ - default priority for :confval:`html_css_files`
+
+ A CSS file can be added to the specific HTML page when an extension calls
+ this method on :event:`html-page-context` event.
+
+ .. versionadded:: 1.0
+
+ .. versionchanged:: 1.6
+ Optional ``alternate`` and/or ``title`` attributes can be supplied
+ with the arguments *alternate* (a Boolean) and *title* (a string).
+ The default is no title and *alternate* = ``False``. For
+ more information, refer to the `documentation
+ <https://mdn.io/Web/CSS/Alternative_style_sheets>`__.
+
+ .. versionchanged:: 1.8
+ Renamed from ``app.add_stylesheet()``.
+ And it allows keyword arguments as attributes of link tag.
+
+ .. versionchanged:: 3.5
+ Take priority argument. Allow to add a CSS file to the specific page.
+ """
+ logger.debug('[app] adding stylesheet: %r', filename)
+ self.registry.add_css_files(filename, priority=priority, **kwargs)
+ with contextlib.suppress(AttributeError):
+ self.builder.add_css_file( # type: ignore[attr-defined]
+ filename, priority=priority, **kwargs,
+ )
+
+ def add_latex_package(self, packagename: str, options: str | None = None,
+ after_hyperref: bool = False) -> None:
+ r"""Register a package to include in the LaTeX source code.
+
+ Add *packagename* to the list of packages that LaTeX source code will
+ include. If you provide *options*, it will be taken to the `\usepackage`
+ declaration. If you set *after_hyperref* truthy, the package will be
+ loaded after ``hyperref`` package.
+
+ .. code-block:: python
+
+ app.add_latex_package('mypackage')
+ # => \usepackage{mypackage}
+ app.add_latex_package('mypackage', 'foo,bar')
+ # => \usepackage[foo,bar]{mypackage}
+
+ .. versionadded:: 1.3
+ .. versionadded:: 3.1
+
+ *after_hyperref* option.
+ """
+ self.registry.add_latex_package(packagename, options, after_hyperref)
+
+ def add_lexer(self, alias: str, lexer: type[Lexer]) -> None:
+ """Register a new lexer for source code.
+
+ Use *lexer* to highlight code blocks with the given language *alias*.
+
+ .. versionadded:: 0.6
+ .. versionchanged:: 2.1
+ Take a lexer class as an argument.
+ .. versionchanged:: 4.0
+ Removed support for lexer instances as an argument.
+ """
+ logger.debug('[app] adding lexer: %r', (alias, lexer))
+ lexer_classes[alias] = lexer
+
+ def add_autodocumenter(self, cls: Any, override: bool = False) -> None:
+ """Register a new documenter class for the autodoc extension.
+
+ Add *cls* as a new documenter class for the :mod:`sphinx.ext.autodoc`
+ extension. It must be a subclass of
+ :class:`sphinx.ext.autodoc.Documenter`. This allows auto-documenting
+ new types of objects. See the source of the autodoc module for
+ examples on how to subclass :class:`~sphinx.ext.autodoc.Documenter`.
+
+ If *override* is True, the given *cls* is forcedly installed even if
+ a documenter having the same name is already installed.
+
+ See :ref:`autodoc_ext_tutorial`.
+
+ .. versionadded:: 0.6
+ .. versionchanged:: 2.2
+ Add *override* keyword.
+ """
+ logger.debug('[app] adding autodocumenter: %r', cls)
+ from sphinx.ext.autodoc.directive import AutodocDirective
+ self.registry.add_documenter(cls.objtype, cls)
+ self.add_directive('auto' + cls.objtype, AutodocDirective, override=override)
+
+ def add_autodoc_attrgetter(self, typ: type, getter: Callable[[Any, str, Any], Any],
+ ) -> None:
+ """Register a new ``getattr``-like function for the autodoc extension.
+
+ Add *getter*, which must be a function with an interface compatible to
+ the :func:`getattr` builtin, as the autodoc attribute getter for
+ objects that are instances of *typ*. All cases where autodoc needs to
+ get an attribute of a type are then handled by this function instead of
+ :func:`getattr`.
+
+ .. versionadded:: 0.6
+ """
+ logger.debug('[app] adding autodoc attrgetter: %r', (typ, getter))
+ self.registry.add_autodoc_attrgetter(typ, getter)
+
+ def add_search_language(self, cls: Any) -> None:
+ """Register a new language for the HTML search index.
+
+ Add *cls*, which must be a subclass of
+ :class:`sphinx.search.SearchLanguage`, as a support language for
+ building the HTML full-text search index. The class must have a *lang*
+ attribute that indicates the language it should be used for. See
+ :confval:`html_search_language`.
+
+ .. versionadded:: 1.1
+ """
+ logger.debug('[app] adding search language: %r', cls)
+ from sphinx.search import SearchLanguage, languages
+ assert issubclass(cls, SearchLanguage)
+ languages[cls.lang] = cls
+
+ def add_source_suffix(self, suffix: str, filetype: str, override: bool = False) -> None:
+ """Register a suffix of source files.
+
+ Same as :confval:`source_suffix`. The users can override this
+ using the config setting.
+
+ :param override: If false, do not install it the same suffix
+ is already installed.
+ If true, unconditionally install the suffix.
+
+ .. versionadded:: 1.8
+ """
+ self.registry.add_source_suffix(suffix, filetype, override=override)
+
+ def add_source_parser(self, parser: type[Parser], override: bool = False) -> None:
+ """Register a parser class.
+
+ :param override: If false, do not install it if another parser
+ is already installed for the same suffix.
+ If true, unconditionally install the parser.
+
+ .. versionadded:: 1.4
+ .. versionchanged:: 1.8
+ *suffix* argument is deprecated. It only accepts *parser* argument.
+ Use :meth:`add_source_suffix` API to register suffix instead.
+ .. versionchanged:: 1.8
+ Add *override* keyword.
+ """
+ self.registry.add_source_parser(parser, override=override)
+
+ def add_env_collector(self, collector: type[EnvironmentCollector]) -> None:
+ """Register an environment collector class.
+
+ Refer to :ref:`collector-api`.
+
+ .. versionadded:: 1.6
+ """
+ logger.debug('[app] adding environment collector: %r', collector)
+ collector().enable(self)
+
+ def add_html_theme(self, name: str, theme_path: str) -> None:
+ """Register a HTML Theme.
+
+ The *name* is a name of theme, and *theme_path* is a full path to the
+ theme (refs: :ref:`distribute-your-theme`).
+
+ .. versionadded:: 1.6
+ """
+ logger.debug('[app] adding HTML theme: %r, %r', name, theme_path)
+ self.registry.add_html_theme(name, theme_path)
+
+ def add_html_math_renderer(
+ self,
+ name: str,
+ inline_renderers: tuple[Callable, Callable | None] | None = None,
+ block_renderers: tuple[Callable, Callable | None] | None = None,
+ ) -> None:
+ """Register a math renderer for HTML.
+
+ The *name* is a name of math renderer. Both *inline_renderers* and
+ *block_renderers* are used as visitor functions for the HTML writer:
+ the former for inline math node (``nodes.math``), the latter for
+ block math node (``nodes.math_block``). Regarding visitor functions,
+ see :meth:`add_node` for details.
+
+ .. versionadded:: 1.8
+
+ """
+ self.registry.add_html_math_renderer(name, inline_renderers, block_renderers)
+
+ def add_message_catalog(self, catalog: str, locale_dir: str) -> None:
+ """Register a message catalog.
+
+ :param catalog: The name of the catalog
+ :param locale_dir: The base path of the message catalog
+
+ For more details, see :func:`sphinx.locale.get_translation()`.
+
+ .. versionadded:: 1.8
+ """
+ locale.init([locale_dir], self.config.language, catalog)
+ locale.init_console(locale_dir, catalog)
+
+ # ---- other methods -------------------------------------------------
+ def is_parallel_allowed(self, typ: str) -> bool:
+ """Check whether parallel processing is allowed or not.
+
+ :param typ: A type of processing; ``'read'`` or ``'write'``.
+ """
+ if typ == 'read':
+ attrname = 'parallel_read_safe'
+ message_not_declared = __("the %s extension does not declare if it "
+ "is safe for parallel reading, assuming "
+ "it isn't - please ask the extension author "
+ "to check and make it explicit")
+ message_not_safe = __("the %s extension is not safe for parallel reading")
+ elif typ == 'write':
+ attrname = 'parallel_write_safe'
+ message_not_declared = __("the %s extension does not declare if it "
+ "is safe for parallel writing, assuming "
+ "it isn't - please ask the extension author "
+ "to check and make it explicit")
+ message_not_safe = __("the %s extension is not safe for parallel writing")
+ else:
+ raise ValueError('parallel type %s is not supported' % typ)
+
+ for ext in self.extensions.values():
+ allowed = getattr(ext, attrname, None)
+ if allowed is None:
+ logger.warning(message_not_declared, ext.name)
+ logger.warning(__('doing serial %s'), typ)
+ return False
+ elif not allowed:
+ logger.warning(message_not_safe, ext.name)
+ logger.warning(__('doing serial %s'), typ)
+ return False
+
+ return True
+
+ def set_html_assets_policy(self, policy):
+ """Set the policy to include assets in HTML pages.
+
+ - always: include the assets in all the pages
+ - per_page: include the assets only in pages where they are used
+
+ .. versionadded: 4.1
+ """
+ if policy not in ('always', 'per_page'):
+ raise ValueError('policy %s is not supported' % policy)
+ self.registry.html_assets_policy = policy
+
+
+class TemplateBridge:
+ """
+ This class defines the interface for a "template bridge", that is, a class
+ that renders templates given a template name and a context.
+ """
+
+ def init(
+ self,
+ builder: Builder,
+ theme: Theme | None = None,
+ dirs: list[str] | None = None,
+ ) -> None:
+ """Called by the builder to initialize the template system.
+
+ *builder* is the builder object; you'll probably want to look at the
+ value of ``builder.config.templates_path``.
+
+ *theme* is a :class:`sphinx.theming.Theme` object or None; in the latter
+ case, *dirs* can be list of fixed directories to look for templates.
+ """
+ msg = 'must be implemented in subclasses'
+ raise NotImplementedError(msg)
+
+ def newest_template_mtime(self) -> float:
+ """Called by the builder to determine if output files are outdated
+ because of template changes. Return the mtime of the newest template
+ file that was changed. The default implementation returns ``0``.
+ """
+ return 0
+
+ def render(self, template: str, context: dict) -> None:
+ """Called by the builder to render a template given as a filename with
+ a specified context (a Python dictionary).
+ """
+ msg = 'must be implemented in subclasses'
+ raise NotImplementedError(msg)
+
+ def render_string(self, template: str, context: dict) -> str:
+ """Called by the builder to render a template given as a string with a
+ specified context (a Python dictionary).
+ """
+ msg = 'must be implemented in subclasses'
+ raise NotImplementedError(msg)