diff --git a/.github/workflows/release-cycle.yml b/.github/workflows/release-cycle.yml new file mode 100644 index 0000000000..8b676d78a4 --- /dev/null +++ b/.github/workflows/release-cycle.yml @@ -0,0 +1,31 @@ +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/Makefile b/Makefile index b64aecc453..6e5ee16931 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ help: @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " check to run a check for frequent markup errors" + @echo " versions to update release cycle after changing release-cycle.json" .PHONY: clean clean: clean-venv @@ -66,7 +67,7 @@ ensure-venv: fi .PHONY: html -html: ensure-venv +html: ensure-venv versions $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." @@ -186,3 +187,16 @@ check: ensure-venv serve: @echo "The 'serve' target was removed, use 'htmlview' instead" \ "(see https://github.com/python/cpython/issues/80510)" + +include/branches.csv: include/release-cycle.json + $(PYTHON) _tools/generate_release_cycle.py + +include/end-of-life.csv: include/release-cycle.json + $(PYTHON) _tools/generate_release_cycle.py + +include/release-cycle.mmd: include/release-cycle.json + $(PYTHON) _tools/generate_release_cycle.py + +.PHONY: versions +versions: include/branches.csv include/end-of-life.csv include/release-cycle.mmd + @echo Release cycle data generated. diff --git a/_static/devguide_overrides.css b/_static/devguide_overrides.css index 8cafebebfd..c34f471332 100644 --- a/_static/devguide_overrides.css +++ b/_static/devguide_overrides.css @@ -5,3 +5,68 @@ width: 111px; height: 110px; } + +/* 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; +} + +#python-release-cycle .mermaid .done0, +#python-release-cycle .mermaid .done1, +#python-release-cycle .mermaid .done2, +#python-release-cycle .mermaid .done3 { + fill: orange; + stroke: darkorange; +} + +#python-release-cycle .mermaid .task0, +#python-release-cycle .mermaid .task1, +#python-release-cycle .mermaid .task2, +#python-release-cycle .mermaid .task3 { + fill: #007acc; + stroke: #004455; +} + +#python-release-cycle .mermaid .section0, +#python-release-cycle .mermaid .section2 { + fill: darkgrey; +} + +/* Set master colours */ +:root { + --mermaid-section1-3: white; + --mermaid-text-color: black; +} + +@media (prefers-color-scheme: dark) { + body[data-theme=auto] { + --mermaid-section1-3: black; + --mermaid-text-color: #ffffffcc; + } +} +body[data-theme=dark] { + --mermaid-section1-3: black; + --mermaid-text-color: #ffffffcc; +} + +#python-release-cycle .mermaid .section1, +#python-release-cycle .mermaid .section3 { + fill: var(--mermaid-section1-3); +} + +#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); +} diff --git a/_tools/generate_release_cycle.py b/_tools/generate_release_cycle.py new file mode 100644 index 0000000000..7e9e69ad83 --- /dev/null +++ b/_tools/generate_release_cycle.py @@ -0,0 +1,108 @@ +"""Read in a JSON and generate two CSVs and a Mermaid file.""" +from __future__ import annotations + +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,", +} + + +def csv_date(date_str: str, now_str: str) -> str: + """Format a date for CSV.""" + if date_str > now_str: + # Future, add italics + return f"*{date_str}*" + return date_str + + +def mermaid_date(date_str: str) -> str: + """Format a date for Mermaid.""" + 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 + + +class Versions: + """For converting JSON to CSV and Mermaid.""" + + def __init__(self) -> None: + with open("include/release-cycle.json", encoding="UTF-8") as in_file: + self.versions = json.load(in_file) + self.sorted_versions = sorted( + self.versions.items(), + key=lambda k: [int(i) for i in k[0].split(".")], + reverse=True, + ) + + def write_csv(self) -> None: + """Output CSV files.""" + now_str = str(dt.datetime.utcnow()) + + versions_by_category = {"branches": {}, "end-of-life": {}} + headers = None + for version, details in self.sorted_versions: + row = { + "Branch": details["branch"], + "Schedule": f":pep:`{details['pep']}`", + "Status": details["status"], + "First release": csv_date(details["first_release"], now_str), + "End of life": csv_date(details["end_of_life"], now_str), + "Release manager": details["release_manager"], + } + headers = row.keys() + cat = "end-of-life" if details["status"] == "end-of-life" else "branches" + versions_by_category[cat][version] = row + + for cat, versions in versions_by_category.items(): + with open(f"include/{cat}.csv", "w", encoding="UTF-8", newline="") as file: + csv_file = csv.DictWriter(file, fieldnames=headers, lineterminator="\n") + 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) + + with open( + "include/release-cycle.mmd", "w", encoding="UTF-8", newline="\n" + ) as f: + f.writelines(out) + + +def main() -> None: + versions = Versions() + versions.write_csv() + versions.write_mermaid() + + +if __name__ == "__main__": + main() diff --git a/conf.py b/conf.py index 7fce14050c..701c719673 100644 --- a/conf.py +++ b/conf.py @@ -10,6 +10,7 @@ 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx_copybutton', + 'sphinxcontrib.mermaid', 'sphinxext.opengraph', 'sphinxext.rediraffe', ] diff --git a/include/branches.csv b/include/branches.csv index 565f0e25e6..bfec9da84b 100644 --- a/include/branches.csv +++ b/include/branches.csv @@ -1,5 +1,5 @@ -Branch,Schedule,Status,First release,End-of-life,Release manager -main,:pep:`693`,features,*2023-10-02*,*2028-10*,Thomas Wouters +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 diff --git a/include/end-of-life.csv b/include/end-of-life.csv index 44bb3cc16b..be9fe1b728 100644 --- a/include/end-of-life.csv +++ b/include/end-of-life.csv @@ -1,4 +1,4 @@ -Branch,Schedule,Status,First release,End-of-life,Release manager +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 diff --git a/include/release-cycle.json b/include/release-cycle.json new file mode 100644 index 0000000000..df82e64ed4 --- /dev/null +++ b/include/release-cycle.json @@ -0,0 +1,122 @@ +{ + "3.12": { + "branch": "main", + "pep": 693, + "status": "feature", + "first_release": "2023-10-02", + "end_of_life": "2028-10", + "release_manager": "Thomas Wouters" + }, + "3.11": { + "branch": "3.11", + "pep": 664, + "status": "bugfix", + "first_release": "2022-10-24", + "end_of_life": "2027-10", + "release_manager": "Pablo Galindo Salgado" + }, + "3.10": { + "branch": "3.10", + "pep": 619, + "status": "bugfix", + "first_release": "2021-10-04", + "end_of_life": "2026-10", + "release_manager": "Pablo Galindo Salgado" + }, + "3.9": { + "branch": "3.9", + "pep": 596, + "status": "security", + "first_release": "2020-10-05", + "end_of_life": "2025-10", + "release_manager": "Łukasz Langa" + }, + "3.8": { + "branch": "3.8", + "pep": 569, + "status": "security", + "first_release": "2019-10-14", + "end_of_life": "2024-10", + "release_manager": "Łukasz Langa" + }, + "3.7": { + "branch": "3.7", + "pep": 537, + "status": "security", + "first_release": "2018-06-27", + "end_of_life": "2023-06-27", + "release_manager": "Ned Deily" + }, + "3.6": { + "branch": "3.6", + "pep": 494, + "status": "end-of-life", + "first_release": "2016-12-23", + "end_of_life": "2021-12-23", + "release_manager": "Ned Deily" + }, + "3.5": { + "branch": "3.5", + "pep": 478, + "status": "end-of-life", + "first_release": "2015-09-13", + "end_of_life": "2020-09-30", + "release_manager": "Larry Hastings" + }, + "3.4": { + "branch": "3.4", + "pep": 429, + "status": "end-of-life", + "first_release": "2014-03-16", + "end_of_life": "2019-03-18", + "release_manager": "Larry Hastings" + }, + "3.3": { + "branch": "3.3", + "pep": 398, + "status": "end-of-life", + "first_release": "2012-09-29", + "end_of_life": "2017-09-29", + "release_manager": "Georg Brandl, Ned Deily (3.3.7+)" + }, + "3.2": { + "branch": "3.2", + "pep": 392, + "status": "end-of-life", + "first_release": "2011-02-20", + "end_of_life": "2016-02-20", + "release_manager": "Georg Brandl" + }, + "2.7": { + "branch": "2.7", + "pep": 373, + "status": "end-of-life", + "first_release": "2010-07-03", + "end_of_life": "2020-01-01", + "release_manager": "Benjamin Peterson" + }, + "3.1": { + "branch": "3.1", + "pep": 375, + "status": "end-of-life", + "first_release": "2009-06-27", + "end_of_life": "2012-04-09", + "release_manager": "Benjamin Peterson" + }, + "3.0": { + "branch": "3.0", + "pep": 361, + "status": "end-of-life", + "first_release": "2008-12-03", + "end_of_life": "2009-06-27", + "release_manager": "Barry Warsaw" + }, + "2.6": { + "branch": "2.6", + "pep": 361, + "status": "end-of-life", + "first_release": "2008-10-01", + "end_of_life": "2013-10-29", + "release_manager": "Barry Warsaw" + } +} diff --git a/include/release-cycle.mmd b/include/release-cycle.mmd new file mode 100644 index 0000000000..fc437ab590 --- /dev/null +++ b/include/release-cycle.mmd @@ -0,0 +1,49 @@ +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/make.bat b/make.bat index 5fe9f98d71..432b7f361f 100644 --- a/make.bat +++ b/make.bat @@ -223,6 +223,14 @@ goto end (see https://github.com/python/cpython/issues/80510) goto end +if "%1" == "versions" ( + %PYTHON% _tools/generate_release_cycle.py + if errorlevel 1 exit /b 1 + echo. + echo Release cycle data generated. + goto end +) + :end popd endlocal diff --git a/requirements.txt b/requirements.txt index e7831193e0..f0397b9c44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ Sphinx==5.3.0 furo>=2022.6.4 -sphinx_copybutton>=0.3.3 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 c4e1b049f3..2d08ac0278 100644 --- a/versions.rst +++ b/versions.rst @@ -10,6 +10,12 @@ branch that accepts new features. The latest release for each Python version can be found on the `download page `_. +Python Release Cycle +==================== + +.. mermaid:: include/release-cycle.mmd + + Supported Versions ================== @@ -35,7 +41,7 @@ Unsupported Versions Status Key ========== -:features: new features, bugfixes, and security fixes are accepted. +: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