-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Generate a release schedule as an .ics file
#4705
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
| RELEASE_DIR = Path(__file__).resolve().parent | ||
| ROOT_DIR = RELEASE_DIR.parent | ||
| PEP_ROOT = ROOT_DIR / 'peps' | ||
| _PYTHON_RELEASES = None | ||
|
|
||
| dc_kw = {'kw_only': True, 'slots': True} if sys.version_info[:2] >= (3, 10) else {} | ||
|
|
||
|
|
@@ -68,6 +69,10 @@ def schedule_bullet(self): | |
|
|
||
|
|
||
| def load_python_releases() -> PythonReleases: | ||
| global _PYTHON_RELEASES | ||
| if _PYTHON_RELEASES is not None: | ||
| return _PYTHON_RELEASES | ||
|
Comment on lines
+72
to
+74
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think chucking a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not complex, it's a black box decorator :) We don't need to worry what it's doing under the hood, the complexity that matters is the code we're looking at here. And Or if it's a problem, let's just ditch this caching altogether. It's only called three times, so it's not like it'll make any noticeable difference. |
||
|
|
||
| with open(RELEASE_DIR / 'python-releases.toml', 'rb') as f: | ||
| python_releases = tomllib.load(f) | ||
| all_metadata = { | ||
|
|
@@ -78,4 +83,5 @@ def load_python_releases() -> PythonReleases: | |
| v: [ReleaseInfo(**r) for r in releases] | ||
| for v, releases in python_releases['release'].items() | ||
| } | ||
| return PythonReleases(metadata=all_metadata, releases=all_releases) | ||
| _PYTHON_RELEASES = PythonReleases(metadata=all_metadata, releases=all_releases) | ||
| return _PYTHON_RELEASES | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,13 +1,26 @@ | ||||||
| from __future__ import annotations | ||||||
|
|
||||||
| import datetime as dt | ||||||
| import dataclasses | ||||||
| import json | ||||||
|
|
||||||
| from release_management import ROOT_DIR, load_python_releases | ||||||
|
|
||||||
| TYPE_CHECKING = False | ||||||
| if TYPE_CHECKING: | ||||||
| from release_management import VersionMetadata | ||||||
| from release_management import ReleaseInfo, VersionMetadata | ||||||
|
|
||||||
| # Seven years captures the full lifecycle from prereleases to end-of-life | ||||||
| TODAY = dt.date.today() | ||||||
| SEVEN_YEARS_AGO = TODAY.replace(year=TODAY.year - 7) | ||||||
|
|
||||||
| # https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11 | ||||||
| CALENDAR_ESCAPE_TEXT = str.maketrans({ | ||||||
| '\\': r'\\', | ||||||
| ';': r'\;', | ||||||
| ',': r'\,', | ||||||
| '\n': r'\n', | ||||||
| }) | ||||||
|
|
||||||
|
|
||||||
| def create_release_json() -> str: | ||||||
|
|
@@ -48,3 +61,52 @@ def version_info(metadata: VersionMetadata, /) -> dict[str, str | int]: | |||||
| 'end_of_life': end_of_life, | ||||||
| 'release_manager': metadata.release_manager, | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| def create_release_schedule_calendar() -> str: | ||||||
| python_releases = load_python_releases() | ||||||
| releases = [] | ||||||
| for version, all_releases in python_releases.releases.items(): | ||||||
| pep_number = python_releases.metadata[version].pep | ||||||
| for release in all_releases: | ||||||
| # Keep size reasonable by omitting releases older than 7 years | ||||||
| if release.date < SEVEN_YEARS_AGO: | ||||||
| continue | ||||||
| releases.append((pep_number, release)) | ||||||
| releases.sort(key=lambda r: r[1].date) | ||||||
| lines = release_schedule_calendar_lines(releases) | ||||||
| return '\r\n'.join(lines) | ||||||
|
|
||||||
|
|
||||||
| def release_schedule_calendar_lines( | ||||||
| releases: list[tuple[int, ReleaseInfo]], / | ||||||
| ) -> list[str]: | ||||||
| lines = [ | ||||||
| 'BEGIN:VCALENDAR', | ||||||
| 'VERSION:2.0', | ||||||
| 'PRODID:-//Python Software Foundation//Python release schedule//EN', | ||||||
| 'X-WR-CALDESC:Python releases schedule from https://peps.python.org', | ||||||
| 'X-WR-CALNAME:Python releases schedule', | ||||||
| ] | ||||||
| for pep_number, release in releases: | ||||||
| normalised_stage = release.stage.casefold().replace(' ', '') | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why These are all lowercase, trusted strings with predictable contents like "3.14.0 alpha 1", "3.14.0 candidate 3" and "3.14.5". They won't have any uppercase or German "ß".
Suggested change
|
||||||
| normalised_stage = normalised_stage.translate(CALENDAR_ESCAPE_TEXT) | ||||||
| if release.note: | ||||||
| normalised_note = release.note.translate(CALENDAR_ESCAPE_TEXT) | ||||||
| note = (f'DESCRIPTION:Note: {normalised_note}',) | ||||||
| else: | ||||||
| note = () | ||||||
| lines += ( | ||||||
| 'BEGIN:VEVENT', | ||||||
| f'SUMMARY:Python {release.stage}', | ||||||
AA-Turner marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| f'DTSTART;VALUE=DATE:{release.date.strftime("%Y%m%d")}', | ||||||
| f'UID:python-{normalised_stage}@releases.python.org', | ||||||
AA-Turner marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| *note, | ||||||
| f'URL:https://peps.python.org/pep-{pep_number:04d}/', | ||||||
| 'END:VEVENT', | ||||||
| ) | ||||||
| lines += ( | ||||||
| 'END:VCALENDAR', | ||||||
| '', | ||||||
| ) | ||||||
| return lines | ||||||
AA-Turner marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import datetime as dt | ||
|
|
||
| from release_management import ReleaseInfo, serialize | ||
|
|
||
| FAKE_RELEASE = ReleaseInfo( | ||
| stage='X.Y.Z final', | ||
| state='actual', | ||
| date=dt.date(2000, 1, 1), | ||
| note='These characters need escaping: \\ , ; \n', | ||
| ) | ||
|
|
||
|
|
||
| def test_create_release_calendar_has_calendar_metadata() -> None: | ||
| # Act | ||
| cal_lines = serialize.create_release_schedule_calendar().split('\r\n') | ||
|
|
||
| # Assert | ||
|
|
||
| # Check calendar metadata | ||
| assert cal_lines[:5] == [ | ||
| 'BEGIN:VCALENDAR', | ||
| 'VERSION:2.0', | ||
| 'PRODID:-//Python Software Foundation//Python release schedule//EN', | ||
| 'X-WR-CALDESC:Python releases schedule from https://peps.python.org', | ||
| 'X-WR-CALNAME:Python releases schedule', | ||
| ] | ||
| assert cal_lines[-2:] == [ | ||
| 'END:VCALENDAR', | ||
| '', | ||
| ] | ||
|
|
||
|
|
||
| def test_create_release_calendar_first_event() -> None: | ||
| # Act | ||
| releases = [(9999, FAKE_RELEASE)] | ||
| cal_lines = serialize.release_schedule_calendar_lines(releases) | ||
|
|
||
| # Assert | ||
| assert cal_lines[5] == 'BEGIN:VEVENT' | ||
| assert cal_lines[6] == 'SUMMARY:Python X.Y.Z final' | ||
| assert cal_lines[7] == 'DTSTART;VALUE=DATE:20000101' | ||
| assert cal_lines[8] == 'UID:[email protected]' | ||
| assert cal_lines[9] == ( | ||
| 'DESCRIPTION:Note: These characters need escaping: \\\\ \\, \\; \\n' | ||
| ) | ||
| assert cal_lines[10] == 'URL:https://peps.python.org/pep-9999/' | ||
| assert cal_lines[11] == 'END:VEVENT' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's also skip measuring coverage for
if __name__ == '__main__':blocks, it's not worth testing.I also suggested skipping https://github.com/python/peps/blob/main/release_management/__main__.py in the original PR. Is there anything special worth testing in there?