22
33from __future__ import annotations
44
5- from typing import TYPE_CHECKING
6-
5+ from docutils import nodes
6+ from sphinx import addnodes
77from sphinx .domains .changeset import (
88 VersionChange ,
99 versionlabel_classes ,
1010 versionlabels ,
1111)
1212from sphinx .locale import _ as sphinx_gettext
1313
14+ TYPE_CHECKING = False
1415if TYPE_CHECKING :
1516 from docutils .nodes import Node
1617 from sphinx .application import Sphinx
@@ -73,6 +74,77 @@ def run(self) -> list[Node]:
7374 versionlabel_classes [self .name ] = ""
7475
7576
77+ class SoftDeprecated (PyVersionChange ):
78+ """Directive for soft deprecations that auto-links to the glossary term.
79+
80+ Usage::
81+
82+ .. soft-deprecated:: 3.15
83+
84+ Use :func:`new_thing` instead.
85+
86+ Renders as: "Soft deprecated since version 3.15: Use new_thing() instead."
87+ with "Soft deprecated" linking to the glossary definition.
88+ """
89+
90+ def run (self ) -> list [Node ]:
91+ versionlabels [self .name ] = sphinx_gettext (
92+ "Soft deprecated since version %s"
93+ )
94+ versionlabel_classes [self .name ] = "softdeprecated"
95+ try :
96+ result = super ().run ()
97+ finally :
98+ versionlabels [self .name ] = ""
99+ versionlabel_classes [self .name ] = ""
100+
101+ for node in result :
102+ # Add "versionchanged" class so existing theme CSS applies
103+ node ["classes" ] = node .get ("classes" , []) + ["versionchanged" ]
104+ # Replace the plain-text "Soft deprecated" with a glossary reference
105+ for inline in node .findall (nodes .inline ):
106+ if "versionmodified" in inline .get ("classes" , []):
107+ self ._add_glossary_link (inline )
108+
109+ return result
110+
111+ @staticmethod
112+ def _add_glossary_link (inline : nodes .inline ) -> None :
113+ """Replace 'Soft deprecated' text with a cross-reference to the
114+ :term:`soft deprecated` glossary entry."""
115+ marker = "Soft deprecated"
116+ ref = addnodes .pending_xref (
117+ "" ,
118+ nodes .Text (marker ),
119+ refdomain = "std" ,
120+ reftype = "term" ,
121+ reftarget = "soft deprecated" ,
122+ refwarn = True ,
123+ )
124+
125+ for child in inline .children :
126+ if not isinstance (child , nodes .Text ):
127+ continue
128+
129+ text = str (child )
130+ idx = text .find (marker )
131+ if idx < 0 :
132+ continue
133+
134+ # Replace the text node with the split parts using docutils API
135+ new_nodes : list [nodes .Node ] = []
136+ if idx > 0 :
137+ new_nodes .append (nodes .Text (text [:idx ]))
138+
139+ new_nodes .append (ref )
140+ remainder = text [idx + len (marker ) :]
141+ if remainder :
142+ new_nodes .append (nodes .Text (remainder ))
143+
144+ child .parent .replace (child , new_nodes )
145+ break
146+
147+
76148def setup (app : Sphinx ) -> ExtensionMetadata :
77149 # Override Sphinx's directives with support for 'next'
78150 app .add_directive ("versionadded" , PyVersionChange , override = True )
@@ -83,6 +155,9 @@ def setup(app: Sphinx) -> ExtensionMetadata:
83155 # Register the ``.. deprecated-removed::`` directive
84156 app .add_directive ("deprecated-removed" , DeprecatedRemoved )
85157
158+ # Register the ``.. soft-deprecated::`` directive
159+ app .add_directive ("soft-deprecated" , SoftDeprecated )
160+
86161 return {
87162 "version" : "1.0" ,
88163 "parallel_read_safe" : True ,
0 commit comments