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 @@ + + + + + + {% for version in versions %} - {% set y = loop.index * LINE_HEIGHT %} + {% set y = version.y * line_height %} - {% if loop.index % 2 %} + {% if version.y % 2 %} {% endif %} {% endfor %} @@ -23,8 +29,8 @@ {% for year in years %} @@ -33,59 +39,159 @@ {% if not loop.last %} {% endif %} {% endfor %} + + + + + + + {% for version in versions %} - {% set y = loop.index * LINE_HEIGHT %} + + + {% set top_y = version.y * line_height - 1 * SCALE %} + {% set height = 1.25 * SCALE %} + {% set start_x = date_to_x(version.first_release_date) %} + {% set end_x = date_to_x(version.end_of_life_date) %} + {% set radius = 0.25 * SCALE %} + + {% set small_text_y = version.y * line_height - 0.1 * SCALE %} + + + {% set middle_x = ([end_x, date_to_x(version.start_security_date)]|min) %} + {% set left_width = (middle_x - start_x) %} + {% set right_width = (end_x - middle_x) %} + + {% if version.status != "end-of-life" %} + + + + + + {% else %} + + + {% endif %} + + + + {{ version.status }} + Python {{ version.key }} - - - {% set start_x = date_to_x(version.first_release_date) %} - {% set end_x = date_to_x(version.end_of_life_date) %} - {% set mid_x = (start_x + end_x) / 2 %} - - - {{ version.status }} - {% endfor %} 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.