| """Support for documenting version of changes, additions, deprecations.""" |
| |
| from __future__ import annotations |
| |
| import re |
| |
| from docutils import nodes |
| from sphinx import addnodes |
| from sphinx.domains.changeset import ( |
| VersionChange, |
| versionlabel_classes, |
| versionlabels, |
| ) |
| from sphinx.locale import _ as sphinx_gettext |
| |
| TYPE_CHECKING = False |
| if TYPE_CHECKING: |
| from docutils.nodes import Node |
| from sphinx.application import Sphinx |
| from sphinx.util.typing import ExtensionMetadata |
| |
| |
| def expand_version_arg(argument: str, release: str) -> str: |
| """Expand "next" to the current version""" |
| if argument == "next": |
| return sphinx_gettext("{} (unreleased)").format(release) |
| return argument |
| |
| |
| class PyVersionChange(VersionChange): |
| def run(self) -> list[Node]: |
| # Replace the 'next' special token with the current development version |
| self.arguments[0] = expand_version_arg( |
| self.arguments[0], self.config.release |
| ) |
| return super().run() |
| |
| |
| class DeprecatedRemoved(VersionChange): |
| required_arguments = 2 |
| |
| _deprecated_label = sphinx_gettext( |
| "Deprecated since version %s, will be removed in version %s" |
| ) |
| _removed_label = sphinx_gettext( |
| "Deprecated since version %s, removed in version %s" |
| ) |
| |
| def run(self) -> list[Node]: |
| # Replace the first two arguments (deprecated version and removed version) |
| # with a single tuple of both versions. |
| version_deprecated = expand_version_arg( |
| self.arguments[0], self.config.release |
| ) |
| version_removed = self.arguments.pop(1) |
| if version_removed == "next": |
| raise ValueError( |
| "deprecated-removed:: second argument cannot be `next`" |
| ) |
| self.arguments[0] = version_deprecated, version_removed |
| |
| # Set the label based on if we have reached the removal version |
| current_version = tuple(map(int, self.config.version.split("."))) |
| removed_version = tuple(map(int, version_removed.split("."))) |
| if current_version < removed_version: |
| versionlabels[self.name] = self._deprecated_label |
| versionlabel_classes[self.name] = "deprecated" |
| else: |
| versionlabels[self.name] = self._removed_label |
| versionlabel_classes[self.name] = "removed" |
| try: |
| return super().run() |
| finally: |
| # reset versionlabels and versionlabel_classes |
| versionlabels[self.name] = "" |
| versionlabel_classes[self.name] = "" |
| |
| |
| class SoftDeprecated(PyVersionChange): |
| """Directive for soft deprecations that auto-links to the glossary term. |
| |
| Usage:: |
| |
| .. soft-deprecated:: 3.15 |
| |
| Use :func:`new_thing` instead. |
| |
| Renders as: "Soft deprecated since version 3.15: Use new_thing() instead." |
| with "Soft deprecated" linking to the glossary definition. |
| """ |
| |
| _TERM_RE = re.compile(r":term:`([^`]+)`") |
| |
| def run(self) -> list[Node]: |
| versionlabels[self.name] = sphinx_gettext( |
| ":term:`Soft deprecated` since version %s" |
| ) |
| versionlabel_classes[self.name] = "soft-deprecated" |
| try: |
| result = super().run() |
| finally: |
| versionlabels[self.name] = "" |
| versionlabel_classes[self.name] = "" |
| |
| for node in result: |
| # Add "versionchanged" class so existing theme CSS applies |
| node["classes"] = node.get("classes", []) + ["versionchanged"] |
| # Replace the plain-text "Soft deprecated" with a glossary reference |
| for inline in node.findall(nodes.inline): |
| if "versionmodified" in inline.get("classes", []): |
| self._add_glossary_link(inline) |
| |
| return result |
| |
| @classmethod |
| def _add_glossary_link(cls, inline: nodes.inline) -> None: |
| """Replace :term:`soft deprecated` text with a cross-reference to the |
| 'Soft deprecated' glossary entry.""" |
| for child in inline.children: |
| if not isinstance(child, nodes.Text): |
| continue |
| |
| text = str(child) |
| match = cls._TERM_RE.search(text) |
| if match is None: |
| continue |
| |
| ref = addnodes.pending_xref( |
| "", |
| nodes.Text(match.group(1)), |
| refdomain="std", |
| reftype="term", |
| reftarget="soft deprecated", |
| refwarn=True, |
| ) |
| |
| start, end = match.span() |
| new_nodes: list[nodes.Node] = [] |
| if start > 0: |
| new_nodes.append(nodes.Text(text[:start])) |
| new_nodes.append(ref) |
| if end < len(text): |
| new_nodes.append(nodes.Text(text[end:])) |
| |
| child.parent.replace(child, new_nodes) |
| break |
| |
| |
| def setup(app: Sphinx) -> ExtensionMetadata: |
| # Override Sphinx's directives with support for 'next' |
| app.add_directive("versionadded", PyVersionChange, override=True) |
| app.add_directive("versionchanged", PyVersionChange, override=True) |
| app.add_directive("versionremoved", PyVersionChange, override=True) |
| app.add_directive("deprecated", PyVersionChange, override=True) |
| |
| # Register the ``.. deprecated-removed::`` directive |
| app.add_directive("deprecated-removed", DeprecatedRemoved) |
| |
| # Register the ``.. soft-deprecated::`` directive |
| app.add_directive("soft-deprecated", SoftDeprecated) |
| |
| return { |
| "version": "1.0", |
| "parallel_read_safe": True, |
| "parallel_write_safe": True, |
| } |