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..a048e1c360 100644 --- a/_static/devguide_overrides.css +++ b/_static/devguide_overrides.css @@ -48,35 +48,66 @@ 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-status-security { + --status-bg-color: #FFDD44; + --status-border-color: #FF8800; +} + +.release-cycle-chart .release-cycle-status-bugfix { + --status-bg-color: #00DD22; + --status-border-color: #008844; +} + +.release-cycle-chart .release-cycle-status-prerelease { + --status-bg-color: teal; + --status-border-color: darkgreen; } -.release-cycle-chart .release-cycle-blob.release-cycle-blob-security { - fill: #FFDD44; - stroke: #FF8800; +.release-cycle-chart .release-cycle-status-feature { + --status-bg-color: #2222EE; + --status-border-color: #008888; } -.release-cycle-chart .release-cycle-blob.release-cycle-blob-bugfix { - fill: #00DD22; - stroke: #008844; +.release-cycle-chart .release-cycle-blob { + fill: var(--status-bg-color); + stroke: transparent; } -.release-cycle-chart .release-cycle-blob.release-cycle-blob-prerelease { - fill: teal; - stroke: darkgreen; +.release-cycle-chart .release-cycle-border { + fill: transparent; + stroke: var(--status-border-color); + stroke-width: 1.6px; } -.release-cycle-chart .release-cycle-blob.release-cycle-blob-feature { - fill: #2222EE; - stroke: #008888; +.release-cycle-chart .release-cycle-shade { + fill: transparent; + stroke: transparent; + + &.release-cycle-status-end-of-life { + fill: #DD2200; + stroke: #FF8888; + } + &.release-cycle-status-feature { + fill: var(--color-background-primary); + opacity: 50%; + } } .good pre { diff --git a/_tools/generate_release_cycle.py b/_tools/generate_release_cycle.py index 3a8fefec02..0c401ebbef 100644 --- a/_tools/generate_release_cycle.py +++ b/_tools/generate_release_cycle.py @@ -28,21 +28,52 @@ def parse_date(date_str: str) -> dt.date: 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"]) + version["first_release_date"] = r1 = parse_date(version["first_release"]) + version["start_security_date"] = r1 + dt.timedelta(days=2 * 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(".")], 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 +99,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/"), @@ -96,7 +127,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: @@ -115,20 +146,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_height=(self.sorted_versions[0]["y"] + 2) * LINE_HEIGHT, years=range(first_date.year, last_date.year + 1), LINE_HEIGHT=LINE_HEIGHT, + LEGEND_WIDTH=LEGEND_WIDTH, + RIGHT_MARGIN=RIGHT_MARGIN, 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 +177,13 @@ def main() -> None: args = parser.parse_args() versions = Versions() + print(versions.versions.keys()) + 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..b39d425f8e 100644 --- a/_tools/release_cycle_template.svg.jinja +++ b/_tools/release_cycle_template.svg.jinja @@ -4,11 +4,17 @@ class="release-cycle-chart" viewBox="0 0 {{ diagram_width * SCALE }} {{ diagram_height * SCALE }}" > + + + + + + {% for version in versions %} - {% set y = loop.index * LINE_HEIGHT %} + {% set y = version.y * LINE_HEIGHT %} - {% if loop.index % 2 %} + {% if version.y % 2 %} + + + + + - - - Python {{ version.key }} - + {% for version in versions %} + {% set y = version.y * LINE_HEIGHT %} {% 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 %} + + + {% set half_x = [end_x, date_to_x(version.start_security_date)]|min %} + {% set height = 1.25 * SCALE %} + {% set left_width = (half_x - start_x) * SCALE %} + {% set right_width = (end_x - half_x) * SCALE %} + {% set left_x = start_x * SCALE %} + {% set middle_x = half_x * SCALE %} + {% set right_x = half_x * SCALE %} + {% set recty = (y - 1) * SCALE %} + {% set radius_value = 0.25 * SCALE %} + + {% if version.status != "end-of-life" %} + + + + {% endif %} + + {% if version.status == "bugfix" %} + + bugfix + + {% elif version.status == "security" %} + + security + + {% elif version.status == "end-of-life" %} + + end-of-life + + {% else %} + + {{ version.status }} + + {% endif %} + + - {{ version.status }} + Python {{ version.key }} {% endfor %} diff --git a/make.ps1 b/make.ps1 index 71a8f56f4c..4cdc6b20cf 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..d6aac12b3d 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,6 +39,15 @@ Unsupported versions :file: include/end-of-life.csv +.. _versions-chart-all: + +Full chart +========== + +.. raw:: html + :file: include/release-cycle-all.svg + + Status key ==========