From cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 19:25:40 +0200 Subject: Adding upstream version 7.2.6. Signed-off-by: Daniel Baumann --- sphinx/registry.py | 517 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 sphinx/registry.py (limited to 'sphinx/registry.py') diff --git a/sphinx/registry.py b/sphinx/registry.py new file mode 100644 index 0000000..501661d --- /dev/null +++ b/sphinx/registry.py @@ -0,0 +1,517 @@ +"""Sphinx component registry.""" + +from __future__ import annotations + +import sys +import traceback +from importlib import import_module +from types import MethodType +from typing import TYPE_CHECKING, Any, Callable + +if sys.version_info >= (3, 10): + from importlib.metadata import entry_points +else: + from importlib_metadata import entry_points + +from sphinx.domains import Domain, Index, ObjType +from sphinx.domains.std import GenericObject, Target +from sphinx.errors import ExtensionError, SphinxError, VersionRequirementError +from sphinx.extension import Extension +from sphinx.io import create_publisher +from sphinx.locale import __ +from sphinx.parsers import Parser as SphinxParser +from sphinx.roles import XRefRole +from sphinx.util import logging +from sphinx.util.logging import prefixed_warnings + +if TYPE_CHECKING: + from collections.abc import Iterator, Sequence + + from docutils import nodes + from docutils.core import Publisher + from docutils.nodes import Element, Node, TextElement + from docutils.parsers import Parser + from docutils.parsers.rst import Directive + from docutils.transforms import Transform + + from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.config import Config + from sphinx.environment import BuildEnvironment + from sphinx.ext.autodoc import Documenter + from sphinx.util.typing import RoleFunction, TitleGetter + +logger = logging.getLogger(__name__) + +# list of deprecated extensions. Keys are extension name. +# Values are Sphinx version that merge the extension. +EXTENSION_BLACKLIST = { + "sphinxjp.themecore": "1.2", +} + + +class SphinxComponentRegistry: + def __init__(self) -> None: + #: special attrgetter for autodoc; class object -> attrgetter + self.autodoc_attrgettrs: dict[type, Callable[[Any, str, Any], Any]] = {} + + #: builders; a dict of builder name -> bulider class + self.builders: dict[str, type[Builder]] = {} + + #: autodoc documenters; a dict of documenter name -> documenter class + self.documenters: dict[str, type[Documenter]] = {} + + #: css_files; a list of tuple of filename and attributes + self.css_files: list[tuple[str, dict[str, Any]]] = [] + + #: domains; a dict of domain name -> domain class + self.domains: dict[str, type[Domain]] = {} + + #: additional directives for domains + #: a dict of domain name -> dict of directive name -> directive + self.domain_directives: dict[str, dict[str, type[Directive]]] = {} + + #: additional indices for domains + #: a dict of domain name -> list of index class + self.domain_indices: dict[str, list[type[Index]]] = {} + + #: additional object types for domains + #: a dict of domain name -> dict of objtype name -> objtype + self.domain_object_types: dict[str, dict[str, ObjType]] = {} + + #: additional roles for domains + #: a dict of domain name -> dict of role name -> role impl. + self.domain_roles: dict[str, dict[str, RoleFunction | XRefRole]] = {} + + #: additional enumerable nodes + #: a dict of node class -> tuple of figtype and title_getter function + self.enumerable_nodes: dict[type[Node], tuple[str, TitleGetter | None]] = {} + + #: HTML inline and block math renderers + #: a dict of name -> tuple of visit function and depart function + self.html_inline_math_renderers: dict[str, + tuple[Callable, Callable | None]] = {} + self.html_block_math_renderers: dict[str, + tuple[Callable, Callable | None]] = {} + + #: HTML assets + self.html_assets_policy: str = 'per_page' + + #: HTML themes + self.html_themes: dict[str, str] = {} + + #: js_files; list of JS paths or URLs + self.js_files: list[tuple[str | None, dict[str, Any]]] = [] + + #: LaTeX packages; list of package names and its options + self.latex_packages: list[tuple[str, str | None]] = [] + + self.latex_packages_after_hyperref: list[tuple[str, str | None]] = [] + + #: post transforms; list of transforms + self.post_transforms: list[type[Transform]] = [] + + #: source paresrs; file type -> parser class + self.source_parsers: dict[str, type[Parser]] = {} + + #: source suffix: suffix -> file type + self.source_suffix: dict[str, str] = {} + + #: custom translators; builder name -> translator class + self.translators: dict[str, type[nodes.NodeVisitor]] = {} + + #: custom handlers for translators + #: a dict of builder name -> dict of node name -> visitor and departure functions + self.translation_handlers: dict[str, dict[str, tuple[Callable, Callable | None]]] = {} + + #: additional transforms; list of transforms + self.transforms: list[type[Transform]] = [] + + # private cache of Docutils Publishers (file type -> publisher object) + self.publishers: dict[str, Publisher] = {} + + def add_builder(self, builder: type[Builder], override: bool = False) -> None: + logger.debug('[app] adding builder: %r', builder) + if not hasattr(builder, 'name'): + raise ExtensionError(__('Builder class %s has no "name" attribute') % builder) + if builder.name in self.builders and not override: + raise ExtensionError(__('Builder %r already exists (in module %s)') % + (builder.name, self.builders[builder.name].__module__)) + self.builders[builder.name] = builder + + def preload_builder(self, app: Sphinx, name: str) -> None: + if name is None: + return + + if name not in self.builders: + builder_entry_points = entry_points(group='sphinx.builders') + try: + entry_point = builder_entry_points[name] + except KeyError as exc: + raise SphinxError(__('Builder name %s not registered or available' + ' through entry point') % name) from exc + + self.load_extension(app, entry_point.module) + + def create_builder(self, app: Sphinx, name: str, env: BuildEnvironment) -> Builder: + if name not in self.builders: + raise SphinxError(__('Builder name %s not registered') % name) + + return self.builders[name](app, env) + + def add_domain(self, domain: type[Domain], override: bool = False) -> None: + logger.debug('[app] adding domain: %r', domain) + if domain.name in self.domains and not override: + raise ExtensionError(__('domain %s already registered') % domain.name) + self.domains[domain.name] = domain + + def has_domain(self, domain: str) -> bool: + return domain in self.domains + + def create_domains(self, env: BuildEnvironment) -> Iterator[Domain]: + for DomainClass in self.domains.values(): + domain = DomainClass(env) + + # transplant components added by extensions + domain.directives.update(self.domain_directives.get(domain.name, {})) + domain.roles.update(self.domain_roles.get(domain.name, {})) + domain.indices.extend(self.domain_indices.get(domain.name, [])) + for name, objtype in self.domain_object_types.get(domain.name, {}).items(): + domain.add_object_type(name, objtype) + + yield domain + + def add_directive_to_domain(self, domain: str, name: str, + cls: type[Directive], override: bool = False) -> None: + logger.debug('[app] adding directive to domain: %r', (domain, name, cls)) + if domain not in self.domains: + raise ExtensionError(__('domain %s not yet registered') % domain) + + directives: dict[str, type[Directive]] = self.domain_directives.setdefault(domain, {}) + if name in directives and not override: + raise ExtensionError(__('The %r directive is already registered to domain %s') % + (name, domain)) + directives[name] = cls + + def add_role_to_domain(self, domain: str, name: str, + role: RoleFunction | XRefRole, override: bool = False, + ) -> None: + logger.debug('[app] adding role to domain: %r', (domain, name, role)) + if domain not in self.domains: + raise ExtensionError(__('domain %s not yet registered') % domain) + roles = self.domain_roles.setdefault(domain, {}) + if name in roles and not override: + raise ExtensionError(__('The %r role is already registered to domain %s') % + (name, domain)) + roles[name] = role + + def add_index_to_domain(self, domain: str, index: type[Index], + override: bool = False) -> None: + logger.debug('[app] adding index to domain: %r', (domain, index)) + if domain not in self.domains: + raise ExtensionError(__('domain %s not yet registered') % domain) + indices = self.domain_indices.setdefault(domain, []) + if index in indices and not override: + raise ExtensionError(__('The %r index is already registered to domain %s') % + (index.name, domain)) + indices.append(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: + logger.debug('[app] adding object type: %r', + (directivename, rolename, indextemplate, parse_node, + ref_nodeclass, objname, doc_field_types)) + + # create a subclass of GenericObject as the new directive + directive = type(directivename, + (GenericObject, object), + {'indextemplate': indextemplate, + 'parse_node': parse_node and staticmethod(parse_node), + 'doc_field_types': doc_field_types}) + + self.add_directive_to_domain('std', directivename, directive) + self.add_role_to_domain('std', rolename, XRefRole(innernodeclass=ref_nodeclass)) + + object_types = self.domain_object_types.setdefault('std', {}) + if directivename in object_types and not override: + raise ExtensionError(__('The %r object_type is already registered') % + directivename) + object_types[directivename] = ObjType(objname or directivename, rolename) + + def add_crossref_type( + self, + directivename: str, + rolename: str, + indextemplate: str = '', + ref_nodeclass: type[TextElement] | None = None, + objname: str = '', + override: bool = False, + ) -> None: + logger.debug('[app] adding crossref type: %r', + (directivename, rolename, indextemplate, ref_nodeclass, objname)) + + # create a subclass of Target as the new directive + directive = type(directivename, + (Target, object), + {'indextemplate': indextemplate}) + + self.add_directive_to_domain('std', directivename, directive) + self.add_role_to_domain('std', rolename, XRefRole(innernodeclass=ref_nodeclass)) + + object_types = self.domain_object_types.setdefault('std', {}) + if directivename in object_types and not override: + raise ExtensionError(__('The %r crossref_type is already registered') % + directivename) + object_types[directivename] = ObjType(objname or directivename, rolename) + + def add_source_suffix(self, suffix: str, filetype: str, override: bool = False) -> None: + logger.debug('[app] adding source_suffix: %r, %r', suffix, filetype) + if suffix in self.source_suffix and not override: + raise ExtensionError(__('source_suffix %r is already registered') % suffix) + self.source_suffix[suffix] = filetype + + def add_source_parser(self, parser: type[Parser], override: bool = False) -> None: + logger.debug('[app] adding search source_parser: %r', parser) + + # create a map from filetype to parser + for filetype in parser.supported: + if filetype in self.source_parsers and not override: + raise ExtensionError(__('source_parser for %r is already registered') % + filetype) + self.source_parsers[filetype] = parser + + def get_source_parser(self, filetype: str) -> type[Parser]: + try: + return self.source_parsers[filetype] + except KeyError as exc: + raise SphinxError(__('Source parser for %s not registered') % filetype) from exc + + def get_source_parsers(self) -> dict[str, type[Parser]]: + return self.source_parsers + + def create_source_parser(self, app: Sphinx, filename: str) -> Parser: + parser_class = self.get_source_parser(filename) + parser = parser_class() + if isinstance(parser, SphinxParser): + parser.set_application(app) + return parser + + def add_translator(self, name: str, translator: type[nodes.NodeVisitor], + override: bool = False) -> None: + logger.debug('[app] Change of translator for the %s builder.', name) + if name in self.translators and not override: + raise ExtensionError(__('Translator for %r already exists') % name) + self.translators[name] = translator + + def add_translation_handlers( + self, + node: type[Element], + **kwargs: tuple[Callable, Callable | None], + ) -> None: + logger.debug('[app] adding translation_handlers: %r, %r', node, kwargs) + for builder_name, handlers in kwargs.items(): + translation_handlers = self.translation_handlers.setdefault(builder_name, {}) + try: + visit, depart = handlers # unpack once for assertion + translation_handlers[node.__name__] = (visit, depart) + except ValueError as exc: + raise ExtensionError( + __('kwargs for add_node() must be a (visit, depart) ' + 'function tuple: %r=%r') % (builder_name, handlers), + ) from exc + + def get_translator_class(self, builder: Builder) -> type[nodes.NodeVisitor]: + try: + return self.translators[builder.name] + except KeyError: + try: + return builder.default_translator_class + except AttributeError as err: + msg = f'translator not found for {builder.name}' + raise AttributeError(msg) from err + + def create_translator(self, builder: Builder, *args: Any) -> nodes.NodeVisitor: + translator_class = self.get_translator_class(builder) + translator = translator_class(*args) + + # transplant handlers for custom nodes to translator instance + handlers = self.translation_handlers.get(builder.name, None) + if handlers is None: + # retry with builder.format + handlers = self.translation_handlers.get(builder.format, {}) + + for name, (visit, depart) in handlers.items(): + setattr(translator, 'visit_' + name, MethodType(visit, translator)) + if depart: + setattr(translator, 'depart_' + name, MethodType(depart, translator)) + + return translator + + def add_transform(self, transform: type[Transform]) -> None: + logger.debug('[app] adding transform: %r', transform) + self.transforms.append(transform) + + def get_transforms(self) -> list[type[Transform]]: + return self.transforms + + def add_post_transform(self, transform: type[Transform]) -> None: + logger.debug('[app] adding post transform: %r', transform) + self.post_transforms.append(transform) + + def get_post_transforms(self) -> list[type[Transform]]: + return self.post_transforms + + def add_documenter(self, objtype: str, documenter: type[Documenter]) -> None: + self.documenters[objtype] = documenter + + def add_autodoc_attrgetter(self, typ: type, + attrgetter: Callable[[Any, str, Any], Any]) -> None: + self.autodoc_attrgettrs[typ] = attrgetter + + def add_css_files(self, filename: str, **attributes: Any) -> None: + self.css_files.append((filename, attributes)) + + def add_js_file(self, filename: str | None, **attributes: Any) -> None: + logger.debug('[app] adding js_file: %r, %r', filename, attributes) + self.js_files.append((filename, attributes)) + + def has_latex_package(self, name: str) -> bool: + packages = self.latex_packages + self.latex_packages_after_hyperref + return bool([x for x in packages if x[0] == name]) + + def add_latex_package( + self, name: str, options: str | None, after_hyperref: bool = False, + ) -> None: + if self.has_latex_package(name): + logger.warning("latex package '%s' already included", name) + + logger.debug('[app] adding latex package: %r', name) + if after_hyperref: + self.latex_packages_after_hyperref.append((name, options)) + else: + self.latex_packages.append((name, options)) + + def add_enumerable_node( + self, + node: type[Node], + figtype: str, + title_getter: TitleGetter | None = None, override: bool = False, + ) -> None: + logger.debug('[app] adding enumerable node: (%r, %r, %r)', node, figtype, title_getter) + if node in self.enumerable_nodes and not override: + raise ExtensionError(__('enumerable_node %r already registered') % node) + self.enumerable_nodes[node] = (figtype, title_getter) + + def add_html_math_renderer( + self, + name: str, + inline_renderers: tuple[Callable, Callable | None] | None, + block_renderers: tuple[Callable, Callable | None] | None, + ) -> None: + logger.debug('[app] adding html_math_renderer: %s, %r, %r', + name, inline_renderers, block_renderers) + if name in self.html_inline_math_renderers: + raise ExtensionError(__('math renderer %s is already registered') % name) + + if inline_renderers is not None: + self.html_inline_math_renderers[name] = inline_renderers + if block_renderers is not None: + self.html_block_math_renderers[name] = block_renderers + + def add_html_theme(self, name: str, theme_path: str) -> None: + self.html_themes[name] = theme_path + + def load_extension(self, app: Sphinx, extname: str) -> None: + """Load a Sphinx extension.""" + if extname in app.extensions: # already loaded + return + if extname in EXTENSION_BLACKLIST: + logger.warning(__('the extension %r was already merged with Sphinx since ' + 'version %s; this extension is ignored.'), + extname, EXTENSION_BLACKLIST[extname]) + return + + # update loading context + prefix = __('while setting up extension %s:') % extname + with prefixed_warnings(prefix): + try: + mod = import_module(extname) + except ImportError as err: + logger.verbose(__('Original exception:\n') + traceback.format_exc()) + raise ExtensionError(__('Could not import extension %s') % extname, + err) from err + + setup = getattr(mod, 'setup', None) + if setup is None: + logger.warning(__('extension %r has no setup() function; is it really ' + 'a Sphinx extension module?'), extname) + metadata: dict[str, Any] = {} + else: + try: + metadata = setup(app) + except VersionRequirementError as err: + # add the extension name to the version required + raise VersionRequirementError( + __('The %s extension used by this project needs at least ' + 'Sphinx v%s; it therefore cannot be built with this ' + 'version.') % (extname, err), + ) from err + + if metadata is None: + metadata = {} + elif not isinstance(metadata, dict): + logger.warning(__('extension %r returned an unsupported object from ' + 'its setup() function; it should return None or a ' + 'metadata dictionary'), extname) + metadata = {} + + app.extensions[extname] = Extension(extname, mod, **metadata) + + def get_envversion(self, app: Sphinx) -> dict[str, str]: + from sphinx.environment import ENV_VERSION + envversion = {ext.name: ext.metadata['env_version'] for ext in app.extensions.values() + if ext.metadata.get('env_version')} + envversion['sphinx'] = ENV_VERSION + return envversion + + def get_publisher(self, app: Sphinx, filetype: str) -> Publisher: + try: + return self.publishers[filetype] + except KeyError: + pass + publisher = create_publisher(app, filetype) + self.publishers[filetype] = publisher + return publisher + + +def merge_source_suffix(app: Sphinx, config: Config) -> None: + """Merge any user-specified source_suffix with any added by extensions.""" + for suffix, filetype in app.registry.source_suffix.items(): + if suffix not in app.config.source_suffix: # NoQA: SIM114 + app.config.source_suffix[suffix] = filetype + elif app.config.source_suffix[suffix] is None: + # filetype is not specified (default filetype). + # So it overrides default filetype by extensions setting. + app.config.source_suffix[suffix] = filetype + + # copy config.source_suffix to registry + app.registry.source_suffix = app.config.source_suffix + + +def setup(app: Sphinx) -> dict[str, Any]: + app.connect('config-inited', merge_source_suffix, priority=800) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } -- cgit v1.2.3