-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
Expand file tree
/
Copy pathchanges.py
More file actions
166 lines (133 loc) · 5.47 KB
/
changes.py
File metadata and controls
166 lines (133 loc) · 5.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
"""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,
}