22
33from __future__ import annotations
44
5- from typing import TYPE_CHECKING
5+ import re
66
7+ from docutils import nodes
8+ from sphinx import addnodes
79from sphinx .domains .changeset import (
810 VersionChange ,
911 versionlabel_classes ,
1012 versionlabels ,
1113)
1214from sphinx .locale import _ as sphinx_gettext
1315
16+ TYPE_CHECKING = False
1417if TYPE_CHECKING :
1518 from docutils .nodes import Node
1619 from sphinx .application import Sphinx
@@ -73,6 +76,76 @@ def run(self) -> list[Node]:
7376 versionlabel_classes [self .name ] = ""
7477
7578
79+ class SoftDeprecated (PyVersionChange ):
80+ """Directive for soft deprecations that auto-links to the glossary term.
81+
82+ Usage::
83+
84+ .. soft-deprecated:: 3.15
85+
86+ Use :func:`new_thing` instead.
87+
88+ Renders as: "Soft deprecated since version 3.15: Use new_thing() instead."
89+ with "Soft deprecated" linking to the glossary definition.
90+ """
91+
92+ _TERM_RE = re .compile (r":term:`([^`]+)`" )
93+
94+ def run (self ) -> list [Node ]:
95+ versionlabels [self .name ] = sphinx_gettext (
96+ ":term:`Soft deprecated` since version %s"
97+ )
98+ versionlabel_classes [self .name ] = "soft-deprecated"
99+ try :
100+ result = super ().run ()
101+ finally :
102+ versionlabels [self .name ] = ""
103+ versionlabel_classes [self .name ] = ""
104+
105+ for node in result :
106+ # Add "versionchanged" class so existing theme CSS applies
107+ node ["classes" ] = node .get ("classes" , []) + ["versionchanged" ]
108+ # Replace the plain-text "Soft deprecated" with a glossary reference
109+ for inline in node .findall (nodes .inline ):
110+ if "versionmodified" in inline .get ("classes" , []):
111+ self ._add_glossary_link (inline )
112+
113+ return result
114+
115+ @classmethod
116+ def _add_glossary_link (cls , inline : nodes .inline ) -> None :
117+ """Replace :term:`soft deprecated` text with a cross-reference to the
118+ 'Soft deprecated' glossary entry."""
119+ for child in inline .children :
120+ if not isinstance (child , nodes .Text ):
121+ continue
122+
123+ text = str (child )
124+ match = cls ._TERM_RE .search (text )
125+ if match is None :
126+ continue
127+
128+ ref = addnodes .pending_xref (
129+ "" ,
130+ nodes .Text (match .group (1 )),
131+ refdomain = "std" ,
132+ reftype = "term" ,
133+ reftarget = "soft deprecated" ,
134+ refwarn = True ,
135+ )
136+
137+ start , end = match .span ()
138+ new_nodes : list [nodes .Node ] = []
139+ if start > 0 :
140+ new_nodes .append (nodes .Text (text [:start ]))
141+ new_nodes .append (ref )
142+ if end < len (text ):
143+ new_nodes .append (nodes .Text (text [end :]))
144+
145+ child .parent .replace (child , new_nodes )
146+ break
147+
148+
76149def setup (app : Sphinx ) -> ExtensionMetadata :
77150 # Override Sphinx's directives with support for 'next'
78151 app .add_directive ("versionadded" , PyVersionChange , override = True )
@@ -83,6 +156,9 @@ def setup(app: Sphinx) -> ExtensionMetadata:
83156 # Register the ``.. deprecated-removed::`` directive
84157 app .add_directive ("deprecated-removed" , DeprecatedRemoved )
85158
159+ # Register the ``.. soft-deprecated::`` directive
160+ app .add_directive ("soft-deprecated" , SoftDeprecated )
161+
86162 return {
87163 "version" : "1.0" ,
88164 "parallel_read_safe" : True ,
0 commit comments