Repository: sphinx-doc/sphinx

Issue:
Add versionremoved directive to complement versionadded and deprecated
## Background

In [PEP4](https://peps.python.org/pep-0004/), deprecated is a word used to indicate something may be removed in the future.

> Usage of a module may be ‘deprecated’, which means that it may be removed from a future Python release.

The existing sphinx directives `versionadded`, `versionchanged`, and `deprecated` are excellent in reference documentation, but when something deprecated is removed, its not clear what to do without a `versionremoved` directive. Authoring reference documentation, I'd like to document that something has been around even after it is removed and can't be used any more.

## Proposal

I suggest a `versionremoved` directive is added alongside the other related directives below, together with new documentation alongside `versionadded` [as seen here](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-versionadded).

https://github.com/sphinx-doc/sphinx/blob/d3c91f951255c6729a53e38c895ddc0af036b5b9/sphinx/domains/changeset.py#L21-L31

Looking into the details, it seems like this is relevant to update:

- [ ] Update `versionlabels` and `versionlabel_classes` [in domains/changeset.py](https://github.com/sphinx-doc/sphinx/blob/d3c91f951255c6729a53e38c895ddc0af036b5b9/sphinx/domains/changeset.py#L21-L31)
- [ ] Update docstring [in addnotes.py](https://github.com/sphinx-doc/sphinx/blob/d3c91f951255c6729a53e38c895ddc0af036b5b9/sphinx/addnodes.py#L357-L362)
- [ ] Update logic in [builders/changes.py](https://github.com/sphinx-doc/sphinx/blob/d3c91f951255c6729a53e38c895ddc0af036b5b9/sphinx/builders/changes.py#L23-L43)
- [ ] Update tests in by searching for `paragraph of versionchanged` without case sensitivity in the code base

## Options

Sphinx users can patch sphinx via the sphinx extension [`sphinx-removed-in` does](https://github.com/MrSenko/sphinx-removed-in/blob/d92df304b94ccd26a0f412dc2c92273ed2d43194/sphinx_removed_in/__init__.py#L17-L20), from where the proposal stems from - thank you @atodorov, @hroncok, @nicoddemus who have made commits to that project.

For `sphinx>=2`, this extension is like adding this to a conf.py file:

```python
# -- Add versionremoved directive ---------------------------------------------------
# ref: https://github.com/sphinx-doc/sphinx/issues/11480
#
from sphinx.domains.changeset import VersionChange, versionlabel_classes, versionlabels


def setup(app):
    if "versionremoved" not in versionlabels:
        versionlabels["versionremoved"] = "Removed in version %s"
        versionlabel_classes["versionremoved"] = "removed"
        app.add_directive("versionremoved", VersionChange)
```


Discussion:
PR is welcomed! 
Please see PR https://github.com/sphinx-doc/sphinx/pull/11905.
Thank you @hugovk and everyone involved!! 🎉 ❤️ 



Patch:
diff --git a/doc/usage/builders/index.rst b/doc/usage/builders/index.rst
index 36d5799cd58..7a590a2b9b0 100644
--- a/doc/usage/builders/index.rst
+++ b/doc/usage/builders/index.rst
@@ -453,9 +453,9 @@ name is ``rinoh``. Refer to the `rinohtype manual`_ for details.
 .. class:: ChangesBuilder
 
    This builder produces an HTML overview of all :rst:dir:`versionadded`,
-   :rst:dir:`versionchanged` and :rst:dir:`deprecated` directives for the
-   current :confval:`version`.  This is useful to generate a ChangeLog file, for
-   example.
+   :rst:dir:`versionchanged`, :rst:dir:`deprecated` and :rst:dir:`versionremoved`
+   directives for the current :confval:`version`.  This is useful to generate a
+   changelog file, for example.
 
    .. autoattribute:: name
 
diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst
index 9eb98be67bf..ff425246fa0 100644
--- a/doc/usage/restructuredtext/directives.rst
+++ b/doc/usage/restructuredtext/directives.rst
@@ -323,6 +323,18 @@ units as well as normal text.
       .. deprecated:: 3.1
          Use :func:`spam` instead.
 
+.. rst:directive:: .. versionremoved:: version
+
+   Similar to :rst:dir:`versionadded`, but describes when the feature was removed.
+   An explanation may be provided to inform the reader what to use instead,
+   or why the feature was removed.
+   Example::
+
+      .. versionremoved:: 4.0
+         The :func:`spam` function is more flexible, and should be used instead.
+
+   .. versionadded:: 7.3
+
 .. rst:directive:: seealso
 
    Many sections include a list of references to module documentation or
diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py
index 28b67f7babf..397d73fb652 100644
--- a/sphinx/addnodes.py
+++ b/sphinx/addnodes.py
@@ -406,8 +406,8 @@ class desc_sig_literal_char(desc_sig_element, _sig_element=True):
 class versionmodified(nodes.Admonition, nodes.TextElement):
     """Node for version change entries.
 
-    Currently used for "versionadded", "versionchanged" and "deprecated"
-    directives.
+    Currently used for "versionadded", "versionchanged", "deprecated"
+    and "versionremoved" directives.
     """
 
 
diff --git a/sphinx/builders/changes.py b/sphinx/builders/changes.py
index 19d688dd3bf..a3934663009 100644
--- a/sphinx/builders/changes.py
+++ b/sphinx/builders/changes.py
@@ -24,7 +24,7 @@
 
 class ChangesBuilder(Builder):
     """
-    Write a summary with all versionadded/changed directives.
+    Write a summary with all versionadded/changed/deprecated/removed directives.
     """
 
     name = 'changes'
@@ -43,6 +43,7 @@ def get_outdated_docs(self) -> str:
         'versionadded': 'added',
         'versionchanged': 'changed',
         'deprecated': 'deprecated',
+        'versionremoved': 'removed',
     }
 
     def write(self, *ignored: Any) -> None:
@@ -106,7 +107,9 @@ def write(self, *ignored: Any) -> None:
 
         hltext = ['.. versionadded:: %s' % version,
                   '.. versionchanged:: %s' % version,
-                  '.. deprecated:: %s' % version]
+                  '.. deprecated:: %s' % version,
+                  '.. versionremoved:: %s' % version,
+                  ]
 
         def hl(no: int, line: str) -> str:
             line = '<a name="L%s"> </a>' % no + html.escape(line)
@@ -143,7 +146,7 @@ def hl(no: int, line: str) -> str:
 
     def hl(self, text: str, version: str) -> str:
         text = html.escape(text)
-        for directive in ('versionchanged', 'versionadded', 'deprecated'):
+        for directive in ('versionchanged', 'versionadded', 'deprecated', 'versionremoved'):
             text = text.replace(f'.. {directive}:: {version}',
                                 f'<b>.. {directive}:: {version}</b>')
         return text
diff --git a/sphinx/domains/changeset.py b/sphinx/domains/changeset.py
index 2d594b29605..87959610283 100644
--- a/sphinx/domains/changeset.py
+++ b/sphinx/domains/changeset.py
@@ -23,12 +23,14 @@
     'versionadded':   _('New in version %s'),
     'versionchanged': _('Changed in version %s'),
     'deprecated':     _('Deprecated since version %s'),
+    'versionremoved': _('Removed in version %s'),
 }
 
 versionlabel_classes = {
     'versionadded':     'added',
     'versionchanged':   'changed',
     'deprecated':       'deprecated',
+    'versionremoved':   'removed',
 }
 
 
@@ -153,6 +155,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
     app.add_directive('deprecated', VersionChange)
     app.add_directive('versionadded', VersionChange)
     app.add_directive('versionchanged', VersionChange)
+    app.add_directive('versionremoved', VersionChange)
 
     return {
         'version': 'builtin',


Test patch:
diff --git a/tests/roots/test-changes/base.rst b/tests/roots/test-changes/base.rst
index a1b28398a89..81d90e66ef4 100644
--- a/tests/roots/test-changes/base.rst
+++ b/tests/roots/test-changes/base.rst
@@ -10,6 +10,9 @@ Version markup
 .. deprecated:: 0.6
    Boring stuff.
 
+.. versionremoved:: 0.6
+   Goodbye boring stuff.
+
 .. versionadded:: 1.2
 
    First paragraph of versionadded.
diff --git a/tests/roots/test-intl/versionchange.txt b/tests/roots/test-intl/versionchange.txt
index 4c57e14e3dd..764534246c0 100644
--- a/tests/roots/test-intl/versionchange.txt
+++ b/tests/roots/test-intl/versionchange.txt
@@ -14,3 +14,5 @@ i18n with versionchange
 .. versionchanged:: 1.0
 
    This is the *first* paragraph of versionchanged.
+
+.. versionremoved:: 1.0 This is the *first* paragraph of versionremoved.
diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po b/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po
index 5a8df380706..b1d786580f0 100644
--- a/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po
+++ b/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po
@@ -31,3 +31,5 @@ msgstr "THIS IS THE *FIRST* PARAGRAPH OF VERSIONADDED."
 msgid "This is the *first* paragraph of versionchanged."
 msgstr "THIS IS THE *FIRST* PARAGRAPH OF VERSIONCHANGED."
 
+msgid "This is the *first* paragraph of versionremoved."
+msgstr "THIS IS THE *FIRST* PARAGRAPH OF VERSIONREMOVED."
diff --git a/tests/roots/test-root/markup.txt b/tests/roots/test-root/markup.txt
index 20c642b1e11..ff677eb54c6 100644
--- a/tests/roots/test-root/markup.txt
+++ b/tests/roots/test-root/markup.txt
@@ -274,6 +274,9 @@ Version markup
 .. deprecated:: 0.6
    Boring stuff.
 
+.. versionremoved:: 0.6
+   Goodbye boring stuff.
+
 .. versionadded:: 1.2
 
    First paragraph of versionadded.
diff --git a/tests/test_builders/test_build_html_5_output.py b/tests/test_builders/test_build_html_5_output.py
index ff7e8f73580..9b50a106f0e 100644
--- a/tests/test_builders/test_build_html_5_output.py
+++ b/tests/test_builders/test_build_html_5_output.py
@@ -120,6 +120,7 @@ def checker(nodes):
      tail_check('First paragraph of versionchanged')),
     ('markup.html', ".//div[@class='versionchanged']/p",
      'Second paragraph of versionchanged'),
+    ('markup.html', ".//div[@class='versionremoved']/p/span", 'Removed in version 0.6: '),
     # footnote reference
     ('markup.html', ".//a[@class='footnote-reference brackets']", r'1'),
     # created by reference lookup
diff --git a/tests/test_intl/test_intl.py b/tests/test_intl/test_intl.py
index adc217946c4..4287182fa98 100644
--- a/tests/test_intl/test_intl.py
+++ b/tests/test_intl/test_intl.py
@@ -840,6 +840,12 @@ def get_content(result, name):
     matched_content = get_content(result, "versionchanged")
     assert expect3 == matched_content
 
+    expect4 = (
+        """<p><span class="versionmodified removed">Removed in version 1.0: </span>"""
+        """THIS IS THE <em>FIRST</em> PARAGRAPH OF VERSIONREMOVED.</p>\n""")
+    matched_content = get_content(result, "versionremoved")
+    assert expect4 == matched_content
+
 
 @sphinx_intl
 @pytest.mark.sphinx('html')