"""The changeset domain.""" from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple, cast from docutils import nodes from sphinx import addnodes from sphinx.domains import Domain from sphinx.locale import _ from sphinx.util.docutils import SphinxDirective if TYPE_CHECKING: from docutils.nodes import Node from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment from sphinx.util.typing import OptionSpec versionlabels = { 'versionadded': _('New in version %s'), 'versionchanged': _('Changed in version %s'), 'deprecated': _('Deprecated since version %s'), } versionlabel_classes = { 'versionadded': 'added', 'versionchanged': 'changed', 'deprecated': 'deprecated', } class ChangeSet(NamedTuple): type: str docname: str lineno: int module: str | None descname: str | None content: str class VersionChange(SphinxDirective): """ Directive to describe a change/addition/deprecation in a specific version. """ has_content = True required_arguments = 1 optional_arguments = 1 final_argument_whitespace = True option_spec: OptionSpec = {} def run(self) -> list[Node]: node = addnodes.versionmodified() node.document = self.state.document self.set_source_info(node) node['type'] = self.name node['version'] = self.arguments[0] text = versionlabels[self.name] % self.arguments[0] if len(self.arguments) == 2: inodes, messages = self.state.inline_text(self.arguments[1], self.lineno + 1) para = nodes.paragraph(self.arguments[1], '', *inodes, translatable=False) self.set_source_info(para) node.append(para) else: messages = [] if self.content: self.state.nested_parse(self.content, self.content_offset, node) classes = ['versionmodified', versionlabel_classes[self.name]] if len(node) > 0 and isinstance(node[0], nodes.paragraph): # the contents start with a paragraph if node[0].rawsource: # make the first paragraph translatable content = nodes.inline(node[0].rawsource, translatable=True) content.source = node[0].source content.line = node[0].line content += node[0].children node[0].replace_self(nodes.paragraph('', '', content, translatable=False)) para = node[0] para.insert(0, nodes.inline('', '%s: ' % text, classes=classes)) elif len(node) > 0: # the contents do not starts with a paragraph para = nodes.paragraph('', '', nodes.inline('', '%s: ' % text, classes=classes), translatable=False) node.insert(0, para) else: # the contents are empty para = nodes.paragraph('', '', nodes.inline('', '%s.' % text, classes=classes), translatable=False) node.append(para) domain = cast(ChangeSetDomain, self.env.get_domain('changeset')) domain.note_changeset(node) ret: list[Node] = [node] ret += messages return ret class ChangeSetDomain(Domain): """Domain for changesets.""" name = 'changeset' label = 'changeset' initial_data: dict[str, Any] = { 'changes': {}, # version -> list of ChangeSet } @property def changesets(self) -> dict[str, list[ChangeSet]]: return self.data.setdefault('changes', {}) # version -> list of ChangeSet def note_changeset(self, node: addnodes.versionmodified) -> None: version = node['version'] module = self.env.ref_context.get('py:module') objname = self.env.temp_data.get('object') changeset = ChangeSet(node['type'], self.env.docname, node.line, module, objname, node.astext()) self.changesets.setdefault(version, []).append(changeset) def clear_doc(self, docname: str) -> None: for changes in self.changesets.values(): for changeset in changes[:]: if changeset.docname == docname: changes.remove(changeset) def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: # XXX duplicates? for version, otherchanges in otherdata['changes'].items(): changes = self.changesets.setdefault(version, []) for changeset in otherchanges: if changeset.docname in docnames: changes.append(changeset) def process_doc( self, env: BuildEnvironment, docname: str, document: nodes.document, ) -> None: pass # nothing to do here. All changesets are registered on calling directive. def get_changesets_for(self, version: str) -> list[ChangeSet]: return self.changesets.get(version, []) def setup(app: Sphinx) -> dict[str, Any]: app.add_domain(ChangeSetDomain) app.add_directive('deprecated', VersionChange) app.add_directive('versionadded', VersionChange) app.add_directive('versionchanged', VersionChange) return { 'version': 'builtin', 'env_version': 1, 'parallel_read_safe': True, 'parallel_write_safe': True, }