Add deprecated-removed directive to Sphinx (#4912)

* Add `deprecated-removed` directive to Sphinx

* Add equivalent function to internal utils
This commit is contained in:
jack1142 2021-04-03 18:48:17 +02:00 committed by GitHub
parent 76bb65912e
commit 0144cbf88b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 149 additions and 0 deletions

View File

@ -0,0 +1,129 @@
"""
A Sphinx extension adding a ``deprecated-removed`` directive that works
similarly to CPython's directive with the same name.
The key difference is that instead of passing the version of planned removal,
the writer must provide the minimum amount of days that must pass
since the date of the release it was deprecated in.
Due to lack of a concrete release schedule for Red, this ensures that
we give enough time to people affected by the changes no matter
when the releases actually happen.
`DeprecatedRemoved` class is heavily based on
`sphinx.domains.changeset.VersionChange` class that is available at:
https://github.com/sphinx-doc/sphinx/blob/0949735210abaa05b6448e531984f159403053f4/sphinx/domains/changeset.py
Copyright 2007-2020 by the Sphinx team, see AUTHORS:
https://github.com/sphinx-doc/sphinx/blob/82f495fed386c798735adf675f867b95d61ee0e1/AUTHORS
The original copy was distributed under BSD License and this derivative work
is distributed under GNU GPL Version 3.
"""
import datetime
import multiprocessing
import subprocess
from typing import Any, Dict, List, Optional
from docutils import nodes
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective
class TagDateCache:
def __init__(self) -> None:
self._tags: Dict[str, datetime.date] = {}
def _populate_tags(self) -> None:
with _LOCK:
if self._tags:
return
out = subprocess.check_output(
("git", "tag", "-l", "--format", "%(creatordate:raw)\t%(refname:short)"),
text=True,
)
lines = out.splitlines(False)
for line in lines:
creator_date, tag_name = line.split("\t", maxsplit=1)
timestamp = int(creator_date.split(" ", maxsplit=1)[0])
self._tags[tag_name] = datetime.datetime.fromtimestamp(
timestamp, tz=datetime.timezone.utc
).date()
def get_tag_date(self, tag_name: str) -> Optional[datetime.date]:
self._populate_tags()
return self._tags.get(tag_name)
_LOCK = multiprocessing.Manager().Lock()
_TAGS = TagDateCache()
class DeprecatedRemoved(SphinxDirective):
has_content = True
required_arguments = 2
optional_arguments = 1
final_argument_whitespace = True
def run(self) -> List[nodes.Node]:
# Some Sphinx stuff
node = addnodes.versionmodified()
node.document = self.state.document
self.set_source_info(node)
node["type"] = self.name
node["version"] = tuple(self.arguments)
if len(self.arguments) == 3:
inodes, messages = self.state.inline_text(self.arguments[2], self.lineno + 1)
para = nodes.paragraph(self.arguments[2], "", *inodes, translatable=False)
self.set_source_info(para)
node.append(para)
else:
messages = []
# Text generation
deprecation_version = self.arguments[0]
minimum_days = int(self.arguments[1])
tag_date = _TAGS.get_tag_date(deprecation_version)
text = (
f"Will be deprecated in version {deprecation_version},"
" and removed in the first minor version that gets released"
f" after {minimum_days} days since deprecation"
if tag_date is None
else f"Deprecated since version {deprecation_version},"
" will be removed in the first minor version that gets released"
f" after {tag_date + datetime.timedelta(days=minimum_days)}"
)
# More Sphinx stuff
if self.content:
self.state.nested_parse(self.content, self.content_offset, node)
classes = ["versionmodified"]
if len(node):
if isinstance(node[0], nodes.paragraph) and node[0].rawsource:
content = nodes.inline(node[0].rawsource, translatable=True)
content.source = node[0].source
content.line = node[0].line
content += node[0].children
node[0].replace_self(nodes.paragraph("", "", content, translatable=False))
node[0].insert(0, nodes.inline("", f"{text}: ", classes=classes))
else:
para = nodes.paragraph(
"", "", nodes.inline("", f"{text}.", classes=classes), translatable=False
)
node.append(para)
ret = [node]
ret += messages
return ret
def setup(app: Sphinx) -> Dict[str, Any]:
app.add_directive("deprecated-removed", DeprecatedRemoved)
return {
"version": "1.0",
"parallel_read_safe": True,
}

View File

@ -21,6 +21,7 @@ import os
import sys import sys
sys.path.insert(0, os.path.abspath("..")) sys.path.insert(0, os.path.abspath(".."))
sys.path.insert(0, os.path.abspath("_ext"))
os.environ["BUILDING_DOCS"] = "1" os.environ["BUILDING_DOCS"] = "1"
@ -42,6 +43,7 @@ extensions = [
"sphinx.ext.napoleon", "sphinx.ext.napoleon",
"sphinx.ext.doctest", "sphinx.ext.doctest",
"sphinxcontrib_trio", "sphinxcontrib_trio",
"deprecated_removed",
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.

View File

@ -8,6 +8,7 @@ import os
import re import re
import shutil import shutil
import tarfile import tarfile
import warnings
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import ( from typing import (
@ -46,6 +47,7 @@ __all__ = (
"send_to_owners_with_prefix_replaced", "send_to_owners_with_prefix_replaced",
"expected_version", "expected_version",
"fetch_latest_red_version_info", "fetch_latest_red_version_info",
"deprecated_removed",
) )
@ -317,3 +319,19 @@ async def fetch_latest_red_version_info() -> Tuple[Optional[VersionInfo], Option
required_python = data["info"]["requires_python"] required_python = data["info"]["requires_python"]
return release, required_python return release, required_python
def deprecated_removed(
deprecation_target: str,
deprecation_version: str,
minimum_days: int,
message: str = "",
stacklevel: int = 1,
) -> None:
warnings.warn(
f"{deprecation_target} is deprecated since version {deprecation_version}"
" and will be removed in the first minor version that gets released"
f" after {minimum_days} days since deprecation. {message}",
DeprecationWarning,
stacklevel=stacklevel + 1,
)