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 @@ + + + + {% for version in versions %} + {% set y = loop.index * LINE_HEIGHT %} + + {% if loop.index % 2 %} + + + {% endif %} + {% endfor %} + + {% for year in years %} + + {{ format_year(year) }} + + {% if not loop.last %} + + {% endif %} + {% endfor %} + + {% for version in versions %} + {% set y = loop.index * LINE_HEIGHT %} + + + + 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/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 - -