diff --git a/.github/workflows/release-cycle.yml b/.github/workflows/release-cycle.yml
deleted file mode 100644
index 8b676d78a4..0000000000
--- a/.github/workflows/release-cycle.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: Test release cycle
-
-on: [pull_request, push, workflow_dispatch]
-
-env:
- FORCE_COLOR: 1
-
-jobs:
- test:
- runs-on: ${{ matrix.os }}
- strategy:
- fail-fast: false
- matrix:
- os: [windows-latest, ubuntu-latest]
-
- steps:
- - uses: actions/checkout@v3
-
- - uses: actions/setup-python@v4
- with:
- python-version: "3"
-
- - name: Generate release cycle output
- run: python -I -bb -X dev -X warn_default_encoding -W error _tools/generate_release_cycle.py
-
- - name: Check for differences
- run: |
- git add .
- git status
- git diff --staged
- test $(git status --porcelain | wc -l) = 0
diff --git a/.gitignore b/.gitignore
index c74e78d0fa..df4dc9415a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ __pycache__/
# Distribution / packaging
.Python
env/
+ENV/
venv/
build/
develop-eggs/
@@ -80,13 +81,13 @@ celerybeat-schedule
# dotenv
.env
-# virtualenv
-venv/
-ENV/
-venv/
-
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
+
+# Generated CSV and SVG files
+include/branches.csv
+include/end-of-life.csv
+include/release-cycle.svg
diff --git a/Makefile b/Makefile
index 6e5ee16931..d86ebf5336 100644
--- a/Makefile
+++ b/Makefile
@@ -73,7 +73,7 @@ html: ensure-venv versions
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
-dirhtml: ensure-venv
+dirhtml: ensure-venv versions
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
@@ -189,14 +189,14 @@ serve:
"(see https://github.com/python/cpython/issues/80510)"
include/branches.csv: include/release-cycle.json
- $(PYTHON) _tools/generate_release_cycle.py
+ $(VENVDIR)/bin/python3 _tools/generate_release_cycle.py
include/end-of-life.csv: include/release-cycle.json
- $(PYTHON) _tools/generate_release_cycle.py
+ $(VENVDIR)/bin/python3 _tools/generate_release_cycle.py
-include/release-cycle.mmd: include/release-cycle.json
- $(PYTHON) _tools/generate_release_cycle.py
+include/release-cycle.svg: include/release-cycle.json
+ $(VENVDIR)/bin/python3 _tools/generate_release_cycle.py
.PHONY: versions
-versions: include/branches.csv include/end-of-life.csv include/release-cycle.mmd
+versions: venv include/branches.csv include/end-of-life.csv include/release-cycle.svg
@echo Release cycle data generated.
diff --git a/_static/devguide_overrides.css b/_static/devguide_overrides.css
index c34f471332..a93acbbff6 100644
--- a/_static/devguide_overrides.css
+++ b/_static/devguide_overrides.css
@@ -7,66 +7,69 @@
}
/* Release cycle chart */
-#python-release-cycle .mermaid .active0,
-#python-release-cycle .mermaid .active1,
-#python-release-cycle .mermaid .active2,
-#python-release-cycle .mermaid .active3 {
- fill: #00dd00;
- stroke: darkgreen;
+
+.release-cycle-chart {
+ width: 100%;
+}
+
+.release-cycle-chart .release-cycle-year-line {
+ stroke: var(--color-foreground-primary);
+ stroke-width: 0.8px;
+ opacity: 75%;
+}
+
+.release-cycle-chart .release-cycle-year-text {
+ fill: var(--color-foreground-primary);
}
-#python-release-cycle .mermaid .done0,
-#python-release-cycle .mermaid .done1,
-#python-release-cycle .mermaid .done2,
-#python-release-cycle .mermaid .done3 {
- fill: orange;
- stroke: darkorange;
+.release-cycle-chart .release-cycle-today-line {
+ stroke: var(--color-brand-primary);
+ stroke-width: 1.6px;
}
-#python-release-cycle .mermaid .task0,
-#python-release-cycle .mermaid .task1,
-#python-release-cycle .mermaid .task2,
-#python-release-cycle .mermaid .task3 {
- fill: #007acc;
- stroke: #004455;
+.release-cycle-chart .release-cycle-row-shade {
+ fill: var(--color-background-item);
+ opacity: 50%;
}
-#python-release-cycle .mermaid .section0,
-#python-release-cycle .mermaid .section2 {
- fill: darkgrey;
+.release-cycle-chart .release-cycle-version-label {
+ fill: var(--color-foreground-primary);
}
-/* Set master colours */
-:root {
- --mermaid-section1-3: white;
- --mermaid-text-color: black;
+.release-cycle-chart .release-cycle-blob {
+ stroke-width: 1.6px;
+ /* default colours, overriden below for individual statuses */
+ fill: var(--color-background-primary);
+ stroke: var(--color-foreground-primary);
}
-@media (prefers-color-scheme: dark) {
- body[data-theme=auto] {
- --mermaid-section1-3: black;
- --mermaid-text-color: #ffffffcc;
- }
+.release-cycle-chart .release-cycle-blob-label {
+ /* white looks good on both light & dark */
+ fill: white;
}
-body[data-theme=dark] {
- --mermaid-section1-3: black;
- --mermaid-text-color: #ffffffcc;
+
+.release-cycle-chart .release-cycle-blob-label.release-cycle-blob-security,
+.release-cycle-chart .release-cycle-blob-label.release-cycle-blob-bugfix {
+ /* but use black to improve contrast for lighter backgrounds */
+ fill: black;
+}
+
+.release-cycle-chart .release-cycle-blob.release-cycle-blob-end-of-life {
+ fill: #DD2200;
+ stroke: #FF8888;
+}
+
+.release-cycle-chart .release-cycle-blob.release-cycle-blob-security {
+ fill: #FFDD44;
+ stroke: #FF8800;
}
-#python-release-cycle .mermaid .section1,
-#python-release-cycle .mermaid .section3 {
- fill: var(--mermaid-section1-3);
+.release-cycle-chart .release-cycle-blob.release-cycle-blob-bugfix {
+ fill: #00DD22;
+ stroke: #008844;
}
-#python-release-cycle .mermaid .grid .tick text,
-#python-release-cycle .mermaid .sectionTitle0,
-#python-release-cycle .mermaid .sectionTitle1,
-#python-release-cycle .mermaid .sectionTitle2,
-#python-release-cycle .mermaid .sectionTitle3,
-#python-release-cycle .mermaid .taskTextOutside0,
-#python-release-cycle .mermaid .taskTextOutside1,
-#python-release-cycle .mermaid .taskTextOutside2,
-#python-release-cycle .mermaid .taskTextOutside3,
-#python-release-cycle .mermaid .titleText {
- fill: var(--mermaid-text-color);
+.release-cycle-chart .release-cycle-blob.release-cycle-blob-feature {
+ fill: #2222EE;
+ stroke: #008888;
}
diff --git a/_tools/generate_release_cycle.py b/_tools/generate_release_cycle.py
index 7e9e69ad83..59e9fcbbec 100644
--- a/_tools/generate_release_cycle.py
+++ b/_tools/generate_release_cycle.py
@@ -1,28 +1,12 @@
-"""Read in a JSON and generate two CSVs and a Mermaid file."""
+"""Read in a JSON and generate two CSVs and an SVG file."""
from __future__ import annotations
+import argparse
import csv
import datetime as dt
import json
-MERMAID_HEADER = """
-gantt
- dateFormat YYYY-MM-DD
- title Python release cycle
- axisFormat %Y
-""".lstrip()
-
-MERMAID_SECTION = """
- section Python {version}
- {release_status} :{mermaid_status} python{version}, {first_release},{eol}
-""" # noqa: E501
-
-MERMAID_STATUS_MAPPING = {
- "feature": "",
- "bugfix": "active,",
- "security": "done,",
- "end-of-life": "crit,",
-}
+import jinja2
def csv_date(date_str: str, now_str: str) -> str:
@@ -33,23 +17,28 @@ def csv_date(date_str: str, now_str: str) -> str:
return date_str
-def mermaid_date(date_str: str) -> str:
- """Format a date for Mermaid."""
+def parse_date(date_str: str) -> dt.date:
if len(date_str) == len("yyyy-mm"):
- # Mermaid needs a full yyyy-mm-dd, so let's approximate
- date_str = f"{date_str}-01"
- return date_str
+ # We need a full yyyy-mm-dd, so let's approximate
+ return dt.date.fromisoformat(date_str + "-01")
+ return dt.date.fromisoformat(date_str)
class Versions:
- """For converting JSON to CSV and Mermaid."""
+ """For converting JSON to CSV and SVG."""
def __init__(self) -> None:
with open("include/release-cycle.json", encoding="UTF-8") as in_file:
self.versions = json.load(in_file)
+
+ # Generate a few additional fields
+ for key, version in self.versions.items():
+ version["key"] = key
+ version["first_release_date"] = parse_date(version["first_release"])
+ version["end_of_life_date"] = parse_date(version["end_of_life"])
self.sorted_versions = sorted(
- self.versions.items(),
- key=lambda k: [int(i) for i in k[0].split(".")],
+ self.versions.values(),
+ key=lambda v: [int(i) for i in v["key"].split(".")],
reverse=True,
)
@@ -59,7 +48,7 @@ def write_csv(self) -> None:
versions_by_category = {"branches": {}, "end-of-life": {}}
headers = None
- for version, details in self.sorted_versions:
+ for details in self.sorted_versions:
row = {
"Branch": details["branch"],
"Schedule": f":pep:`{details['pep']}`",
@@ -70,7 +59,7 @@ def write_csv(self) -> None:
}
headers = row.keys()
cat = "end-of-life" if details["status"] == "end-of-life" else "branches"
- versions_by_category[cat][version] = row
+ versions_by_category[cat][details["key"]] = row
for cat, versions in versions_by_category.items():
with open(f"include/{cat}.csv", "w", encoding="UTF-8", newline="") as file:
@@ -78,30 +67,85 @@ def write_csv(self) -> None:
csv_file.writeheader()
csv_file.writerows(versions.values())
- def write_mermaid(self) -> None:
- """Output Mermaid file."""
- out = [MERMAID_HEADER]
-
- for version, details in reversed(self.versions.items()):
- v = MERMAID_SECTION.format(
- version=version,
- first_release=details["first_release"],
- eol=mermaid_date(details["end_of_life"]),
- release_status=details["status"],
- mermaid_status=MERMAID_STATUS_MAPPING[details["status"]],
- )
- out.append(v)
+ def write_svg(self, today: str) -> None:
+ """Output SVG file."""
+ env = jinja2.Environment(
+ loader=jinja2.FileSystemLoader("_tools/"),
+ autoescape=True,
+ lstrip_blocks=True,
+ trim_blocks=True,
+ undefined=jinja2.StrictUndefined,
+ )
+ template = env.get_template("release_cycle_template.svg.jinja")
+
+ # Scale. Should be roughly the pixel size of the font.
+ # All later sizes are multiplied by this, so you can think of all other
+ # numbers being multiples of the font size, like using `em` units in
+ # CSS.
+ # (Ideally we'd actually use `em` units, but SVG viewBox doesn't take
+ # those.)
+ SCALE = 18
+
+ # Width of the drawing and main parts
+ DIAGRAM_WIDTH = 46
+ LEGEND_WIDTH = 7
+ RIGHT_MARGIN = 0.5
+
+ # Height of one line. If you change this you'll need to tweak
+ # some positioning numbers in the template as well.
+ LINE_HEIGHT = 1.5
+
+ first_date = min(ver["first_release_date"] for ver in self.sorted_versions)
+ last_date = max(ver["end_of_life_date"] for ver in self.sorted_versions)
+
+ def date_to_x(date: dt.date) -> float:
+ """Convert datetime.date to an SVG X coordinate"""
+ num_days = (date - first_date).days
+ total_days = (last_date - first_date).days
+ ratio = num_days / total_days
+ x = ratio * (DIAGRAM_WIDTH - LEGEND_WIDTH - RIGHT_MARGIN)
+ return x + LEGEND_WIDTH
+
+ def year_to_x(year: int) -> float:
+ """Convert year number to an SVG X coordinate of 1st January"""
+ return date_to_x(dt.date(year, 1, 1))
+
+ def format_year(year: int) -> str:
+ """Format year number for display"""
+ return f"'{year % 100:02}"
with open(
- "include/release-cycle.mmd", "w", encoding="UTF-8", newline="\n"
+ "include/release-cycle.svg", "w", encoding="UTF-8", newline="\n"
) as f:
- f.writelines(out)
+ template.stream(
+ SCALE=SCALE,
+ diagram_width=DIAGRAM_WIDTH,
+ diagram_height=(len(self.sorted_versions) + 2) * LINE_HEIGHT,
+ years=range(first_date.year, last_date.year + 1),
+ LINE_HEIGHT=LINE_HEIGHT,
+ versions=list(reversed(self.sorted_versions)),
+ today=dt.datetime.strptime(today, "%Y-%m-%d").date(),
+ year_to_x=year_to_x,
+ date_to_x=date_to_x,
+ format_year=format_year,
+ ).dump(f)
def main() -> None:
+ parser = argparse.ArgumentParser(
+ description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
+ )
+ parser.add_argument(
+ "--today",
+ default=str(dt.date.today()),
+ metavar=" YYYY-MM-DD",
+ help="Override today for testing",
+ )
+ args = parser.parse_args()
+
versions = Versions()
versions.write_csv()
- versions.write_mermaid()
+ versions.write_svg(args.today)
if __name__ == "__main__":
diff --git a/_tools/release_cycle_template.svg.jinja b/_tools/release_cycle_template.svg.jinja
new file mode 100644
index 0000000000..5d39d307a5
--- /dev/null
+++ b/_tools/release_cycle_template.svg.jinja
@@ -0,0 +1,91 @@
+
+
diff --git a/conf.py b/conf.py
index 29563db509..849538be72 100644
--- a/conf.py
+++ b/conf.py
@@ -10,7 +10,6 @@
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx_copybutton',
- 'sphinxcontrib.mermaid',
'sphinxext.opengraph',
'sphinxext.rediraffe',
]
diff --git a/include/branches.csv b/include/branches.csv
deleted file mode 100644
index bfec9da84b..0000000000
--- a/include/branches.csv
+++ /dev/null
@@ -1,7 +0,0 @@
-Branch,Schedule,Status,First release,End of life,Release manager
-main,:pep:`693`,feature,*2023-10-02*,*2028-10*,Thomas Wouters
-3.11,:pep:`664`,bugfix,2022-10-24,*2027-10*,Pablo Galindo Salgado
-3.10,:pep:`619`,bugfix,2021-10-04,*2026-10*,Pablo Galindo Salgado
-3.9,:pep:`596`,security,2020-10-05,*2025-10*,Łukasz Langa
-3.8,:pep:`569`,security,2019-10-14,*2024-10*,Łukasz Langa
-3.7,:pep:`537`,security,2018-06-27,*2023-06-27*,Ned Deily
diff --git a/include/end-of-life.csv b/include/end-of-life.csv
deleted file mode 100644
index be9fe1b728..0000000000
--- a/include/end-of-life.csv
+++ /dev/null
@@ -1,10 +0,0 @@
-Branch,Schedule,Status,First release,End of life,Release manager
-3.6,:pep:`494`,end-of-life,2016-12-23,2021-12-23,Ned Deily
-3.5,:pep:`478`,end-of-life,2015-09-13,2020-09-30,Larry Hastings
-3.4,:pep:`429`,end-of-life,2014-03-16,2019-03-18,Larry Hastings
-3.3,:pep:`398`,end-of-life,2012-09-29,2017-09-29,"Georg Brandl, Ned Deily (3.3.7+)"
-3.2,:pep:`392`,end-of-life,2011-02-20,2016-02-20,Georg Brandl
-3.1,:pep:`375`,end-of-life,2009-06-27,2012-04-09,Benjamin Peterson
-3.0,:pep:`361`,end-of-life,2008-12-03,2009-06-27,Barry Warsaw
-2.7,:pep:`373`,end-of-life,2010-07-03,2020-01-01,Benjamin Peterson
-2.6,:pep:`361`,end-of-life,2008-10-01,2013-10-29,Barry Warsaw
diff --git a/include/release-cycle.mmd b/include/release-cycle.mmd
deleted file mode 100644
index fc437ab590..0000000000
--- a/include/release-cycle.mmd
+++ /dev/null
@@ -1,49 +0,0 @@
-gantt
- dateFormat YYYY-MM-DD
- title Python release cycle
- axisFormat %Y
-
- section Python 2.6
- end-of-life :crit, python2.6, 2008-10-01,2013-10-29
-
- section Python 3.0
- end-of-life :crit, python3.0, 2008-12-03,2009-06-27
-
- section Python 3.1
- end-of-life :crit, python3.1, 2009-06-27,2012-04-09
-
- section Python 2.7
- end-of-life :crit, python2.7, 2010-07-03,2020-01-01
-
- section Python 3.2
- end-of-life :crit, python3.2, 2011-02-20,2016-02-20
-
- section Python 3.3
- end-of-life :crit, python3.3, 2012-09-29,2017-09-29
-
- section Python 3.4
- end-of-life :crit, python3.4, 2014-03-16,2019-03-18
-
- section Python 3.5
- end-of-life :crit, python3.5, 2015-09-13,2020-09-30
-
- section Python 3.6
- end-of-life :crit, python3.6, 2016-12-23,2021-12-23
-
- section Python 3.7
- security :done, python3.7, 2018-06-27,2023-06-27
-
- section Python 3.8
- security :done, python3.8, 2019-10-14,2024-10-01
-
- section Python 3.9
- security :done, python3.9, 2020-10-05,2025-10-01
-
- section Python 3.10
- bugfix :active, python3.10, 2021-10-04,2026-10-01
-
- section Python 3.11
- bugfix :active, python3.11, 2022-10-24,2027-10-01
-
- section Python 3.12
- feature : python3.12, 2023-10-02,2028-10-01
diff --git a/requirements.txt b/requirements.txt
index 3370b57dab..d9e6a324a0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
Sphinx==6.0.0
furo>=2022.6.4
+jinja2
sphinx-lint==0.6.7
sphinx_copybutton>=0.3.3
-sphinxcontrib-mermaid
sphinxext-opengraph>=0.7.1
sphinxext-rediraffe
diff --git a/versions.rst b/versions.rst
index c36f7e6725..fd28b3f85d 100644
--- a/versions.rst
+++ b/versions.rst
@@ -13,8 +13,8 @@ version can be found on the `download page `_
Python Release Cycle
====================
-.. mermaid:: include/release-cycle.mmd
-
+.. raw:: html
+ :file: include/release-cycle.svg
Supported Versions
==================
@@ -55,16 +55,3 @@ See also the :ref:`devcycle` page for more information about branches.
By default, the end-of-life is scheduled 5 years after the first release,
but can be adjusted by the release manager of each branch. All Python 2
versions have reached end-of-life.
-
-.. raw:: html
-
-