diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:25:40 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:25:40 +0000 |
commit | cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a (patch) | |
tree | 18dcde1a8d1f5570a77cd0c361de3b490d02c789 /sphinx/domains/__init__.py | |
parent | Initial commit. (diff) | |
download | sphinx-upstream/7.2.6.tar.xz sphinx-upstream/7.2.6.zip |
Adding upstream version 7.2.6.upstream/7.2.6
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sphinx/domains/__init__.py')
-rw-r--r-- | sphinx/domains/__init__.py | 406 |
1 files changed, 406 insertions, 0 deletions
diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py new file mode 100644 index 0000000..7c11220 --- /dev/null +++ b/sphinx/domains/__init__.py @@ -0,0 +1,406 @@ +"""Support for domains. + +Domains are groupings of description directives +and roles describing e.g. constructs of one programming language. +""" + +from __future__ import annotations + +import copy +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Callable, NamedTuple, Optional, cast + +from docutils.nodes import Element, Node, system_message + +from sphinx.errors import SphinxError +from sphinx.locale import _ + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + from docutils import nodes + from docutils.parsers.rst import Directive + from docutils.parsers.rst.states import Inliner + + from sphinx.addnodes import pending_xref + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + from sphinx.roles import XRefRole + from sphinx.util.typing import RoleFunction + + +class ObjType: + """ + An ObjType is the description for a type of object that a domain can + document. In the object_types attribute of Domain subclasses, object type + names are mapped to instances of this class. + + Constructor arguments: + + - *lname*: localized name of the type (do not include domain name) + - *roles*: all the roles that can refer to an object of this type + - *attrs*: object attributes -- currently only "searchprio" is known, + which defines the object's priority in the full-text search index, + see :meth:`Domain.get_objects()`. + """ + + known_attrs = { + 'searchprio': 1, + } + + def __init__(self, lname: str, *roles: Any, **attrs: Any) -> None: + self.lname = lname + self.roles: tuple = roles + self.attrs: dict = self.known_attrs.copy() + self.attrs.update(attrs) + + +class IndexEntry(NamedTuple): + name: str + subtype: int + docname: str + anchor: str + extra: str + qualifier: str + descr: str + + +class Index(ABC): + """ + An Index is the description for a domain-specific index. To add an index to + a domain, subclass Index, overriding the three name attributes: + + * `name` is an identifier used for generating file names. + It is also used for a hyperlink target for the index. Therefore, users can + refer the index page using ``ref`` role and a string which is combined + domain name and ``name`` attribute (ex. ``:ref:`py-modindex```). + * `localname` is the section title for the index. + * `shortname` is a short name for the index, for use in the relation bar in + HTML output. Can be empty to disable entries in the relation bar. + + and providing a :meth:`generate()` method. Then, add the index class to + your domain's `indices` list. Extensions can add indices to existing + domains using :meth:`~sphinx.application.Sphinx.add_index_to_domain()`. + + .. versionchanged:: 3.0 + + Index pages can be referred by domain name and index name via + :rst:role:`ref` role. + """ + + name: str + localname: str + shortname: str | None = None + + def __init__(self, domain: Domain) -> None: + if self.name is None or self.localname is None: + raise SphinxError('Index subclass %s has no valid name or localname' + % self.__class__.__name__) + self.domain = domain + + @abstractmethod + def generate(self, docnames: Iterable[str] | None = None, + ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: + """Get entries for the index. + + If ``docnames`` is given, restrict to entries referring to these + docnames. + + The return value is a tuple of ``(content, collapse)``: + + ``collapse`` + A boolean that determines if sub-entries should start collapsed (for + output formats that support collapsing sub-entries). + + ``content``: + A sequence of ``(letter, entries)`` tuples, where ``letter`` is the + "heading" for the given ``entries``, usually the starting letter, and + ``entries`` is a sequence of single entries. Each entry is a sequence + ``[name, subtype, docname, anchor, extra, qualifier, descr]``. The + items in this sequence have the following meaning: + + ``name`` + The name of the index entry to be displayed. + + ``subtype`` + The sub-entry related type. One of: + + ``0`` + A normal entry. + ``1`` + An entry with sub-entries. + ``2`` + A sub-entry. + + ``docname`` + *docname* where the entry is located. + + ``anchor`` + Anchor for the entry within ``docname`` + + ``extra`` + Extra info for the entry. + + ``qualifier`` + Qualifier for the description. + + ``descr`` + Description for the entry. + + Qualifier and description are not rendered for some output formats such + as LaTeX. + """ + raise NotImplementedError + + +TitleGetter = Callable[[Node], Optional[str]] + + +class Domain: + """ + A Domain is meant to be a group of "object" description directives for + objects of a similar nature, and corresponding roles to create references to + them. Examples would be Python modules, classes, functions etc., elements + of a templating language, Sphinx roles and directives, etc. + + Each domain has a separate storage for information about existing objects + and how to reference them in `self.data`, which must be a dictionary. It + also must implement several functions that expose the object information in + a uniform way to parts of Sphinx that allow the user to reference or search + for objects in a domain-agnostic way. + + About `self.data`: since all object and cross-referencing information is + stored on a BuildEnvironment instance, the `domain.data` object is also + stored in the `env.domaindata` dict under the key `domain.name`. Before the + build process starts, every active domain is instantiated and given the + environment object; the `domaindata` dict must then either be nonexistent or + a dictionary whose 'version' key is equal to the domain class' + :attr:`data_version` attribute. Otherwise, `OSError` is raised and the + pickled environment is discarded. + """ + + #: domain name: should be short, but unique + name = '' + #: domain label: longer, more descriptive (used in messages) + label = '' + #: type (usually directive) name -> ObjType instance + object_types: dict[str, ObjType] = {} + #: directive name -> directive class + directives: dict[str, type[Directive]] = {} + #: role name -> role callable + roles: dict[str, RoleFunction | XRefRole] = {} + #: a list of Index subclasses + indices: list[type[Index]] = [] + #: role name -> a warning message if reference is missing + dangling_warnings: dict[str, str] = {} + #: node_class -> (enum_node_type, title_getter) + enumerable_nodes: dict[type[Node], tuple[str, TitleGetter | None]] = {} + #: data value for a fresh environment + initial_data: dict = {} + #: data value + data: dict + #: data version, bump this when the format of `self.data` changes + data_version = 0 + + def __init__(self, env: BuildEnvironment) -> None: + self.env: BuildEnvironment = env + self._role_cache: dict[str, Callable] = {} + self._directive_cache: dict[str, Callable] = {} + self._role2type: dict[str, list[str]] = {} + self._type2role: dict[str, str] = {} + + # convert class variables to instance one (to enhance through API) + self.object_types = dict(self.object_types) + self.directives = dict(self.directives) + self.roles = dict(self.roles) + self.indices = list(self.indices) + + if self.name not in env.domaindata: + assert isinstance(self.initial_data, dict) + new_data = copy.deepcopy(self.initial_data) + new_data['version'] = self.data_version + self.data = env.domaindata[self.name] = new_data + else: + self.data = env.domaindata[self.name] + if self.data['version'] != self.data_version: + raise OSError('data of %r domain out of date' % self.label) + for name, obj in self.object_types.items(): + for rolename in obj.roles: + self._role2type.setdefault(rolename, []).append(name) + self._type2role[name] = obj.roles[0] if obj.roles else '' + self.objtypes_for_role = self._role2type.get + self.role_for_objtype = self._type2role.get + + 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')) + for index in self.indices: + if index.name and index.localname: + docname = f"{self.name}-{index.name}" + std.note_hyperlink_target(docname, docname, '', index.localname) + + def add_object_type(self, name: str, objtype: ObjType) -> None: + """Add an object type.""" + self.object_types[name] = objtype + if objtype.roles: + self._type2role[name] = objtype.roles[0] + else: + self._type2role[name] = '' + + for role in objtype.roles: + self._role2type.setdefault(role, []).append(name) + + def role(self, name: str) -> RoleFunction | None: + """Return a role adapter function that always gives the registered + role its full name ('domain:name') as the first argument. + """ + if name in self._role_cache: + return self._role_cache[name] + if name not in self.roles: + return None + fullname = f'{self.name}:{name}' + + def role_adapter(typ: str, rawtext: str, text: str, lineno: int, + inliner: Inliner, options: dict | None = None, + content: Sequence[str] = (), + ) -> tuple[list[Node], list[system_message]]: + return self.roles[name](fullname, rawtext, text, lineno, + inliner, options or {}, content) + self._role_cache[name] = role_adapter + return role_adapter + + def directive(self, name: str) -> Callable | None: + """Return a directive adapter class that always gives the registered + directive its full name ('domain:name') as ``self.name``. + """ + if name in self._directive_cache: + return self._directive_cache[name] + if name not in self.directives: + return None + fullname = f'{self.name}:{name}' + BaseDirective = self.directives[name] + + class DirectiveAdapter(BaseDirective): # type: ignore[valid-type,misc] + def run(self) -> list[Node]: + self.name = fullname + return super().run() + self._directive_cache[name] = DirectiveAdapter + return DirectiveAdapter + + # methods that should be overwritten + + 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) -> None: + """Merge in data regarding *docnames* from a different domaindata + inventory (coming from a subprocess in parallel builds). + """ + raise NotImplementedError('merge_domaindata must be implemented in %s ' + 'to be able to do parallel builds!' % + self.__class__) + + def process_doc(self, env: BuildEnvironment, docname: str, + document: nodes.document) -> None: + """Process a document after it is read by the environment.""" + pass + + def check_consistency(self) -> None: + """Do consistency checks (**experimental**).""" + pass + + def process_field_xref(self, pnode: pending_xref) -> None: + """Process a pending xref created in a doc field. + For example, attach information about the current scope. + """ + pass + + def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, contnode: Element, + ) -> Element | None: + """Resolve the pending_xref *node* with the given *typ* and *target*. + + This method should return a new node, to replace the xref node, + containing the *contnode* which is the markup content of the + cross-reference. + + If no resolution can be found, None can be returned; the xref node will + then given to the :event:`missing-reference` event, and if that yields no + resolution, replaced by *contnode*. + + The method can also raise :exc:`sphinx.environment.NoUri` to suppress + the :event:`missing-reference` event being emitted. + """ + pass + + def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + target: str, node: pending_xref, contnode: Element, + ) -> list[tuple[str, Element]]: + """Resolve the pending_xref *node* with the given *target*. + + The reference comes from an "any" or similar role, which means that we + don't know the type. Otherwise, the arguments are the same as for + :meth:`resolve_xref`. + + The method must return a list (potentially empty) of tuples + ``('domain:role', newnode)``, where ``'domain:role'`` is the name of a + role that could have created the same reference, e.g. ``'py:func'``. + ``newnode`` is what :meth:`resolve_xref` would return. + + .. versionadded:: 1.3 + """ + raise NotImplementedError + + def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]: + """Return an iterable of "object descriptions". + + Object descriptions are tuples with six items: + + ``name`` + Fully qualified name. + + ``dispname`` + Name to display when searching/linking. + + ``type`` + Object type, a key in ``self.object_types``. + + ``docname`` + The document where it is to be found. + + ``anchor`` + The anchor name for the object. + + ``priority`` + How "important" the object is (determines placement in search + results). One of: + + ``1`` + Default priority (placed before full-text matches). + ``0`` + Object is important (placed before default-priority objects). + ``2`` + Object is unimportant (placed after full-text matches). + ``-1`` + Object should not show up in search at all. + """ + return [] + + def get_type_name(self, type: ObjType, primary: bool = False) -> str: + """Return full name for given ObjType.""" + if primary: + return type.lname + return _('%s %s') % (self.label, type.lname) + + def get_enumerable_node_type(self, node: Node) -> str | None: + """Get type of enumerable nodes (experimental).""" + enum_node_type, _ = self.enumerable_nodes.get(node.__class__, (None, None)) + return enum_node_type + + def get_full_qualified_name(self, node: Element) -> str | None: + """Return full qualified name for given node.""" + pass |