diff options
Diffstat (limited to 'sphinx/application.py')
-rw-r--r-- | sphinx/application.py | 1371 |
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) |