Skip to content

Commit 3191a58

Browse files
committed
Generate an iCalendar file of the release dates
1 parent e0dde92 commit 3191a58

File tree

7 files changed

+131
-4
lines changed

7 files changed

+131
-4
lines changed

.coveragerc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[run]
2+
omit =
3+
release_management/__main__.py
4+
5+
[report]
6+
exclude_also =
7+
if __name__ == .__main__.:

pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from pep_sphinx_extensions.pep_zero_generator import subindices
2727
from pep_sphinx_extensions.pep_zero_generator import writer
2828
from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC
29-
from release_management.serialize import create_release_cycle, create_release_json
29+
from release_management.serialize import create_release_cycle, create_release_ical, create_release_json
3030

3131
if TYPE_CHECKING:
3232
from sphinx.application import Sphinx
@@ -77,5 +77,8 @@ def create_pep_zero(app: Sphinx, env: BuildEnvironment, docnames: list[str]) ->
7777
release_cycle = create_release_cycle()
7878
app.outdir.joinpath('api/release-cycle.json').write_text(release_cycle, encoding="utf-8")
7979

80+
release_ical = create_release_ical()
81+
app.outdir.joinpath('api/python-releases.ics').write_text(release_ical, encoding="utf-8")
82+
8083
release_json = create_release_json()
8184
app.outdir.joinpath('api/python-releases.json').write_text(release_json, encoding="utf-8")

pytest.ini

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ addopts =
55
--strict-config
66
--strict-markers
77
--import-mode=importlib
8-
--cov check_peps --cov pep_sphinx_extensions
9-
--cov-report html --cov-report xml
8+
--cov check_peps
9+
--cov pep_sphinx_extensions
10+
--cov release_management
11+
--cov-report html
12+
--cov-report xml
1013
empty_parameter_set_mark = fail_at_collect
1114
filterwarnings =
1215
error
1316
minversion = 6.0
14-
testpaths = pep_sphinx_extensions
17+
testpaths = pep_sphinx_extensions release_management
1518
xfail_strict = True
1619
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True

release_management/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import sys
44
from dataclasses import dataclass
5+
from functools import cache
56
from pathlib import Path
67

78
try:
@@ -67,6 +68,7 @@ def schedule_bullet(self):
6768
return f'- {self.stage}: {self.date:%A, %Y-%m-%d}'
6869

6970

71+
@cache
7072
def load_python_releases() -> PythonReleases:
7173
with open(RELEASE_DIR / 'python-releases.toml', 'rb') as f:
7274
python_releases = tomllib.load(f)

release_management/serialize.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import dataclasses
44
import json
5+
import re
6+
7+
from icalendar import Calendar, Event
58

69
from release_management import load_python_releases
710

@@ -48,3 +51,42 @@ def version_info(metadata: VersionMetadata, /) -> dict[str, str | int]:
4851
'end_of_life': end_of_life,
4952
'release_manager': metadata.release_manager,
5053
}
54+
55+
56+
def ical_uid(name: str) -> str:
57+
user = re.sub(r'[^a-z0-9.]+', '', name.lower())
58+
return f'python-{user}@python.org'
59+
60+
61+
def create_release_ical() -> str:
62+
python_releases = load_python_releases()
63+
64+
calendar = Calendar()
65+
calendar.add('version', '2.0')
66+
calendar.add('prodid', '-//Python Software Foundation//Python releases//EN')
67+
calendar.add('x-wr-calname', 'Python releases')
68+
calendar.add(
69+
'x-wr-caldesc', 'Python releases schedule from https://peps.python.org'
70+
)
71+
72+
all_releases = []
73+
for version, releases in python_releases.releases.items():
74+
for release in releases:
75+
all_releases.append((version, release))
76+
77+
all_releases.sort(key=lambda r: r[1].date)
78+
79+
for version, release in all_releases:
80+
event = Event()
81+
event.add('summary', f'Python {release.stage}')
82+
event.add('uid', ical_uid(release.stage))
83+
event.add('dtstart', release.date)
84+
pep_number = python_releases.metadata[version].pep
85+
event.add('url', f'https://peps.python.org/pep-{pep_number:04d}/')
86+
87+
if release.note:
88+
event.add('description', f'Note: {release.note}')
89+
90+
calendar.add_component(event)
91+
92+
return calendar.to_ical().decode('utf-8')
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import pytest
2+
from icalendar import Calendar
3+
from release_management import serialize
4+
5+
6+
@pytest.mark.parametrize(
7+
('test_input', 'expected'),
8+
[
9+
('3.14.0 alpha 1', '[email protected]'),
10+
('3.14.0 beta 2', '[email protected]'),
11+
('3.14.0 candidate 3', '[email protected]'),
12+
('3.14.1', '[email protected]'),
13+
],
14+
)
15+
def test_ical_uid(test_input, expected):
16+
assert serialize.ical_uid(test_input) == expected
17+
18+
19+
def test_create_release_ical_returns_valid_icalendar():
20+
# Act
21+
ical_str = serialize.create_release_ical()
22+
23+
# Assert
24+
# Non-empty string
25+
assert isinstance(ical_str, str)
26+
assert len(ical_str) > 0
27+
28+
# Parseable as a valid iCalendar
29+
cal = Calendar.from_ical(ical_str)
30+
assert cal is not None
31+
32+
33+
def test_create_release_ical_has_calendar_metadata():
34+
# Act
35+
ical_str = serialize.create_release_ical()
36+
37+
# Assert
38+
cal = Calendar.from_ical(ical_str)
39+
40+
# Check calendar metadata
41+
assert cal.get('version') == '2.0'
42+
assert cal.get('prodid') == '-//Python Software Foundation//Python releases//EN'
43+
assert cal.get('x-wr-calname') == 'Python releases'
44+
assert 'peps.python.org' in cal.get('x-wr-caldesc')
45+
46+
47+
def test_create_release_ical_first_event():
48+
# Act
49+
ical_str = serialize.create_release_ical()
50+
51+
# Assert
52+
cal = Calendar.from_ical(ical_str)
53+
first_event = cal.events[0]
54+
assert first_event.get('summary') == 'Python 1.6.0 alpha 1'
55+
assert str(first_event.get('dtstart').dt) == '2000-03-31'
56+
assert first_event.get('uid') == '[email protected]'
57+
assert first_event.get('url') == 'https://peps.python.org/pep-0160/'
58+
59+
60+
def test_create_release_ical_sorted_by_date():
61+
# Act
62+
ical_str = serialize.create_release_ical()
63+
64+
# Assert
65+
cal = Calendar.from_ical(ical_str)
66+
dates = [event.get('dtstart').dt for event in cal.events]
67+
assert dates == sorted(dates)

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@ sphinx-notfound-page >= 1.0.2
1010
pytest
1111
pytest-cov
1212

13+
# For python-releases.ical
14+
icalendar
15+
1316
# For python-releases.toml
1417
tomli >= 1.1.0 ; python_version < "3.11"

0 commit comments

Comments
 (0)