Source code for sphinx.domains

"""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


[docs] 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
[docs] 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 not self.name or self.localname is None: raise SphinxError('Index subclass %s has no valid name or localname' % self.__class__.__name__) self.domain = domain
[docs] @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]]
[docs] 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
[docs] 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)
[docs] 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)
[docs] 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
[docs] 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
[docs] def clear_doc(self, docname: str) -> None: """Remove traces of a document in the domain-specific inventories.""" pass
[docs] def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> 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__)
[docs] def process_doc(self, env: BuildEnvironment, docname: str, document: nodes.document) -> None: """Process a document after it is read by the environment.""" pass
[docs] def check_consistency(self) -> None: """Do consistency checks (**experimental**).""" pass
[docs] 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
[docs] 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
[docs] 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
[docs] 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 []
[docs] 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)
[docs] 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
[docs] def get_full_qualified_name(self, node: Element) -> str | None: """Return full qualified name for given node.""" pass