diff --git a/.gitignore b/.gitignore
index df4dc9415a..b712492014 100644
--- a/.gitignore
+++ b/.gitignore
@@ -91,3 +91,4 @@ celerybeat-schedule
include/branches.csv
include/end-of-life.csv
include/release-cycle.svg
+include/release-cycle-all.svg
diff --git a/Makefile b/Makefile
index 5a33d50897..6baf33b325 100644
--- a/Makefile
+++ b/Makefile
@@ -22,6 +22,7 @@ REQUIREMENTS = requirements.txt
_ALL_SPHINX_OPTS = --jobs $(JOBS) $(SPHINXOPTS)
_RELEASE_CYCLE = include/branches.csv \
include/end-of-life.csv \
+ include/release-cycle-all.svg \
include/release-cycle.svg
.PHONY: help
diff --git a/_static/devguide_overrides.css b/_static/devguide_overrides.css
index 8e2c7c6fca..d47a1e6025 100644
--- a/_static/devguide_overrides.css
+++ b/_static/devguide_overrides.css
@@ -48,35 +48,57 @@
fill: white;
}
-.release-cycle-chart .release-cycle-blob-label.release-cycle-blob-security,
-.release-cycle-chart .release-cycle-blob-label.release-cycle-blob-bugfix {
+.release-cycle-chart .release-cycle-blob-label.release-cycle-status-security,
+.release-cycle-chart .release-cycle-blob-label.release-cycle-status-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-label.release-cycle-status-end-of-life,
+.release-cycle-chart .release-cycle-blob-label.release-cycle-status-feature {
+ /* and FG when it's not in a blob */
+ fill: var(--color-foreground-primary);
+}
+
+.release-cycle-chart .release-cycle-status-end-of-life {
+ --status-bg-color: #DD2200;
+ --status-border-color: #FF8888;
}
-.release-cycle-chart .release-cycle-blob.release-cycle-blob-security {
- fill: #FFDD44;
- stroke: #FF8800;
+.release-cycle-chart .release-cycle-status-security {
+ --status-bg-color: #FFDD44;
+ --status-border-color: #FF8800;
}
-.release-cycle-chart .release-cycle-blob.release-cycle-blob-bugfix {
- fill: #00DD22;
- stroke: #008844;
+.release-cycle-chart .release-cycle-status-bugfix {
+ --status-bg-color: #00DD22;
+ --status-border-color: #008844;
}
-.release-cycle-chart .release-cycle-blob.release-cycle-blob-prerelease {
- fill: teal;
- stroke: darkgreen;
+.release-cycle-chart .release-cycle-status-prerelease {
+ --status-bg-color: teal;
+ --status-border-color: darkgreen;
}
-.release-cycle-chart .release-cycle-blob.release-cycle-blob-feature {
- fill: #2222EE;
- stroke: #008888;
+.release-cycle-chart .release-cycle-status-feature {
+ --status-bg-color: #2222EE;
+ --status-border-color: #008888;
+}
+
+.release-cycle-chart .release-cycle-blob {
+ fill: var(--status-bg-color);
+ stroke: transparent;
+}
+
+.release-cycle-chart .release-cycle-blob-full {
+ fill: var(--status-bg-color);
+ stroke: var(--status-border-color);
+}
+
+.release-cycle-chart .release-cycle-border {
+ fill: transparent;
+ stroke: var(--status-border-color);
+ stroke-width: 1.6px;
}
.good pre {
diff --git a/_tools/generate_release_cycle.py b/_tools/generate_release_cycle.py
index 3a8fefec02..63d98cfced 100644
--- a/_tools/generate_release_cycle.py
+++ b/_tools/generate_release_cycle.py
@@ -25,24 +25,64 @@ def parse_date(date_str: str) -> dt.date:
return dt.date.fromisoformat(date_str)
+def parse_version(ver: str) -> list[int]:
+ return [int(i) for i in ver["key"].split(".")]
+
+
class Versions:
"""For converting JSON to CSV and SVG."""
- def __init__(self) -> None:
+ def __init__(self, *, limit_to_active=False, special_py27=False) -> 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"])
+ ver_info = parse_version(version)
+ if ver_info >= [3, 13]:
+ full_years = 2
+ else:
+ full_years = 1.5
+ version["first_release_date"] = r1 = parse_date(version["first_release"])
+ version["start_security_date"] = r1 + dt.timedelta(days=full_years * 365)
version["end_of_life_date"] = parse_date(version["end_of_life"])
+
+ self.cutoff = min(ver["first_release_date"] for ver in self.versions.values())
+
+ if limit_to_active:
+ self.cutoff = min(
+ version["first_release_date"]
+ for version in self.versions.values()
+ if version["status"] != "end-of-life"
+ )
+ self.versions = {
+ key: version
+ for key, version in self.versions.items()
+ if version["end_of_life_date"] >= self.cutoff
+ or (special_py27 and key == "2.7")
+ }
+ if special_py27:
+ self.cutoff = min(self.cutoff, dt.date(2019, 8, 1))
+ self.id_key = "active"
+ else:
+ self.id_key = "all"
+
self.sorted_versions = sorted(
self.versions.values(),
- key=lambda v: [int(i) for i in v["key"].split(".")],
+ key=parse_version,
reverse=True,
)
+ # Set the row (Y coordinate) for the chart, to allow a gap between 2.7
+ # and the rest
+ y = len(self.sorted_versions) + (1 if special_py27 else 0)
+ for version in self.sorted_versions:
+ if special_py27 and version["key"] == "2.7":
+ y -= 1
+ version["y"] = y
+ y -= 1
+
def write_csv(self) -> None:
"""Output CSV files."""
now_str = str(dt.datetime.now(dt.timezone.utc))
@@ -68,7 +108,7 @@ def write_csv(self) -> None:
csv_file.writeheader()
csv_file.writerows(versions.values())
- def write_svg(self, today: str) -> None:
+ def write_svg(self, today: str, out_path: str) -> None:
"""Output SVG file."""
env = jinja2.Environment(
loader=jinja2.FileSystemLoader("_tools/"),
@@ -85,6 +125,8 @@ def write_svg(self, today: str) -> None:
# CSS.
# (Ideally we'd actually use `em` units, but SVG viewBox doesn't take
# those.)
+
+ # Uppercase sizes are un-scaled
SCALE = 18
# Width of the drawing and main parts
@@ -96,7 +138,7 @@ def write_svg(self, today: str) -> None:
# 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)
+ first_date = self.cutoff
last_date = max(ver["end_of_life_date"] for ver in self.sorted_versions)
def date_to_x(date: dt.date) -> float:
@@ -105,7 +147,7 @@ def date_to_x(date: dt.date) -> float:
total_days = (last_date - first_date).days
ratio = num_days / total_days
x = ratio * (DIAGRAM_WIDTH - LEGEND_WIDTH - RIGHT_MARGIN)
- return x + LEGEND_WIDTH
+ return (x + LEGEND_WIDTH) * SCALE
def year_to_x(year: int) -> float:
"""Convert year number to an SVG X coordinate of 1st January"""
@@ -115,20 +157,21 @@ def format_year(year: int) -> str:
"""Format year number for display"""
return f"'{year % 100:02}"
- with open(
- "include/release-cycle.svg", "w", encoding="UTF-8", newline="\n"
- ) as f:
+ with open(out_path, "w", encoding="UTF-8", newline="\n") as f:
template.stream(
SCALE=SCALE,
- diagram_width=DIAGRAM_WIDTH,
- diagram_height=(len(self.sorted_versions) + 2) * LINE_HEIGHT,
+ diagram_width=DIAGRAM_WIDTH * SCALE,
+ diagram_height=(self.sorted_versions[0]["y"] + 2) * LINE_HEIGHT * SCALE,
years=range(first_date.year, last_date.year + 1),
- LINE_HEIGHT=LINE_HEIGHT,
+ line_height=LINE_HEIGHT * SCALE,
+ legend_width=LEGEND_WIDTH * SCALE,
+ right_margin=RIGHT_MARGIN * SCALE,
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,
+ id_key=self.id_key,
).dump(f)
@@ -145,8 +188,12 @@ def main() -> None:
args = parser.parse_args()
versions = Versions()
+ assert len(versions.versions) > 10
versions.write_csv()
- versions.write_svg(args.today)
+ versions.write_svg(args.today, "include/release-cycle-all.svg")
+
+ versions = Versions(limit_to_active=True, special_py27=True)
+ versions.write_svg(args.today, "include/release-cycle.svg")
if __name__ == "__main__":
diff --git a/_tools/release_cycle_template.svg.jinja b/_tools/release_cycle_template.svg.jinja
index 5d39d307a5..d3d5866a06 100644
--- a/_tools/release_cycle_template.svg.jinja
+++ b/_tools/release_cycle_template.svg.jinja
@@ -2,20 +2,26 @@
diff --git a/make.ps1 b/make.ps1
index 71a8f56f4c..d6b5f3293e 100644
--- a/make.ps1
+++ b/make.ps1
@@ -64,7 +64,8 @@ if ($target -Eq "clean") {
$ToClean = @(
$BUILDDIR,
$_VENV_DIR,
- "include/branches.csv", "include/end-of-life.csv", "include/release-cycle.svg"
+ "include/branches.csv", "include/end-of-life.csv",
+ "include/release-cycle.svg", "include/release-cycle-all.svg"
)
foreach ($item in $ToClean) {
if (Test-Path -Path $item) {
diff --git a/versions.rst b/versions.rst
index db7f946829..8cfd259f8a 100644
--- a/versions.rst
+++ b/versions.rst
@@ -10,13 +10,12 @@ branch that accepts new features. The latest release for each Python
version can be found on the `download page `_.
-Python release cycle
-====================
-
.. raw:: html
:file: include/release-cycle.svg
-Another useful visualization is `endoflife.date/python `_.
+(See :ref:`below ` for a chart with older versions.
+Another useful visualization is `endoflife.date/python `_.)
+
Supported versions
==================
@@ -40,20 +39,37 @@ Unsupported versions
:file: include/end-of-life.csv
+.. _full-chart:
+
+Full chart
+==========
+
+.. raw:: html
+ :file: include/release-cycle-all.svg
+
+
Status key
==========
-:feature: new features, bugfixes, and security fixes are accepted.
-:prerelease: feature fixes, bugfixes, and security fixes are accepted for the
- upcoming feature release.
-:bugfix: bugfixes and security fixes are accepted, new binaries are still
- released. (Also called **maintenance** mode or **stable** release)
-:security: only security fixes are accepted and no more binaries are released,
- but new source-only versions can be released
-:end-of-life: release cycle is frozen; no further changes can be pushed to it.
+Python releases go through five phases, as described in :pep:`602`. Release
+managers can adjust specific dates as needed.
-See also the :ref:`devcycle` page for more information about branches and backporting.
+:feature: Before the first beta, the next full release can accept new features,
+ bug fixes, and security fixes.
+
+:prerelease: After the first beta, no new features can go in, but feature fixes
+ (including significant changes to new features), bug fixes, and security fixes
+ are accepted for the upcoming feature release.
+
+:bugfix: Once a version has been fully released, bug fixes and security fixes are
+ accepted. New binaries are built and released roughly every two months. This
+ phase is also called **maintenance** mode or **stable** release.
-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.
+:security: After two years (18 months for versions before 3.13), only security
+ fixes are accepted and no more binaries are released. New source-only versions
+ can be released as needed.
+
+:end-of-life: Five years after a release, support ends. The release cycle is
+ frozen; no further changes are allowed.
+
+See also the :ref:`devcycle` page for more information about branches and backporting.