diff --git a/sphinx/builders/changes.py b/sphinx/builders/changes.py index afc5f064b22..28c95ad06e0 100644 --- a/sphinx/builders/changes.py +++ b/sphinx/builders/changes.py @@ -4,11 +4,10 @@ import html from os import path -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from sphinx import package_dir from sphinx.builders import Builder -from sphinx.domains.changeset import ChangeSetDomain from sphinx.locale import _, __ from sphinx.theming import HTMLThemeFactory from sphinx.util import logging @@ -49,7 +48,7 @@ def get_outdated_docs(self) -> str: def write(self, *ignored: Any) -> None: version = self.config.version - domain = cast(ChangeSetDomain, self.env.get_domain('changeset')) + domain = self.env.domains.changeset_domain libchanges: dict[str, list[tuple[str, str, int]]] = {} apichanges: list[tuple[str, str, int]] = [] otherchanges: dict[tuple[str, str], list[tuple[str, str, int]]] = {} diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index e6453bea7c3..5ef3c1cea6f 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -33,7 +33,7 @@ from sphinx.builders.html._build_info import BuildInfo from sphinx.config import ENUM, Config from sphinx.deprecation import _deprecation_warning -from sphinx.domains import Domain, Index, IndexEntry +from sphinx.domains import Index, IndexEntry from sphinx.environment.adapters.asset import ImageAdapter from sphinx.environment.adapters.indexentries import IndexEntries from sphinx.environment.adapters.toctree import document_toc, global_toctree_for_doc @@ -439,8 +439,7 @@ def prepare_writing(self, docnames: set[str]) -> None: indices_config = frozenset(indices_config) else: check_names = False - for domain_name in sorted(self.env.domains): - domain: Domain = self.env.domains[domain_name] + for domain in self.env.domains.sorted(): for index_cls in domain.indices: index_name = f'{domain.name}-{index_cls.name}' if check_names and index_name not in indices_config: diff --git a/sphinx/builders/latex/transforms.py b/sphinx/builders/latex/transforms.py index 5d2dfb09a47..3d3fd52e6a9 100644 --- a/sphinx/builders/latex/transforms.py +++ b/sphinx/builders/latex/transforms.py @@ -15,7 +15,6 @@ math_reference, thebibliography, ) -from sphinx.domains.citation import CitationDomain from sphinx.locale import __ from sphinx.transforms import SphinxTransform from sphinx.transforms.post_transforms import SphinxPostTransform @@ -536,7 +535,7 @@ class CitationReferenceTransform(SphinxPostTransform): formats = ('latex',) def run(self, **kwargs: Any) -> None: - domain = cast(CitationDomain, self.env.get_domain('citation')) + domain = self.env.domains.citation_domain matcher = NodeMatcher(addnodes.pending_xref, refdomain='citation', reftype='ref') for node in matcher.findall(self.document): docname, labelid, _ = domain.citations.get(node['reftarget'], ('', '', 0)) @@ -557,7 +556,7 @@ class MathReferenceTransform(SphinxPostTransform): formats = ('latex',) def run(self, **kwargs: Any) -> None: - equations = self.env.get_domain('math').data['objects'] + equations = self.env.domains.math_domain.data['objects'] for node in self.document.findall(addnodes.pending_xref): if node['refdomain'] == 'math' and node['reftype'] in ('eq', 'numref'): docname, _ = equations.get(node['reftarget'], (None, None)) diff --git a/sphinx/builders/xml.py b/sphinx/builders/xml.py index 6be0ff8d6e4..6716a94b452 100644 --- a/sphinx/builders/xml.py +++ b/sphinx/builders/xml.py @@ -75,9 +75,8 @@ def write_doc(self, docname: str, doctree: nodes.document) -> None: # work around multiple string % tuple issues in docutils; # replace tuples in attribute values with lists doctree = doctree.deepcopy() - for domain in self.env.domains.values(): - xmlns = "xmlns:" + domain.name - doctree[xmlns] = "https://www.sphinx-doc.org/" + for domain in self.env.domains.sorted(): + doctree[f'xmlns:{domain.name}'] = 'https://www.sphinx-doc.org/' for node in doctree.findall(nodes.Element): for att, value in node.attributes.items(): if isinstance(value, tuple): diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 0c0c0cc519b..fb42e5ee314 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -351,7 +351,7 @@ def run(self) -> list[Node]: domain_name = self.arguments[0].lower() # if domain_name not in env.domains: # # try searching by label - # for domain in env.domains.values(): + # for domain in env.domains.sorted(): # if domain.label.lower() == domain_name: # domain_name = domain.name # break diff --git a/sphinx/directives/patches.py b/sphinx/directives/patches.py index 55716ba894f..0dd6dc82a30 100644 --- a/sphinx/directives/patches.py +++ b/sphinx/directives/patches.py @@ -12,7 +12,6 @@ from docutils.parsers.rst.roles import set_classes from sphinx.directives import optional_int -from sphinx.domains.math import MathDomain from sphinx.locale import __ from sphinx.util import logging from sphinx.util.docutils import SphinxDirective @@ -165,7 +164,7 @@ def add_target(self, ret: list[Node]) -> None: return # register label to domain - domain = cast(MathDomain, self.env.get_domain('math')) + domain = self.env.domains.math_domain domain.note_equation(self.env.docname, node['label'], location=node) node['number'] = domain.get_equation_number_for(node['label']) diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index ef3b5e90ade..12a80e0d8d4 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -7,13 +7,14 @@ from __future__ import annotations import copy -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING from sphinx.domains._index import Index, IndexEntry from sphinx.locale import _ if TYPE_CHECKING: - from collections.abc import Callable, Iterable, Sequence + from collections.abc import Callable, Iterable, Sequence, Set + from typing import Any from docutils import nodes from docutils.nodes import Element, Node @@ -136,10 +137,8 @@ def __init__(self, env: BuildEnvironment) -> None: def setup(self) -> None: """Set up domain object.""" - from sphinx.domains.std import StandardDomain - # Add special hyperlink target for index pages (ex. py-modindex) - std = cast(StandardDomain, self.env.get_domain('std')) + std = self.env.domains.standard_domain for index in self.indices: if index.name and index.localname: docname = f"{self.name}-{index.name}" @@ -199,7 +198,7 @@ def clear_doc(self, docname: str) -> None: """Remove traces of a document in the domain-specific inventories.""" pass - def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: """Merge in data regarding *docnames* from a different domaindata inventory (coming from a subprocess in parallel builds). """ diff --git a/sphinx/domains/_domains_container.py b/sphinx/domains/_domains_container.py new file mode 100644 index 00000000000..7e2eb218117 --- /dev/null +++ b/sphinx/domains/_domains_container.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, overload + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator, Mapping, Set + from typing import Any, Final, Literal, NoReturn + + from docutils import nodes + from typing_extensions import Self + + from sphinx.domains import Domain + from sphinx.domains.c import CDomain + from sphinx.domains.changeset import ChangeSetDomain + from sphinx.domains.citation import CitationDomain + from sphinx.domains.cpp import CPPDomain + from sphinx.domains.index import IndexDomain + from sphinx.domains.javascript import JavaScriptDomain + from sphinx.domains.math import MathDomain + from sphinx.domains.python import PythonDomain + from sphinx.domains.rst import ReSTDomain + from sphinx.domains.std import StandardDomain + from sphinx.environment import BuildEnvironment + from sphinx.ext.duration import DurationDomain + from sphinx.ext.todo import TodoDomain + + +class _DomainsContainer: + """Container for domain instances. + + This class is private, including its name, constructor, and all methods. + Any or all of these will change without notice or warning in any release. + + The public interface is restricted to: + + * the``domains.['']`` mapping interface + * the ``domains.`` attributes for core domains. + * the `.get()``, ``.keys()``, ``.items()``, and ``.values()`` methods. + + Additionally, this class supports ``iter`` and ``len``, + and provides membership testing via the ``in`` operator. + """ + + __slots__ = ( + '_domain_instances', + 'c_domain', + 'changeset_domain', + 'citation_domain', + 'cpp_domain', + 'index_domain', + 'javascript_domain', + 'math_domain', + 'python_domain', + 'restructuredtext_domain', + 'standard_domain', + ) + + #: First-party domains in :mod:`sphinx.domains` + _core_domains: Final = frozenset({ + 'std', + # Language-specific domains + 'c', + 'cpp', + 'js', + 'py', + 'rst', + # Other core domains + 'changeset', + 'citation', + 'index', + 'math', + }) + + @classmethod + def _from_environment(cls, env: BuildEnvironment, /) -> Self: + create_domains = env.app.registry.create_domains + # Initialise domains + if domains := {domain.name: domain for domain in create_domains(env)}: + return cls(**domains) # type: ignore[arg-type] + + return cls._from_environment_default(env=env) + + @classmethod + def _from_environment_default(cls, *, env: BuildEnvironment) -> Self: + """Return a default instance with every domain we require.""" + from sphinx.domains.c import CDomain + from sphinx.domains.changeset import ChangeSetDomain + from sphinx.domains.citation import CitationDomain + from sphinx.domains.cpp import CPPDomain + from sphinx.domains.index import IndexDomain + from sphinx.domains.javascript import JavaScriptDomain + from sphinx.domains.math import MathDomain + from sphinx.domains.python import PythonDomain + from sphinx.domains.rst import ReSTDomain + from sphinx.domains.std import StandardDomain + + return cls( + c=CDomain(env), + changeset=ChangeSetDomain(env), + citation=CitationDomain(env), + cpp=CPPDomain(env), + index=IndexDomain(env), + js=JavaScriptDomain(env), + math=MathDomain(env), + py=PythonDomain(env), + rst=ReSTDomain(env), + std=StandardDomain(env), + ) + + def __init__( + self, + *, + c: CDomain, + cpp: CPPDomain, + js: JavaScriptDomain, + py: PythonDomain, + rst: ReSTDomain, + std: StandardDomain, + changeset: ChangeSetDomain, + citation: CitationDomain, + index: IndexDomain, + math: MathDomain, + **domains: Domain, + ) -> None: + + # All domains, including core. + # Implemented as a dict for backwards compatibility. + self._domain_instances: Mapping[str, Domain] = { + 'c': c, + 'changeset': changeset, + 'citation': citation, + 'cpp': cpp, + 'index': index, + 'js': js, + 'math': math, + 'py': py, + 'rst': rst, + 'std': std, + **domains, + } + + # Provide typed attributes for the core domains + self.standard_domain: StandardDomain = std + self.c_domain: CDomain = c + self.cpp_domain: CPPDomain = cpp + self.javascript_domain: JavaScriptDomain = js + self.python_domain: PythonDomain = py + self.restructuredtext_domain: ReSTDomain = rst + self.changeset_domain: ChangeSetDomain = changeset + self.citation_domain: CitationDomain = citation + self.index_domain: IndexDomain = index + self.math_domain: MathDomain = math + + for domain_name, domain in self._domain_instances.items(): + # invariant from ``_DomainsContainer._from_environment`` + if domain_name != domain.name: + msg = f'Domain name mismatch in {domain!r}: {domain_name!r} != {domain.name!r}' + raise ValueError(msg) + + def _setup(self) -> None: + for domain in self._domain_instances.values(): + domain.setup() + + def _process_doc( + self, env: BuildEnvironment, docname: str, document: nodes.document + ) -> None: + for domain in self._domain_instances.values(): + domain.process_doc(env, docname, document) + + def _clear_doc(self, docname: str) -> None: + for domain in self._domain_instances.values(): + domain.clear_doc(docname) + + def _merge_domain_data(self, docnames: Set[str], domain_data: dict[str, Any]) -> None: + for domain_name, domain in self._domain_instances.items(): + domain.merge_domaindata(docnames, domain_data[domain_name]) + + def _check_consistency(self) -> None: + for domain in self._domain_instances.values(): + domain.check_consistency() + + def __contains__(self, key: str) -> bool: + return key in self._domain_instances + + def __eq__(self, other: object) -> bool: + if not isinstance(other, _DomainsContainer): + return NotImplemented + return self._domain_instances == other._domain_instances + + def __setattr__(self, key: str, value: object) -> None: + if key in self._core_domains: + msg = f'{self.__class__.__name__!r} object does not support assignment to {key!r}' + raise TypeError(msg) + super().__setattr__(key, value) + + def __delattr__(self, key: str) -> None: + if key in self._core_domains: + msg = f'{self.__class__.__name__!r} object does not support deletion of {key!r}' + raise TypeError(msg) + super().__delattr__(key) + + # Mapping interface: builtin domains + + @overload + def __getitem__(self, key: Literal["c"]) -> CDomain: + ... + + @overload + def __getitem__(self, key: Literal["cpp"]) -> CPPDomain: + ... + + @overload + def __getitem__(self, key: Literal["changeset"]) -> ChangeSetDomain: + ... + + @overload + def __getitem__(self, key: Literal["citation"]) -> CitationDomain: + ... + + @overload + def __getitem__(self, key: Literal["index"]) -> IndexDomain: + ... + + @overload + def __getitem__(self, key: Literal["js"]) -> JavaScriptDomain: + ... + + @overload + def __getitem__(self, key: Literal["math"]) -> MathDomain: + ... + + @overload + def __getitem__(self, key: Literal["py"]) -> PythonDomain: + ... + + @overload + def __getitem__(self, key: Literal["rst"]) -> ReSTDomain: + ... + + @overload + def __getitem__(self, key: Literal["std"]) -> StandardDomain: + ... + + # Mapping interface: first-party domains + + @overload + def __getitem__(self, key: Literal["duration"]) -> DurationDomain: + ... + + @overload + def __getitem__(self, key: Literal["todo"]) -> TodoDomain: + ... + + # Mapping interface: third-party domains + + @overload + def __getitem__(self, key: str) -> Domain: + ... + + def __getitem__(self, key: str) -> Domain: + if domain := getattr(self, key, None): + return domain + return self._domain_instances[key] + + def __setitem__(self, key: str, value: Domain) -> NoReturn: + msg = f'{self.__class__.__name__!r} object does not support item assignment' + raise TypeError(msg) + + def __delitem__(self, key: str) -> NoReturn: + msg = f'{self.__class__.__name__!r} object does not support item deletion' + raise TypeError(msg) + + def __iter__(self) -> Iterator[str]: + return iter(self._domain_instances.keys()) + + def __len__(self) -> int: + return len(self._domain_instances) + + def get(self, key: str, default: Domain | None = None) -> Domain | None: + return self._domain_instances.get(key, default) + + def keys(self) -> Iterable[str]: + return self._domain_instances.keys() + + def items(self) -> Iterable[tuple[str, Domain]]: + return self._domain_instances.items() + + def values(self) -> Iterable[Domain]: + return self._domain_instances.values() + + def sorted(self) -> Iterable[Domain]: + for _domain_name, domain in sorted(self._domain_instances.items()): + yield domain diff --git a/sphinx/domains/c/__init__.py b/sphinx/domains/c/__init__.py index c16432b7f9a..b6ae757e435 100644 --- a/sphinx/domains/c/__init__.py +++ b/sphinx/domains/c/__init__.py @@ -33,7 +33,7 @@ from sphinx.util.nodes import make_refnode if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Iterator, Set from docutils.nodes import Element, Node, TextElement, system_message @@ -526,7 +526,7 @@ def apply(self, **kwargs: Any) -> None: node.replace_self(signode) continue - rootSymbol: Symbol = self.env.domains['c'].data['root_symbol'] + rootSymbol: Symbol = self.env.domains.c_domain.data['root_symbol'] parentSymbol: Symbol | None = rootSymbol.direct_lookup(parentKey) if not parentSymbol: logger.debug("Target: %s", sig) @@ -745,7 +745,7 @@ def process_doc(self, env: BuildEnvironment, docname: str, def process_field_xref(self, pnode: pending_xref) -> None: pnode.attributes.update(self.env.ref_context) - def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: if Symbol.debug_show_tree: logger.debug("merge_domaindata:") logger.debug("\tself:") diff --git a/sphinx/domains/changeset.py b/sphinx/domains/changeset.py index cc1d4a33824..a27c5f571a8 100644 --- a/sphinx/domains/changeset.py +++ b/sphinx/domains/changeset.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, cast +from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple from docutils import nodes @@ -12,6 +12,8 @@ from sphinx.util.docutils import SphinxDirective if TYPE_CHECKING: + from collections.abc import Set + from docutils.nodes import Node from sphinx.application import Sphinx @@ -96,7 +98,7 @@ def run(self) -> list[Node]: translatable=False) node.append(para) - domain = cast(ChangeSetDomain, self.env.get_domain('changeset')) + domain = self.env.domains.changeset_domain domain.note_changeset(node) ret: list[Node] = [node] @@ -132,7 +134,7 @@ def clear_doc(self, docname: str) -> None: if changeset.docname == docname: changes.remove(changeset) - def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: # XXX duplicates? for version, otherchanges in otherdata['changes'].items(): changes = self.changesets.setdefault(version, []) diff --git a/sphinx/domains/citation.py b/sphinx/domains/citation.py index 4f00feb81e7..58d55774f48 100644 --- a/sphinx/domains/citation.py +++ b/sphinx/domains/citation.py @@ -14,6 +14,8 @@ from sphinx.util.nodes import copy_source_info, make_refnode if TYPE_CHECKING: + from collections.abc import Set + from docutils.nodes import Element from sphinx.application import Sphinx @@ -53,7 +55,7 @@ def clear_doc(self, docname: str) -> None: elif docname in docnames: docnames.remove(docname) - def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: # XXX duplicates? for key, data in otherdata['citations'].items(): if data[0] in docnames: @@ -108,7 +110,7 @@ class CitationDefinitionTransform(SphinxTransform): default_priority = 619 def apply(self, **kwargs: Any) -> None: - domain = cast(CitationDomain, self.env.get_domain('citation')) + domain = self.env.domains.citation_domain for node in self.document.findall(nodes.citation): # register citation node to domain node['docname'] = self.env.docname @@ -128,7 +130,7 @@ class CitationReferenceTransform(SphinxTransform): default_priority = 619 def apply(self, **kwargs: Any) -> None: - domain = cast(CitationDomain, self.env.get_domain('citation')) + domain = self.env.domains.citation_domain for node in self.document.findall(nodes.citation_reference): target = node.astext() ref = pending_xref(target, refdomain='citation', reftype='ref', diff --git a/sphinx/domains/cpp/__init__.py b/sphinx/domains/cpp/__init__.py index 71ced98e003..a043958c857 100644 --- a/sphinx/domains/cpp/__init__.py +++ b/sphinx/domains/cpp/__init__.py @@ -37,7 +37,7 @@ from sphinx.util.nodes import make_refnode if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Iterator, Set from docutils.nodes import Element, Node, TextElement, system_message @@ -661,7 +661,7 @@ def apply(self, **kwargs: Any) -> None: node.replace_self(signode) continue - rootSymbol: Symbol = self.env.domains['cpp'].data['root_symbol'] + rootSymbol: Symbol = self.env.domains.cpp_domain.data['root_symbol'] parentSymbol: Symbol = rootSymbol.direct_lookup(parentKey) if not parentSymbol: logger.debug("Target: %s", sig) @@ -933,7 +933,7 @@ def process_doc(self, env: BuildEnvironment, docname: str, def process_field_xref(self, pnode: pending_xref) -> None: pnode.attributes.update(self.env.ref_context) - def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: if Symbol.debug_show_tree: logger.debug("merge_domaindata:") logger.debug("\tself:") diff --git a/sphinx/domains/index.py b/sphinx/domains/index.py index 67cc4050a36..b31dfd488cc 100644 --- a/sphinx/domains/index.py +++ b/sphinx/domains/index.py @@ -15,7 +15,7 @@ from sphinx.util.nodes import process_index_entry if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Set from docutils.nodes import Node, system_message @@ -40,7 +40,7 @@ def entries(self) -> dict[str, list[tuple[str, str, str, str, str | None]]]: def clear_doc(self, docname: str) -> None: self.entries.pop(docname, None) - def merge_domaindata(self, docnames: Iterable[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: for docname in docnames: self.entries[docname] = otherdata['entries'][docname] diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index 6cfc274bed8..c5c0cdc3375 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -3,7 +3,7 @@ from __future__ import annotations import contextlib -from typing import TYPE_CHECKING, Any, ClassVar, cast +from typing import TYPE_CHECKING, Any, ClassVar from docutils import nodes from docutils.parsers.rst import directives @@ -20,7 +20,7 @@ from sphinx.util.nodes import make_id, make_refnode if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Iterator, Set from docutils.nodes import Element, Node @@ -150,7 +150,7 @@ def add_target_and_index(self, name_obj: tuple[str, str], sig: str, signode['ids'].append(node_id) self.state.document.note_explicit_target(signode) - domain = cast(JavaScriptDomain, self.env.get_domain('js')) + domain = self.env.domains.javascript_domain domain.note_object(fullname, self.objtype, node_id, location=signode) if 'no-index-entry' not in self.options: @@ -321,7 +321,7 @@ def run(self) -> list[Node]: ret: list[Node] = [] if not no_index: - domain = cast(JavaScriptDomain, self.env.get_domain('js')) + domain = self.env.domains.javascript_domain node_id = make_id(self.env, self.state.document, 'module', mod_name) domain.note_module(mod_name, node_id) @@ -423,7 +423,7 @@ def clear_doc(self, docname: str) -> None: if pkg_docname == docname: del self.modules[modname] - def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: # XXX check duplicates for fullname, (fn, node_id, objtype) in otherdata['objects'].items(): if fn in docnames: diff --git a/sphinx/domains/math.py b/sphinx/domains/math.py index afa36062d6b..9e6db18ddd9 100644 --- a/sphinx/domains/math.py +++ b/sphinx/domains/math.py @@ -14,7 +14,7 @@ from sphinx.util.nodes import make_refnode if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Iterable, Set from sphinx.addnodes import pending_xref from sphinx.application import Sphinx @@ -85,7 +85,7 @@ def clear_doc(self, docname: str) -> None: self.data['has_equations'].pop(docname, None) - def merge_domaindata(self, docnames: Iterable[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: for labelid, (doc, eqno) in otherdata['objects'].items(): if doc in docnames: self.equations[labelid] = (doc, eqno) diff --git a/sphinx/domains/python/__init__.py b/sphinx/domains/python/__init__.py index b199e004c58..d84a87f97f3 100644 --- a/sphinx/domains/python/__init__.py +++ b/sphinx/domains/python/__init__.py @@ -25,7 +25,7 @@ ) if TYPE_CHECKING: - from collections.abc import Iterable, Iterator + from collections.abc import Iterable, Iterator, Set from docutils.nodes import Element, Node @@ -455,7 +455,7 @@ def run(self) -> list[Node]: if 'no-index' not in self.options and 'noindex' in self.options: self.options['no-index'] = self.options['noindex'] - domain = cast(PythonDomain, self.env.get_domain('py')) + domain = self.env.domains.python_domain modname = self.arguments[0].strip() no_index = 'no-index' in self.options @@ -721,7 +721,7 @@ def clear_doc(self, docname: str) -> None: if mod.docname == docname: del self.modules[modname] - def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: # XXX check duplicates? for fullname, obj in otherdata['objects'].items(): if obj.docname in docnames: diff --git a/sphinx/domains/python/_object.py b/sphinx/domains/python/_object.py index bdd1fdc970c..ceb7f8bf83b 100644 --- a/sphinx/domains/python/_object.py +++ b/sphinx/domains/python/_object.py @@ -337,7 +337,7 @@ def add_target_and_index(self, name_cls: tuple[str, str], sig: str, signode['ids'].append(node_id) self.state.document.note_explicit_target(signode) - domain = self.env.domains['py'] + domain = self.env.domains.python_domain domain.note_object(fullname, self.objtype, node_id, location=signode) canonical_name = self.options.get('canonical') diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index 99d995d48ac..9eec281f3e6 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -3,7 +3,7 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any, ClassVar, cast +from typing import TYPE_CHECKING, Any, ClassVar from docutils.parsers.rst import directives @@ -16,7 +16,7 @@ from sphinx.util.nodes import make_id, make_refnode if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Iterator, Set from docutils.nodes import Element @@ -51,7 +51,7 @@ def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> signode['ids'].append(node_id) self.state.document.note_explicit_target(signode) - domain = cast(ReSTDomain, self.env.get_domain('rst')) + domain = self.env.domains.restructuredtext_domain domain.note_object(self.objtype, name, node_id, location=signode) if 'no-index-entry' not in self.options: @@ -164,7 +164,7 @@ def handle_signature(self, sig: str, signode: desc_signature) -> str: return name def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: - domain = cast(ReSTDomain, self.env.get_domain('rst')) + domain = self.env.domains.restructuredtext_domain directive_name = self.current_directive if directive_name: @@ -254,7 +254,7 @@ def clear_doc(self, docname: str) -> None: if doc == docname: del self.objects[typ, name] - def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: # XXX check duplicates for (typ, name), (doc, node_id) in otherdata['objects'].items(): if doc in docnames: diff --git a/sphinx/domains/std/__init__.py b/sphinx/domains/std/__init__.py index 882a8a7bf1b..1953f7c3a04 100644 --- a/sphinx/domains/std/__init__.py +++ b/sphinx/domains/std/__init__.py @@ -23,7 +23,7 @@ from sphinx.util.parsing import nested_parse_to_nodes if TYPE_CHECKING: - from collections.abc import Callable, Iterable, Iterator + from collections.abc import Callable, Iterable, Iterator, Set from sphinx.application import Sphinx from sphinx.builders import Builder @@ -78,7 +78,7 @@ def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> indexentry = self.indextemplate % (name,) self.indexnode['entries'].append((indextype, indexentry, node_id, '', None)) - std = cast(StandardDomain, self.env.get_domain('std')) + std = self.env.domains.standard_domain std.note_object(self.objtype, name, node_id, location=signode) @@ -140,7 +140,8 @@ def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> self.state.document.note_explicit_target(signode) index_entry = self.index_template % name self.indexnode['entries'].append(('pair', index_entry, node_id, '', None)) - self.env.domains['std'].note_object(self.objtype, name, node_id, location=signode) + domain = self.env.domains.standard_domain + domain.note_object(self.objtype, name, node_id, location=signode) def transform_content(self, content_node: addnodes.desc_content) -> None: """Insert *type* and *default* as a field list.""" @@ -211,7 +212,7 @@ def run(self) -> list[Node]: if ':' in self.name: _, name = self.name.split(':', 1) - std = cast(StandardDomain, self.env.get_domain('std')) + std = self.env.domains.standard_domain std.note_object(name, fullname, node_id, location=node) return ret @@ -294,7 +295,7 @@ def add_target_and_index(self, firstname: str, sig: str, signode: desc_signature self.state.document.note_explicit_target(signode) - domain = self.env.domains['std'] + domain = self.env.domains.standard_domain for optname in signode.get('allnames', []): domain.add_program_option(currprogram, optname, self.env.docname, signode['ids'][0]) @@ -365,8 +366,7 @@ def make_glossary_term(env: BuildEnvironment, textnodes: Iterable[Node], index_k term['ids'].append(node_id) document.note_explicit_target(term) - std = cast(StandardDomain, env.get_domain('std')) - std._note_term(termtext, node_id, location=term) + env.domains.standard_domain._note_term(termtext, node_id, location=term) # add an index entry too indexnode = addnodes.index() @@ -539,7 +539,7 @@ class ProductionList(SphinxDirective): option_spec: ClassVar[OptionSpec] = {} def run(self) -> list[Node]: - domain = cast(StandardDomain, self.env.get_domain('std')) + domain = self.env.domains.standard_domain node: Element = addnodes.productionlist() self.set_source_info(node) # The backslash handling is from ObjectDescription.get_signatures @@ -766,7 +766,7 @@ def clear_doc(self, docname: str) -> None: if fn == docname: del self.anonlabels[key] - def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: # XXX duplicates? for key, data in otherdata['progoptions'].items(): if data[0] in docnames: diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 67087e61829..6ae9435c5b1 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -8,9 +8,10 @@ from collections import defaultdict from copy import copy from os import path -from typing import TYPE_CHECKING, Any, NoReturn +from typing import TYPE_CHECKING from sphinx import addnodes +from sphinx.domains._domains_container import _DomainsContainer from sphinx.environment.adapters import toctree as toctree_adapters from sphinx.errors import ( BuildEnvironmentError, @@ -29,8 +30,9 @@ from sphinx.util.osutil import _last_modified_time, canon_path, os_path if TYPE_CHECKING: - from collections.abc import Callable, Iterator + from collections.abc import Callable, Iterable, Iterator from pathlib import Path + from typing import Any, Literal from docutils import nodes from docutils.nodes import Node @@ -66,7 +68,7 @@ # This is increased every time an environment attribute is added # or changed to properly invalidate pickle files. -ENV_VERSION = 63 +ENV_VERSION = 64 # config status CONFIG_UNSET = -1 @@ -87,61 +89,6 @@ 'text': is_translatable, } -if TYPE_CHECKING: - from collections.abc import MutableMapping - from typing import Literal, overload - - from sphinx.domains.c import CDomain - from sphinx.domains.changeset import ChangeSetDomain - from sphinx.domains.citation import CitationDomain - from sphinx.domains.cpp import CPPDomain - from sphinx.domains.index import IndexDomain - from sphinx.domains.javascript import JavaScriptDomain - from sphinx.domains.math import MathDomain - from sphinx.domains.python import PythonDomain - from sphinx.domains.rst import ReSTDomain - from sphinx.domains.std import StandardDomain - from sphinx.ext.duration import DurationDomain - from sphinx.ext.todo import TodoDomain - - class _DomainsType(MutableMapping[str, Domain]): - @overload - def __getitem__(self, key: Literal["c"]) -> CDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: Literal["cpp"]) -> CPPDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: Literal["changeset"]) -> ChangeSetDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: Literal["citation"]) -> CitationDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: Literal["index"]) -> IndexDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: Literal["js"]) -> JavaScriptDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: Literal["math"]) -> MathDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: Literal["py"]) -> PythonDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: Literal["rst"]) -> ReSTDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: Literal["std"]) -> StandardDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: Literal["duration"]) -> DurationDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: Literal["todo"]) -> TodoDomain: ... # NoQA: E704 - @overload - def __getitem__(self, key: str) -> Domain: ... # NoQA: E704 - def __getitem__(self, _key: str) -> Domain: raise NotImplementedError # NoQA: E704 - def __setitem__( # NoQA: E301,E704 - self, key: str, value: Domain, - ) -> NoReturn: raise NotImplementedError - def __delitem__(self, key: str) -> NoReturn: raise NotImplementedError # NoQA: E704 - def __iter__(self) -> NoReturn: raise NotImplementedError # NoQA: E704 - def __len__(self) -> NoReturn: raise NotImplementedError # NoQA: E704 - -else: - _DomainsType = dict - class BuildEnvironment: """ @@ -150,8 +97,6 @@ class BuildEnvironment: transformations to resolve links to them. """ - domains: _DomainsType - # --------- ENVIRONMENT INITIALIZATION ------------------------------------- def __init__(self, app: Sphinx) -> None: @@ -169,9 +114,6 @@ def __init__(self, app: Sphinx) -> None: self.versioning_condition: Literal[False] | Callable[[Node], bool] | None = None self.versioning_compare: bool | None = None - # all the registered domains, set by the application - self.domains = _DomainsType() - # the docutils settings for building self.settings: dict[str, Any] = default_settings.copy() self.settings['env'] = self @@ -276,6 +218,9 @@ def __init__(self, app: Sphinx) -> None: # objtype index -> (domain, type, objname (localized)) self._search_index_objnames: dict[int, tuple[str, str, str]] = {} + # all the registered domains, set by the application + self.domains: _DomainsContainer = _DomainsContainer._from_environment(self) + # set up environment self.setup(app) @@ -283,7 +228,7 @@ def __getstate__(self) -> dict[str, Any]: """Obtains serializable data for pickling.""" __dict__ = self.__dict__.copy() # clear unpickable attributes - __dict__.update(app=None, domains={}, events=None) + __dict__.update(app=None, domains=None, events=None) # clear in-memory doctree caches, to reduce memory consumption and # ensure that, upon restoring the state, the most recent pickled files # on the disk are used instead of those from a possibly outdated state @@ -310,14 +255,12 @@ def setup(self, app: Sphinx) -> None: self.project = app.project self.version = app.registry.get_envversion(app) - # initialize domains - self.domains = _DomainsType() - for domain in app.registry.create_domains(self): - self.domains[domain.name] = domain - + # initialise domains + if self.domains is None: + # if we are unpickling an environment, we need to recreate the domains + self.domains = _DomainsContainer._from_environment(self) # setup domains (must do after all initialization) - for domain in self.domains.values(): - domain.setup() + self.domains._setup() # initialize config self._update_config(app.config) @@ -392,25 +335,23 @@ def clear_doc(self, docname: str) -> None: self.included.pop(docname, None) self.reread_always.discard(docname) - for domain in self.domains.values(): - domain.clear_doc(docname) + self.domains._clear_doc(docname) - def merge_info_from(self, docnames: list[str], other: BuildEnvironment, + def merge_info_from(self, docnames: Iterable[str], other: BuildEnvironment, app: Sphinx) -> None: """Merge global information gathered about *docnames* while reading them from the *other* environment. This possibly comes from a parallel build process. """ - docnames = set(docnames) # type: ignore[assignment] + docnames = frozenset(docnames) for docname in docnames: self.all_docs[docname] = other.all_docs[docname] self.included[docname] = other.included[docname] if docname in other.reread_always: self.reread_always.add(docname) - for domainname, domain in self.domains.items(): - domain.merge_domaindata(docnames, other.domaindata[domainname]) + self.domains._merge_domain_data(docnames, other.domaindata) self.events.emit('env-merge-info', self, docnames, other) def path2doc(self, filename: str | os.PathLike[str]) -> str | None: @@ -563,8 +504,7 @@ def prepare_settings(self, docname: str) -> None: self.temp_data['docname'] = docname # defaults to the global default, but can be re-set in a document self.temp_data['default_role'] = self.config.default_role - self.temp_data['default_domain'] = \ - self.domains.get(self.config.primary_domain) + self.temp_data['default_domain'] = self.domains.get(self.config.primary_domain) # utilities to use while reading a document @@ -622,7 +562,8 @@ def get_domain(self, domainname: str) -> Domain: try: return self.domains[domainname] except KeyError as exc: - raise ExtensionError(__('Domain %r is not registered') % domainname) from exc + msg = __('Domain %r is not registered') % domainname + raise ExtensionError(msg) from exc # --------- RESOLVING REFERENCES AND TOCTREES ------------------------------ @@ -760,8 +701,7 @@ def check_consistency(self) -> None: location=docname) # call check-consistency for all extensions - for domain in self.domains.values(): - domain.check_consistency() + self.domains._check_consistency() self.events.emit('env-check-consistency', self) diff --git a/sphinx/environment/adapters/indexentries.py b/sphinx/environment/adapters/indexentries.py index 64788f51a26..fbceeb56b0c 100644 --- a/sphinx/environment/adapters/indexentries.py +++ b/sphinx/environment/adapters/indexentries.py @@ -65,7 +65,7 @@ def create_index( new: _IndexEntryMap = {} rel_uri: str | Literal[False] - index_domain = self.env.domains['index'] + index_domain = self.env.domains.index_domain for docname, entries in index_domain.entries.items(): try: rel_uri = builder.get_relative_uri('genindex', docname) diff --git a/sphinx/environment/collectors/toctree.py b/sphinx/environment/collectors/toctree.py index e3f80f25155..f004fc6820a 100644 --- a/sphinx/environment/collectors/toctree.py +++ b/sphinx/environment/collectors/toctree.py @@ -7,6 +7,7 @@ from docutils import nodes from sphinx import addnodes +from sphinx.domains.std import StandardDomain from sphinx.environment.adapters.toctree import note_toctree from sphinx.environment.collectors import EnvironmentCollector from sphinx.locale import __ @@ -260,7 +261,7 @@ def _walk_toctree(toctreenode: addnodes.toctree, depth: int) -> None: def assign_figure_numbers(self, env: BuildEnvironment) -> list[str]: """Assign a figure number to each figure under a numbered toctree.""" - generated_docnames = frozenset(env.domains['std']._virtual_doc_names) + generated_docnames = frozenset(env.domains.standard_domain._virtual_doc_names) rewrite_needed = [] @@ -270,10 +271,9 @@ def assign_figure_numbers(self, env: BuildEnvironment) -> list[str]: fignum_counter: dict[str, dict[tuple[int, ...], int]] = {} def get_figtype(node: Node) -> str | None: - for domain in env.domains.values(): + for domain in env.domains.sorted(): figtype = domain.get_enumerable_node_type(node) - if (domain.name == 'std' - and not domain.get_numfig_title(node)): # type: ignore[attr-defined] # NoQA: E501 + if isinstance(domain, StandardDomain) and not domain.get_numfig_title(node): # Skip if uncaptioned node continue diff --git a/sphinx/ext/autosectionlabel.py b/sphinx/ext/autosectionlabel.py index c1eb46bf04f..63885e7039f 100644 --- a/sphinx/ext/autosectionlabel.py +++ b/sphinx/ext/autosectionlabel.py @@ -7,7 +7,6 @@ from docutils import nodes import sphinx -from sphinx.domains.std import StandardDomain from sphinx.locale import __ from sphinx.util import logging from sphinx.util.nodes import clean_astext @@ -31,7 +30,7 @@ def get_node_depth(node: Node) -> int: def register_sections_as_label(app: Sphinx, document: Node) -> None: - domain = cast(StandardDomain, app.env.get_domain('std')) + domain = app.env.domains.standard_domain for node in document.findall(nodes.section): if (app.config.autosectionlabel_maxdepth and get_node_depth(node) >= app.config.autosectionlabel_maxdepth): diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 6ded1e9f939..a31911ba0f1 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -754,7 +754,7 @@ class AutoLink(SphinxRole): """ def run(self) -> tuple[list[Node], list[system_message]]: - pyobj_role = self.env.get_domain('py').role('obj') + pyobj_role = self.env.domains.python_domain.role('obj') assert pyobj_role is not None objects, errors = pyobj_role('obj', self.rawtext, self.text, self.lineno, self.inliner, self.options, self.content) diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index c075e954883..b287f6c8a85 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -205,7 +205,7 @@ def write(self, *ignored: Any) -> None: def build_c_coverage(self) -> None: c_objects = {} - for obj in self.env.domains['c'].get_objects(): + for obj in self.env.domains.c_domain.get_objects(): c_objects[obj[2]] = obj[1] for filename in self.c_sourcefiles: undoc: set[tuple[str, str]] = set() diff --git a/sphinx/ext/duration.py b/sphinx/ext/duration.py index 1053856e07e..6739f676ff9 100644 --- a/sphinx/ext/duration.py +++ b/sphinx/ext/duration.py @@ -5,7 +5,7 @@ import time from itertools import islice from operator import itemgetter -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING import sphinx from sphinx.domains import Domain @@ -13,6 +13,7 @@ from sphinx.util import logging if TYPE_CHECKING: + from collections.abc import Set from typing import TypedDict from docutils import nodes @@ -43,7 +44,7 @@ def clear(self) -> None: def clear_doc(self, docname: str) -> None: self.reading_durations.pop(docname, None) - def merge_domaindata(self, docnames: list[str], otherdata: _DurationDomainData) -> None: # type: ignore[override] + def merge_domaindata(self, docnames: Set[str], otherdata: _DurationDomainData) -> None: # type: ignore[override] other_reading_durations = otherdata.get('reading_durations', {}) docnames_set = frozenset(docnames) for docname, duration in other_reading_durations.items(): @@ -56,7 +57,7 @@ def on_builder_inited(app: Sphinx) -> None: This clears the results of the last build. """ - domain = cast(DurationDomain, app.env.get_domain('duration')) + domain = app.env.domains['duration'] domain.clear() @@ -69,13 +70,13 @@ def on_doctree_read(app: Sphinx, doctree: nodes.document) -> None: """Record a reading duration.""" started_at = app.env.temp_data['started_at'] duration = time.monotonic() - started_at - domain = cast(DurationDomain, app.env.get_domain('duration')) + domain = app.env.domains['duration'] domain.note_reading_duration(duration) def on_build_finished(app: Sphinx, error: Exception) -> None: """Display duration ranking on the current build.""" - domain = cast(DurationDomain, app.env.get_domain('duration')) + domain = app.env.domains['duration'] if not domain.reading_durations: return durations = sorted(domain.reading_durations.items(), key=itemgetter(1), reverse=True) diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index 7f8d9c9c6f8..21dda6fe65b 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -359,7 +359,7 @@ def run(self) -> list[Node]: node = inheritance_diagram() node.document = self.state.document class_names = self.arguments[0].split() - class_role = self.env.get_domain('py').role('class') + class_role = self.env.domains.python_domain.role('class') # Store the original content for use as a hash node['parts'] = self.options.get('parts', 0) node['content'] = ', '.join(class_names) diff --git a/sphinx/ext/intersphinx/_resolve.py b/sphinx/ext/intersphinx/_resolve.py index 35a8c12bc7d..a816a94f257 100644 --- a/sphinx/ext/intersphinx/_resolve.py +++ b/sphinx/ext/intersphinx/_resolve.py @@ -162,8 +162,8 @@ def _resolve_reference(env: BuildEnvironment, typ = node['reftype'] if typ == 'any': - for domain_name, domain in env.domains.items(): - if honor_disabled_refs and f'{domain_name}:*' in intersphinx_disabled_reftypes: + for domain in env.domains.sorted(): + if honor_disabled_refs and f'{domain.name}:*' in intersphinx_disabled_reftypes: continue objtypes: Iterable[str] = domain.object_types.keys() res = _resolve_reference_in_domain(env, inv_name, inventory, @@ -302,16 +302,17 @@ def run(self) -> tuple[list[Node], list[system_message]]: if domain_name is not None: # the user specified a domain, so we only check that - if (domain := self.env.domains.get(domain_name)) is None: + if domain_name not in self.env.domains: self._emit_warning( __('domain for external cross-reference not found: %r'), domain_name ) return [], [] - if (role_func := domain.roles.get(role_name)) is None: + domain = self.env.domains[domain_name] + role_func = domain.roles.get(role_name) + if role_func is None: msg = 'role for external cross-reference not found in domain %r: %r' - if ( - object_types := domain.object_types.get(role_name) - ) is not None and object_types.roles: + object_types = domain.object_types.get(role_name) + if object_types is not None and object_types.roles: self._emit_warning( __(f'{msg} (perhaps you meant one of: %s)'), domain_name, @@ -329,7 +330,7 @@ def run(self) -> tuple[list[Node], list[system_message]]: if default_domain := self.env.temp_data.get('default_domain'): domains.append(default_domain) if ( - std_domain := self.env.domains.get('std') + std_domain := self.env.domains.standard_domain ) is not None and std_domain not in domains: domains.append(std_domain) diff --git a/sphinx/ext/mathjax.py b/sphinx/ext/mathjax.py index f67f87fed20..ba8cd03e945 100644 --- a/sphinx/ext/mathjax.py +++ b/sphinx/ext/mathjax.py @@ -14,7 +14,6 @@ import sphinx from sphinx.builders.html import StandaloneHTMLBuilder -from sphinx.domains.math import MathDomain from sphinx.errors import ExtensionError from sphinx.locale import _ from sphinx.util.math import get_node_equation_number @@ -82,7 +81,7 @@ def install_mathjax(app: Sphinx, pagename: str, templatename: str, context: dict msg = 'mathjax_path config value must be set for the mathjax extension to work' raise ExtensionError(msg) - domain = cast(MathDomain, app.env.get_domain('math')) + domain = app.env.domains.math_domain builder = cast(StandaloneHTMLBuilder, app.builder) if app.registry.html_assets_policy == 'always' or domain.has_equations(pagename): # Enable mathjax only if equations exists diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py index ac8e17fd566..0945731702a 100644 --- a/sphinx/ext/todo.py +++ b/sphinx/ext/todo.py @@ -24,6 +24,8 @@ from sphinx.util.docutils import SphinxDirective, new_document if TYPE_CHECKING: + from collections.abc import Set + from docutils.nodes import Element, Node from sphinx.application import Sphinx @@ -87,7 +89,7 @@ def todos(self) -> dict[str, list[todo_node]]: def clear_doc(self, docname: str) -> None: self.todos.pop(docname, None) - def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: for docname in docnames: self.todos[docname] = otherdata['todos'][docname] @@ -125,7 +127,7 @@ def __init__(self, app: Sphinx, doctree: nodes.document, docname: str) -> None: self.builder = app.builder self.config = app.config self.env = app.env - self.domain = cast(TodoDomain, app.env.get_domain('todo')) + self.domain = app.env.domains['todo'] self.document = new_document('') self.process(doctree, docname) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index cf73e377fd4..e56a00d7b8a 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -336,9 +336,9 @@ def get_objects(self, fn2index: dict[str, int] rv: dict[str, list[tuple[int, int, int, str, str]]] = {} otypes = self._objtypes onames = self._objnames - for domainname, domain in sorted(self.env.domains.items()): - for fullname, dispname, type, docname, anchor, prio in \ - sorted(domain.get_objects()): + for domain in self.env.domains.sorted(): + sorted_objects = sorted(domain.get_objects()) + for fullname, dispname, type, docname, anchor, prio in sorted_objects: if docname not in fn2index: continue if prio < 0: @@ -348,17 +348,17 @@ def get_objects(self, fn2index: dict[str, int] prefix, _, name = dispname.rpartition('.') plist = rv.setdefault(prefix, []) try: - typeindex = otypes[domainname, type] + typeindex = otypes[domain.name, type] except KeyError: typeindex = len(otypes) - otypes[domainname, type] = typeindex + otypes[domain.name, type] = typeindex otype = domain.object_types.get(type) if otype: # use str() to fire translation proxies - onames[typeindex] = (domainname, type, + onames[typeindex] = (domain.name, type, str(domain.get_type_name(otype))) else: - onames[typeindex] = (domainname, type, type) + onames[typeindex] = (domain.name, type, type) if anchor == fullname: shortanchor = '' elif anchor == type + '-' + fullname: diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index 9cf77d30439..f99442eb004 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -123,6 +123,8 @@ def __init__( # unknown keyword arguments **extras: Any, ) -> None: + self._builder_name = buildername + assert srcdir is not None if verbosity == -1: @@ -209,7 +211,7 @@ def cleanup(self, doctrees: bool = False) -> None: os.remove(self.docutils_conf_path) def __repr__(self) -> str: - return f'<{self.__class__.__name__} buildername={self.builder.name!r}>' + return f'<{self.__class__.__name__} buildername={self._builder_name!r}>' def build(self, force_all: bool = False, filenames: list[str] | None = None) -> None: self.env._pickled_doctree_cache.clear() diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 5fe133137fd..56ba6ab5726 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -208,7 +208,7 @@ class AutoNumbering(SphinxTransform): default_priority = 210 def apply(self, **kwargs: Any) -> None: - domain: StandardDomain = self.env.domains['std'] + domain: StandardDomain = self.env.domains.standard_domain for node in self.document.findall(nodes.Element): if (domain.is_enumerable_node(node) and diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index 357cf61488d..82de90a6e3f 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -122,7 +122,7 @@ def resolve_anyref( self, refdoc: str, node: pending_xref, contnode: Element, ) -> Element | None: """Resolve reference generated by the "any" role.""" - stddomain = self.env.get_domain('std') + stddomain = self.env.domains.standard_domain target = node['reftarget'] results: list[tuple[str, Element]] = [] # first, try resolving as :doc: @@ -133,7 +133,7 @@ def resolve_anyref( # next, do the standard domain (makes this a priority) results.extend(stddomain.resolve_any_xref(self.env, refdoc, self.app.builder, target, node, contnode)) - for domain in self.env.domains.values(): + for domain in self.env.domains.sorted(): if domain.name == 'std': continue # we did this one already try: diff --git a/sphinx/transforms/references.py b/sphinx/transforms/references.py index 6f935aa2afb..15afd92d0de 100644 --- a/sphinx/transforms/references.py +++ b/sphinx/transforms/references.py @@ -34,8 +34,7 @@ class SphinxDomains(SphinxTransform): default_priority = 850 def apply(self, **kwargs: Any) -> None: - for domain in self.env.domains.values(): - domain.process_doc(self.env, self.env.docname, self.document) + self.env.domains._process_doc(self.env, self.env.docname, self.document) def setup(app: Sphinx) -> ExtensionMetadata: diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index 269f3794a33..7ee8a82dffa 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -268,7 +268,7 @@ def lookup_domain_element(self, type: str, name: str) -> Any: return element, [] # always look in the std domain - element = getattr(self.env.get_domain('std'), type)(name) + element = getattr(self.env.domains.standard_domain, type)(name) if element is not None: return element, [] diff --git a/sphinx/util/inventory.py b/sphinx/util/inventory.py index 8ef3831e280..909d14983c5 100644 --- a/sphinx/util/inventory.py +++ b/sphinx/util/inventory.py @@ -194,18 +194,18 @@ def escape(string: str) -> str: # body compressor = zlib.compressobj(9) - for domainname, domain in sorted(env.domains.items()): - for name, dispname, typ, docname, anchor, prio in \ - sorted(domain.get_objects()): - if anchor.endswith(name): + for domain in env.domains.sorted(): + sorted_objects = sorted(domain.get_objects()) + for fullname, dispname, type, docname, anchor, prio in sorted_objects: + if anchor.endswith(fullname): # this can shorten the inventory by as much as 25% - anchor = anchor[:-len(name)] + '$' + anchor = anchor.removesuffix(fullname) + '$' uri = builder.get_target_uri(docname) if anchor: uri += '#' + anchor - if dispname == name: + if dispname == fullname: dispname = '-' entry = ('%s %s:%s %s %s %s\n' % - (name, domainname, typ, prio, uri, dispname)) + (fullname, domain.name, type, prio, uri, dispname)) f.write(compressor.compress(entry.encode())) f.write(compressor.flush()) diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index 2d6338d8c6e..2c13f16660e 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -402,7 +402,7 @@ def append_fignumber(figtype: str, figure_id: str) -> None: self.body.append(prefix % '.'.join(map(str, numbers)) + ' ') self.body.append('') - figtype = self.builder.env.domains['std'].get_enumerable_node_type(node) + figtype = self.builder.env.domains.standard_domain.get_enumerable_node_type(node) if figtype: if len(node['ids']) == 0: msg = __('Any IDs not assigned for %s node') % node.tagname diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 00e8db942a2..44c1398206f 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -15,7 +15,6 @@ from docutils import nodes, writers from sphinx import addnodes, highlighting -from sphinx.domains.std import StandardDomain from sphinx.errors import SphinxError from sphinx.locale import _, __, admonitionlabels from sphinx.util import logging, texescape @@ -507,8 +506,7 @@ def generate(content: list[tuple[str, list[IndexEntry]]], collapsed: bool) -> No indices_config = frozenset(indices_config) else: check_names = False - for domain_name in sorted(self.builder.env.domains): - domain = self.builder.env.domains[domain_name] + for domain in self.builder.env.domains.sorted(): for index_cls in domain.indices: index_name = f'{domain.name}-{index_cls.name}' if check_names and index_name not in indices_config: @@ -1682,7 +1680,7 @@ def add_target(id: str) -> None: while isinstance(next_node, nodes.target): next_node = next_node.next_node(ascend=True) - domain = cast(StandardDomain, self.builder.env.get_domain('std')) + domain = self.builder.env.domains.standard_domain if isinstance(next_node, HYPERLINK_SUPPORT_NODES): return if domain.get_enumerable_node_type(next_node) and domain.get_numfig_title(next_node): diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index 953115e5d57..d9c1c89776d 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -11,7 +11,6 @@ from docutils import nodes, writers from sphinx import __display_version__, addnodes -from sphinx.domains.index import IndexDomain from sphinx.errors import ExtensionError from sphinx.locale import _, __, admonitionlabels from sphinx.util import logging @@ -482,8 +481,7 @@ def generate(content: list[tuple[str, list[IndexEntry]]], collapsed: bool) -> st indices_config = frozenset(indices_config) else: check_names = False - for domain_name in sorted(self.builder.env.domains): - domain = self.builder.env.domains[domain_name] + for domain in self.builder.env.domains.sorted(): for index_cls in domain.indices: index_name = f'{domain.name}-{index_cls.name}' if check_names and index_name not in indices_config: @@ -496,7 +494,7 @@ def generate(content: list[tuple[str, list[IndexEntry]]], collapsed: bool) -> st generate(content, collapsed), )) # only add the main Index if it's not empty - domain = cast(IndexDomain, self.builder.env.get_domain('index')) + domain = self.builder.env.domains.index_domain for docname in self.builder.docnames: if domain.entries[docname]: self.indices.append((_('Index'), '\n@printindex ge\n')) diff --git a/tests/js/fixtures/cpp/searchindex.js b/tests/js/fixtures/cpp/searchindex.js index 78e5f761485..989c877a8cc 100644 --- a/tests/js/fixtures/cpp/searchindex.js +++ b/tests/js/fixtures/cpp/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {}, "docnames": ["index"], "envversion": {"sphinx": 63, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {"sphinx (c++ class)": [[0, "_CPPv46Sphinx", false]]}, "objects": {"": [[0, 0, 1, "_CPPv46Sphinx", "Sphinx"]]}, "objnames": {"0": ["cpp", "class", "C++ class"]}, "objtypes": {"0": "cpp:class"}, "terms": {"The": 0, "becaus": 0, "c": 0, "can": 0, "cardin": 0, "challeng": 0, "charact": 0, "class": 0, "descript": 0, "drop": 0, "engin": 0, "fixtur": 0, "frequent": 0, "gener": 0, "i": 0, "index": 0, "inflat": 0, "mathemat": 0, "occur": 0, "often": 0, "project": 0, "punctuat": 0, "queri": 0, "relat": 0, "sampl": 0, "search": 0, "size": 0, "sphinx": 0, "term": 0, "thei": 0, "thi": 0, "token": 0, "us": 0, "web": 0, "would": 0}, "titles": ["<no title>"], "titleterms": {}}) \ No newline at end of file +Search.setIndex({"alltitles": {}, "docnames": ["index"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {"sphinx (c++ class)": [[0, "_CPPv46Sphinx", false]]}, "objects": {"": [[0, 0, 1, "_CPPv46Sphinx", "Sphinx"]]}, "objnames": {"0": ["cpp", "class", "C++ class"]}, "objtypes": {"0": "cpp:class"}, "terms": {"The": 0, "becaus": 0, "c": 0, "can": 0, "cardin": 0, "challeng": 0, "charact": 0, "class": 0, "descript": 0, "drop": 0, "engin": 0, "fixtur": 0, "frequent": 0, "gener": 0, "i": 0, "index": 0, "inflat": 0, "mathemat": 0, "occur": 0, "often": 0, "project": 0, "punctuat": 0, "queri": 0, "relat": 0, "sampl": 0, "search": 0, "size": 0, "sphinx": 0, "term": 0, "thei": 0, "thi": 0, "token": 0, "us": 0, "web": 0, "would": 0}, "titles": ["<no title>"], "titleterms": {}}) \ No newline at end of file diff --git a/tests/js/fixtures/multiterm/searchindex.js b/tests/js/fixtures/multiterm/searchindex.js index 96b093c5fda..c4e4a32e30a 100644 --- a/tests/js/fixtures/multiterm/searchindex.js +++ b/tests/js/fixtures/multiterm/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"Main Page": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 63, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"At": 0, "adjac": 0, "all": 0, "an": 0, "appear": 0, "applic": 0, "ar": 0, "built": 0, "can": 0, "check": 0, "contain": 0, "do": 0, "document": 0, "doesn": 0, "each": 0, "fixtur": 0, "format": 0, "function": 0, "futur": 0, "html": 0, "i": 0, "includ": 0, "match": 0, "messag": 0, "multipl": 0, "multiterm": 0, "order": 0, "other": 0, "output": 0, "perform": 0, "perhap": 0, "phrase": 0, "project": 0, "queri": 0, "requir": 0, "same": 0, "search": 0, "successfulli": 0, "support": 0, "t": 0, "term": 0, "test": 0, "thi": 0, "time": 0, "us": 0, "when": 0, "write": 0}, "titles": ["Main Page"], "titleterms": {"main": 0, "page": 0}}) \ No newline at end of file +Search.setIndex({"alltitles": {"Main Page": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"At": 0, "adjac": 0, "all": 0, "an": 0, "appear": 0, "applic": 0, "ar": 0, "built": 0, "can": 0, "check": 0, "contain": 0, "do": 0, "document": 0, "doesn": 0, "each": 0, "fixtur": 0, "format": 0, "function": 0, "futur": 0, "html": 0, "i": 0, "includ": 0, "match": 0, "messag": 0, "multipl": 0, "multiterm": 0, "order": 0, "other": 0, "output": 0, "perform": 0, "perhap": 0, "phrase": 0, "project": 0, "queri": 0, "requir": 0, "same": 0, "search": 0, "successfulli": 0, "support": 0, "t": 0, "term": 0, "test": 0, "thi": 0, "time": 0, "us": 0, "when": 0, "write": 0}, "titles": ["Main Page"], "titleterms": {"main": 0, "page": 0}}) \ No newline at end of file diff --git a/tests/js/fixtures/partial/searchindex.js b/tests/js/fixtures/partial/searchindex.js index b650c366e94..4925083169b 100644 --- a/tests/js/fixtures/partial/searchindex.js +++ b/tests/js/fixtures/partial/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"sphinx_utils module": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 63, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"ar": 0, "both": 0, "built": 0, "confirm": 0, "document": 0, "function": 0, "html": 0, "i": 0, "includ": 0, "input": 0, "javascript": 0, "match": 0, "partial": 0, "possibl": 0, "project": 0, "provid": 0, "restructuredtext": 0, "sampl": 0, "search": 0, "should": 0, "term": 0, "thi": 0, "titl": 0, "us": 0, "when": 0}, "titles": ["sphinx_utils module"], "titleterms": {"modul": 0, "sphinx_util": 0}}) \ No newline at end of file +Search.setIndex({"alltitles": {"sphinx_utils module": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"ar": 0, "both": 0, "built": 0, "confirm": 0, "document": 0, "function": 0, "html": 0, "i": 0, "includ": 0, "input": 0, "javascript": 0, "match": 0, "partial": 0, "possibl": 0, "project": 0, "provid": 0, "restructuredtext": 0, "sampl": 0, "search": 0, "should": 0, "term": 0, "thi": 0, "titl": 0, "us": 0, "when": 0}, "titles": ["sphinx_utils module"], "titleterms": {"modul": 0, "sphinx_util": 0}}) \ No newline at end of file diff --git a/tests/js/fixtures/titles/searchindex.js b/tests/js/fixtures/titles/searchindex.js index a084952248a..293b087149b 100644 --- a/tests/js/fixtures/titles/searchindex.js +++ b/tests/js/fixtures/titles/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"Main Page": [[0, null]], "Relevance": [[0, "relevance"], [1, null]], "Result Scoring": [[0, "result-scoring"]]}, "docnames": ["index", "relevance"], "envversion": {"sphinx": 63, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst", "relevance.rst"], "indexentries": {"example (class in relevance)": [[0, "relevance.Example", false]], "module": [[0, "module-relevance", false]], "relevance": [[0, "index-1", false], [0, "module-relevance", false]], "relevance (relevance.example attribute)": [[0, "relevance.Example.relevance", false]], "scoring": [[0, "index-0", true]]}, "objects": {"": [[0, 0, 0, "-", "relevance"]], "relevance": [[0, 1, 1, "", "Example"]], "relevance.Example": [[0, 2, 1, "", "relevance"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "attribute", "Python attribute"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:attribute"}, "terms": {"": [0, 1], "A": 1, "By": 0, "For": [0, 1], "In": [0, 1], "against": 0, "align": 0, "also": 1, "an": 0, "answer": 0, "appear": 1, "ar": 1, "area": 0, "ask": 0, "assign": 0, "attempt": 0, "attribut": 0, "both": 0, "built": 1, "can": [0, 1], "class": 0, "code": [0, 1], "collect": 0, "consid": 1, "contain": 0, "context": 0, "corpu": 1, "could": 1, "demonstr": 0, "describ": 1, "detail": 1, "determin": [0, 1], "docstr": 0, "document": [0, 1], "domain": 1, "dure": 0, "engin": 0, "evalu": 0, "exampl": [0, 1], "extract": 0, "feedback": 0, "find": 0, "found": 0, "from": 0, "function": 1, "ha": 1, "handl": 0, "happen": 1, "head": 0, "help": 0, "highli": [0, 1], "how": 0, "i": [0, 1], "improv": 0, "inform": 0, "intend": 0, "issu": [0, 1], "itself": 1, "knowledg": 0, "languag": 1, "less": 1, "like": [0, 1], "mani": 0, "match": 0, "mention": 1, "more": 0, "name": [0, 1], "numer": 0, "object": 0, "often": 0, "one": [0, 1], "onli": [0, 1], "order": 0, "other": 0, "over": 0, "page": 1, "part": 1, "particular": 0, "present": 0, "printf": 1, "program": 1, "project": 0, "queri": [0, 1], "question": 0, "re": 0, "rel": 0, "research": 0, "result": 1, "retriev": 0, "sai": 0, "same": 1, "search": [0, 1], "seem": 0, "softwar": 1, "some": 1, "sphinx": 0, "straightforward": 1, "subject": 0, "subsect": 0, "term": [0, 1], "test": 0, "text": 0, "than": [0, 1], "thei": 0, "them": 0, "thi": 0, "time": 0, "titl": 0, "two": 0, "typic": 0, "us": 0, "user": [0, 1], "we": [0, 1], "when": 0, "whether": 1, "which": 0, "within": 0, "word": 0, "would": [0, 1]}, "titles": ["Main Page", "Relevance"], "titleterms": {"main": 0, "page": 0, "relev": [0, 1], "result": 0, "score": 0}}) \ No newline at end of file +Search.setIndex({"alltitles": {"Main Page": [[0, null]], "Relevance": [[0, "relevance"], [1, null]], "Result Scoring": [[0, "result-scoring"]]}, "docnames": ["index", "relevance"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst", "relevance.rst"], "indexentries": {"example (class in relevance)": [[0, "relevance.Example", false]], "module": [[0, "module-relevance", false]], "relevance": [[0, "index-1", false], [0, "module-relevance", false]], "relevance (relevance.example attribute)": [[0, "relevance.Example.relevance", false]], "scoring": [[0, "index-0", true]]}, "objects": {"": [[0, 0, 0, "-", "relevance"]], "relevance": [[0, 1, 1, "", "Example"]], "relevance.Example": [[0, 2, 1, "", "relevance"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "attribute", "Python attribute"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:attribute"}, "terms": {"": [0, 1], "A": 1, "By": 0, "For": [0, 1], "In": [0, 1], "against": 0, "align": 0, "also": 1, "an": 0, "answer": 0, "appear": 1, "ar": 1, "area": 0, "ask": 0, "assign": 0, "attempt": 0, "attribut": 0, "both": 0, "built": 1, "can": [0, 1], "class": 0, "code": [0, 1], "collect": 0, "consid": 1, "contain": 0, "context": 0, "corpu": 1, "could": 1, "demonstr": 0, "describ": 1, "detail": 1, "determin": [0, 1], "docstr": 0, "document": [0, 1], "domain": 1, "dure": 0, "engin": 0, "evalu": 0, "exampl": [0, 1], "extract": 0, "feedback": 0, "find": 0, "found": 0, "from": 0, "function": 1, "ha": 1, "handl": 0, "happen": 1, "head": 0, "help": 0, "highli": [0, 1], "how": 0, "i": [0, 1], "improv": 0, "inform": 0, "intend": 0, "issu": [0, 1], "itself": 1, "knowledg": 0, "languag": 1, "less": 1, "like": [0, 1], "mani": 0, "match": 0, "mention": 1, "more": 0, "name": [0, 1], "numer": 0, "object": 0, "often": 0, "one": [0, 1], "onli": [0, 1], "order": 0, "other": 0, "over": 0, "page": 1, "part": 1, "particular": 0, "present": 0, "printf": 1, "program": 1, "project": 0, "queri": [0, 1], "question": 0, "re": 0, "rel": 0, "research": 0, "result": 1, "retriev": 0, "sai": 0, "same": 1, "search": [0, 1], "seem": 0, "softwar": 1, "some": 1, "sphinx": 0, "straightforward": 1, "subject": 0, "subsect": 0, "term": [0, 1], "test": 0, "text": 0, "than": [0, 1], "thei": 0, "them": 0, "thi": 0, "time": 0, "titl": 0, "two": 0, "typic": 0, "us": 0, "user": [0, 1], "we": [0, 1], "when": 0, "whether": 1, "which": 0, "within": 0, "word": 0, "would": [0, 1]}, "titles": ["Main Page", "Relevance"], "titleterms": {"main": 0, "page": 0, "relev": [0, 1], "result": 0, "score": 0}}) \ No newline at end of file diff --git a/tests/test_domains/test_domain_c.py b/tests/test_domains/test_domain_c.py index 77fe612eced..491c0844b69 100644 --- a/tests/test_domains/test_domain_c.py +++ b/tests/test_domains/test_domain_c.py @@ -804,7 +804,7 @@ def test_domain_c_build_field_role(app): def _get_obj(app, queryName): - domain = app.env.get_domain('c') + domain = app.env.domains.c_domain for name, _dispname, objectType, docname, anchor, _prio in domain.get_objects(): if name == queryName: return docname, anchor, objectType diff --git a/tests/test_domains/test_domain_js.py b/tests/test_domains/test_domain_js.py index 1976928df43..a51dd13be58 100644 --- a/tests/test_domains/test_domain_js.py +++ b/tests/test_domains/test_domain_js.py @@ -102,8 +102,8 @@ def assert_refnode(node, mod_name, prefix, target, reftype=None, domain='js'): def test_domain_js_objects(app): app.build(force_all=True) - modules = app.env.domains['js'].data['modules'] - objects = app.env.domains['js'].data['objects'] + modules = app.env.domains.javascript_domain.data['modules'] + objects = app.env.domains.javascript_domain.data['objects'] assert 'module_a.submodule' in modules assert 'module_a.submodule' in objects @@ -131,7 +131,7 @@ def test_domain_js_objects(app): @pytest.mark.sphinx('dummy', testroot='domain-js') def test_domain_js_find_obj(app): def find_obj(mod_name, prefix, obj_name, obj_type, searchmode=0): - return app.env.domains['js'].find_obj( + return app.env.domains.javascript_domain.find_obj( app.env, mod_name, prefix, obj_name, obj_type, searchmode ) diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py index 4df7081c627..74531d1097f 100644 --- a/tests/test_domains/test_domain_py.py +++ b/tests/test_domains/test_domain_py.py @@ -206,8 +206,8 @@ def test_domain_py_xrefs_abbreviations(app): def test_domain_py_objects(app): app.build(force_all=True) - modules = app.env.domains['py'].data['modules'] - objects = app.env.domains['py'].data['objects'] + modules = app.env.domains.python_domain.data['modules'] + objects = app.env.domains.python_domain.data['objects'] assert 'module_a.submodule' in modules assert 'module_a.submodule' in objects @@ -264,7 +264,7 @@ def test_resolve_xref_for_properties(app): @pytest.mark.sphinx('dummy', testroot='domain-py') def test_domain_py_find_obj(app): def find_obj(modname, prefix, obj_name, obj_type, searchmode=0): - return app.env.domains['py'].find_obj( + return app.env.domains.python_domain.find_obj( app.env, modname, prefix, obj_name, obj_type, searchmode ) @@ -569,7 +569,7 @@ def test_module_index(app): '.. py:module:: sphinx_intl\n' ) restructuredtext.parse(app, text) - index = PythonModuleIndex(app.env.get_domain('py')) + index = PythonModuleIndex(app.env.domains.python_domain) assert index.generate() == ( [ ('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), @@ -612,7 +612,7 @@ def test_module_index(app): def test_module_index_submodule(app): text = '.. py:module:: sphinx.config\n' restructuredtext.parse(app, text) - index = PythonModuleIndex(app.env.get_domain('py')) + index = PythonModuleIndex(app.env.domains.python_domain) assert index.generate() == ( [ ( @@ -633,7 +633,7 @@ def test_module_index_submodule(app): def test_module_index_not_collapsed(app): text = '.. py:module:: docutils\n.. py:module:: sphinx\n' restructuredtext.parse(app, text) - index = PythonModuleIndex(app.env.get_domain('py')) + index = PythonModuleIndex(app.env.domains.python_domain) assert index.generate() == ( [ ('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), @@ -659,7 +659,7 @@ def test_modindex_common_prefix(app): '.. py:module:: sphinx_intl\n' ) restructuredtext.parse(app, text) - index = PythonModuleIndex(app.env.get_domain('py')) + index = PythonModuleIndex(app.env.domains.python_domain) assert index.generate() == ( [ ( diff --git a/tests/test_domains/test_domain_py_canonical.py b/tests/test_domains/test_domain_py_canonical.py index bf74c44f4e8..0c6982d652d 100644 --- a/tests/test_domains/test_domain_py_canonical.py +++ b/tests/test_domains/test_domain_py_canonical.py @@ -38,7 +38,7 @@ def test_domain_py_canonical(app): @pytest.mark.sphinx('html', testroot='root') def test_canonical(app): text = '.. py:class:: io.StringIO\n :canonical: _io.StringIO' - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -75,7 +75,7 @@ def test_canonical_definition_overrides(app): restructuredtext.parse(app, text) assert app.warning.getvalue() == '' - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain assert domain.objects['_io.StringIO'] == ('index', 'id0', 'class', False) @@ -90,7 +90,7 @@ def test_canonical_definition_skip(app): restructuredtext.parse(app, text) assert app.warning.getvalue() == '' - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain assert domain.objects['_io.StringIO'] == ('index', 'io.StringIO', 'class', False) diff --git a/tests/test_domains/test_domain_py_pyfunction.py b/tests/test_domains/test_domain_py_pyfunction.py index 93dcb1f2564..d9d11dbde01 100644 --- a/tests/test_domains/test_domain_py_pyfunction.py +++ b/tests/test_domains/test_domain_py_pyfunction.py @@ -36,7 +36,7 @@ def test_pyfunction(app): '.. py:function:: func2\n' ' :async:\n' ) - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, diff --git a/tests/test_domains/test_domain_py_pyobject.py b/tests/test_domains/test_domain_py_pyobject.py index 100aaf9e31d..8691e0e08cc 100644 --- a/tests/test_domains/test_domain_py_pyobject.py +++ b/tests/test_domains/test_domain_py_pyobject.py @@ -199,7 +199,7 @@ def test_pyobject_prefix(app): @pytest.mark.sphinx('html', testroot='root') def test_pydata(app): text = '.. py:module:: example\n.. py:data:: var\n :type: int\n' - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -238,7 +238,7 @@ def test_pydata(app): @pytest.mark.sphinx('html', testroot='root') def test_pyclass_options(app): text = '.. py:class:: Class1\n.. py:class:: Class2\n :final:\n' - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -313,7 +313,7 @@ def test_pymethod_options(app): ' .. py:method:: meth6\n' ' :final:\n' ) - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -486,7 +486,7 @@ def test_pymethod_options(app): @pytest.mark.sphinx('html', testroot='root') def test_pyclassmethod(app): text = '.. py:class:: Class\n\n .. py:classmethod:: meth\n' - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -533,7 +533,7 @@ def test_pyclassmethod(app): @pytest.mark.sphinx('html', testroot='root') def test_pystaticmethod(app): text = '.. py:class:: Class\n\n .. py:staticmethod:: meth\n' - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -586,7 +586,7 @@ def test_pyattribute(app): ' :type: Optional[str]\n' " :value: ''\n" ) - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -664,7 +664,7 @@ def test_pyproperty(app): ' :classmethod:\n' ' :type: str\n' ) - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -761,7 +761,7 @@ def test_py_type_alias(app): ' .. py:type:: Alias2\n' ' :canonical: int\n' ) - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -891,7 +891,7 @@ def test_domain_py_type_alias(app): @pytest.mark.sphinx('html', testroot='root') def test_pydecorator_signature(app): text = '.. py:decorator:: deco' - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -922,7 +922,7 @@ def test_pydecorator_signature(app): @pytest.mark.sphinx('html', testroot='root') def test_pydecoratormethod_signature(app): text = '.. py:decoratormethod:: deco' - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -968,7 +968,7 @@ def test_pycurrentmodule(app): ' .. py:method:: m3\n' ' .. py:method:: m4\n' ) - domain = app.env.get_domain('py') + domain = app.env.domains.python_domain doctree = restructuredtext.parse(app, text) print(doctree) assert_node( diff --git a/tests/test_domains/test_domain_std.py b/tests/test_domains/test_domain_std.py index a571885a200..4e5d88b63de 100644 --- a/tests/test_domains/test_domain_std.py +++ b/tests/test_domains/test_domain_std.py @@ -110,7 +110,7 @@ def test_cmd_option_with_optional_value(app): entries=[('pair', 'command line option; -j', 'cmdoption-j', '', None)], ) - objects = list(app.env.get_domain('std').get_objects()) + objects = list(app.env.domains.standard_domain.get_objects()) assert ('-j', '-j', 'cmdoption', 'index', 'cmdoption-j', 1) in objects @@ -134,7 +134,7 @@ def test_cmd_option_starting_with_bracket(app): ], ), ) - objects = list(app.env.get_domain('std').get_objects()) + objects = list(app.env.domains.standard_domain.get_objects()) assert ( '[enable', '[enable', @@ -209,7 +209,7 @@ def test_glossary(app): assert_node(doctree[0][0][2][1], [nodes.definition, nodes.paragraph, 'description']) # index - domain = app.env.get_domain('std') + domain = app.env.domains.standard_domain objects = list(domain.get_objects()) assert ('term1', 'term1', 'term', 'index', 'term-term1', -1) in objects assert ('TERM2', 'TERM2', 'term', 'index', 'term-TERM2', -1) in objects @@ -375,7 +375,7 @@ def test_glossary_sorted(app): def test_glossary_alphanumeric(app): text = '.. glossary::\n\n 1\n /\n' restructuredtext.parse(app, text) - objects = list(app.env.get_domain('std').get_objects()) + objects = list(app.env.domains.standard_domain.get_objects()) assert ('1', '1', 'term', 'index', 'term-1', -1) in objects assert ('/', '/', 'term', 'index', 'term-0', -1) in objects @@ -384,14 +384,14 @@ def test_glossary_alphanumeric(app): def test_glossary_conflicted_labels(app): text = '.. _term-foo:\n.. glossary::\n\n foo\n' restructuredtext.parse(app, text) - objects = list(app.env.get_domain('std').get_objects()) + objects = list(app.env.domains.standard_domain.get_objects()) assert ('foo', 'foo', 'term', 'index', 'term-0', -1) in objects @pytest.mark.sphinx('html', testroot='root') def test_cmdoption(app): text = '.. program:: ls\n\n.. option:: -l\n' - domain = app.env.get_domain('std') + domain = app.env.domains.standard_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -418,7 +418,7 @@ def test_cmdoption(app): @pytest.mark.sphinx('html', testroot='root') def test_cmdoption_for_None(app): text = '.. program:: ls\n.. program:: None\n\n.. option:: -l\n' - domain = app.env.get_domain('std') + domain = app.env.domains.standard_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -445,7 +445,7 @@ def test_cmdoption_for_None(app): @pytest.mark.sphinx('html', testroot='root') def test_multiple_cmdoptions(app): text = '.. program:: cmd\n\n.. option:: -o directory, --output directory\n' - domain = app.env.get_domain('std') + domain = app.env.domains.standard_domain doctree = restructuredtext.parse(app, text) assert_node( doctree, @@ -577,7 +577,7 @@ def test_labeled_rubric(app): text = '.. _label:\n.. rubric:: blah *blah* blah\n' restructuredtext.parse(app, text) - domain = app.env.get_domain('std') + domain = app.env.domains.standard_domain assert 'label' in domain.labels assert domain.labels['label'] == ('index', 'label', 'blah blah blah') @@ -598,7 +598,7 @@ def test_labeled_definition(app): ) restructuredtext.parse(app, text) - domain = app.env.get_domain('std') + domain = app.env.domains.standard_domain assert 'label1' in domain.labels assert domain.labels['label1'] == ('index', 'label1', 'Foo blah blah blah') assert 'label2' in domain.labels @@ -621,7 +621,7 @@ def test_labeled_field(app): ) restructuredtext.parse(app, text) - domain = app.env.get_domain('std') + domain = app.env.domains.standard_domain assert 'label1' in domain.labels assert domain.labels['label1'] == ('index', 'label1', 'Foo blah blah blah') assert 'label2' in domain.labels diff --git a/tests/test_environment/test_environment.py b/tests/test_environment/test_environment.py index 77f8043cafe..1f83b7a2221 100644 --- a/tests/test_environment/test_environment.py +++ b/tests/test_environment/test_environment.py @@ -130,8 +130,8 @@ def test_object_inventory(app): False, ) - assert app.env.domains['py'].data is app.env.domaindata['py'] - assert app.env.domains['c'].data is app.env.domaindata['c'] + assert app.env.domains.python_domain.data is app.env.domaindata['py'] + assert app.env.domains.c_domain.data is app.env.domaindata['c'] @pytest.mark.sphinx('dummy', testroot='basic') diff --git a/tests/test_environment/test_environment_indexentries.py b/tests/test_environment/test_environment_indexentries.py index 1de53c2bcea..1de861abc49 100644 --- a/tests/test_environment/test_environment_indexentries.py +++ b/tests/test_environment/test_environment_indexentries.py @@ -263,7 +263,7 @@ def test_create_index_with_name(app): assert index[2] == ('S', [('Sphinx', ([('', '#index-0')], [], None))]) # check the reference labels are created correctly - std = app.env.get_domain('std') + std = app.env.domains.standard_domain assert std.anonlabels['ref1'] == ('index', 'ref1') assert std.anonlabels['ref2'] == ('index', 'ref2') diff --git a/tests/test_extensions/test_ext_autosummary.py b/tests/test_extensions/test_ext_autosummary.py index 007bed0975e..d1ea3d1ac96 100644 --- a/tests/test_extensions/test_ext_autosummary.py +++ b/tests/test_extensions/test_ext_autosummary.py @@ -2,7 +2,6 @@ import sys from io import StringIO -from pathlib import Path from unittest.mock import Mock, patch import pytest @@ -37,7 +36,6 @@ 'extensions': ['sphinx.ext.autosummary'], 'autosummary_generate': True, 'autosummary_generate_overwrite': False, - 'source_suffix': '.rst', } @@ -219,13 +217,10 @@ def str_content(elem): def test_escaping(app): app.build(force_all=True) - outdir = Path(app.builder.outdir) - - docpage = outdir / 'underscore_module_.xml' + docpage = app.builder.outdir / 'underscore_module_.xml' assert docpage.exists() title = etree_parse(docpage).find('section/title') - assert str_content(title) == 'underscore_module_' diff --git a/tests/test_search.py b/tests/test_search.py index 453b83ea406..f350d192ac1 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -5,6 +5,7 @@ import json import warnings from io import BytesIO +from typing import TYPE_CHECKING import pytest from docutils import frontend, utils @@ -14,6 +15,9 @@ from tests.utils import TESTS_ROOT +if TYPE_CHECKING: + from collections.abc import Iterator + JAVASCRIPT_TEST_ROOTS = [ directory for directory in (TESTS_ROOT / 'js' / 'roots').iterdir() @@ -21,22 +25,32 @@ ] +class DummyDomainsContainer: + def __init__(self, **domains: DummyDomain) -> None: + self._domain_instances = domains + + def sorted(self) -> Iterator[DummyDomain]: + for _domain_name, domain in sorted(self._domain_instances.items()): + yield domain + + class DummyEnvironment: - def __init__(self, version, domains): + def __init__(self, version: str, domains: DummyDomainsContainer) -> None: self.version = version self.domains = domains - def __getattr__(self, name): + def __getattr__(self, name: str): if name.startswith('_search_index_'): setattr(self, name, {}) return getattr(self, name, {}) - def __str__(self): + def __str__(self) -> str: return f'DummyEnvironment({self.version!r}, {self.domains!r})' class DummyDomain: - def __init__(self, data): + def __init__(self, name: str, data: dict) -> None: + self.name = name self.data = data self.object_types = {} @@ -162,15 +176,21 @@ def test_term_in_raw_directive(app): def test_IndexBuilder(): - domain1 = DummyDomain([ - ('objname1', 'objdispname1', 'objtype1', 'docname1_1', '#anchor', 1), - ('objname2', 'objdispname2', 'objtype2', 'docname1_2', '', -1), - ]) - domain2 = DummyDomain([ - ('objname1', 'objdispname1', 'objtype1', 'docname2_1', '#anchor', 1), - ('objname2', 'objdispname2', 'objtype2', 'docname2_2', '', -1), - ]) - env = DummyEnvironment('1.0', {'dummy1': domain1, 'dummy2': domain2}) + domain1 = DummyDomain( + 'dummy1', + [ + ('objname1', 'objdispname1', 'objtype1', 'docname1_1', '#anchor', 1), + ('objname2', 'objdispname2', 'objtype2', 'docname1_2', '', -1), + ], + ) + domain2 = DummyDomain( + 'dummy2', + [ + ('objname1', 'objdispname1', 'objtype1', 'docname2_1', '#anchor', 1), + ('objname2', 'objdispname2', 'objtype2', 'docname2_2', '', -1), + ], + ) + env = DummyEnvironment('1.0', DummyDomainsContainer(dummy1=domain1, dummy2=domain2)) doc = utils.new_document(b'test data', settings) doc['file'] = 'dummy' parser.parse(FILE_CONTENTS, doc) @@ -258,7 +278,7 @@ def test_IndexBuilder(): 1: ('dummy2', 'objtype1', 'objtype1'), } - env = DummyEnvironment('1.0', {'dummy1': domain1, 'dummy2': domain2}) + env = DummyEnvironment('1.0', DummyDomainsContainer(dummy1=domain1, dummy2=domain2)) # dump / load stream = BytesIO()