Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[run]
omit =
release_management/__main__.py

[report]
exclude_also =
if __name__ == .__main__.:
19 changes: 8 additions & 11 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ default_stages: [pre-commit]
repos:
# General file checks and fixers
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: end-of-file-fixer
name: "Ensure files end with a single newline"
Expand Down Expand Up @@ -48,37 +48,34 @@ repos:
name: "Check YAML"

- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.10.0
rev: 25.11.0
hooks:
- id: black
name: "Format with Black"
args:
- '--target-version=py39'
- '--target-version=py310'
files: '^(peps/conf\.py|pep_sphinx_extensions/tests/.*)$'

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.0
rev: v0.14.4
hooks:
- id: ruff
- id: ruff-check
name: "Lint with Ruff"
args:
- '--exit-non-zero-on-fix'
files: '^pep_sphinx_extensions/tests/'
files: '^(pep_sphinx_extensions/tests/|release_management/)'
- id: ruff-format
name: "Format with Ruff"
args:
- '--check'
files: '^release_management/'

- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.4.1
rev: 1.7.0
hooks:
- id: tox-ini-fmt
name: "Format tox.ini"

- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v1.0.0
rev: v1.0.1
hooks:
- id: sphinx-lint
name: "Sphinx lint"
Expand All @@ -99,7 +96,7 @@ repos:

# Manual codespell check
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
rev: v2.4.1
hooks:
- id: codespell
name: "Check for common misspellings in text files"
Expand Down
1 change: 1 addition & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
output-format = "full"
target-version = "py310"
fix = true

[lint]
ignore = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from pep_sphinx_extensions.pep_zero_generator import subindices
from pep_sphinx_extensions.pep_zero_generator import writer
from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC
from release_management.serialize import create_release_cycle, create_release_json
from release_management.serialize import create_release_cycle, create_release_ical, create_release_json

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

release_ical = create_release_ical()
app.outdir.joinpath('api/python-releases.ics').write_text(release_ical, encoding="utf-8")

release_json = create_release_json()
app.outdir.joinpath('api/python-releases.json').write_text(release_json, encoding="utf-8")
9 changes: 6 additions & 3 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ addopts =
--strict-config
--strict-markers
--import-mode=importlib
--cov check_peps --cov pep_sphinx_extensions
--cov-report html --cov-report xml
--cov check_peps
--cov pep_sphinx_extensions
--cov release_management
--cov-report html
--cov-report xml
empty_parameter_set_mark = fail_at_collect
filterwarnings =
error
minversion = 6.0
testpaths = pep_sphinx_extensions
testpaths = pep_sphinx_extensions release_management
xfail_strict = True
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
2 changes: 2 additions & 0 deletions release_management/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import sys
from dataclasses import dataclass
from functools import cache
from pathlib import Path

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


@cache
def load_python_releases() -> PythonReleases:
with open(RELEASE_DIR / 'python-releases.toml', 'rb') as f:
python_releases = tomllib.load(f)
Expand Down
44 changes: 43 additions & 1 deletion release_management/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import dataclasses
import json
import re

from release_management import ROOT_DIR, load_python_releases
from icalendar import Calendar, Event

from release_management import load_python_releases

TYPE_CHECKING = False
if TYPE_CHECKING:
Expand Down Expand Up @@ -48,3 +51,42 @@ def version_info(metadata: VersionMetadata, /) -> dict[str, str | int]:
'end_of_life': end_of_life,
'release_manager': metadata.release_manager,
}


def ical_uid(name: str) -> str:
user = re.sub(r'[^a-z0-9.]+', '', name.lower())
return f'python-{user}@python.org'


def create_release_ical() -> str:
python_releases = load_python_releases()

calendar = Calendar()
calendar.add('version', '2.0')
calendar.add('prodid', '-//Python Software Foundation//Python releases//EN')
calendar.add('x-wr-calname', 'Python releases')
calendar.add(
'x-wr-caldesc', 'Python releases schedule from https://peps.python.org'
)

all_releases = []
for version, releases in python_releases.releases.items():
for release in releases:
all_releases.append((version, release))

all_releases.sort(key=lambda r: r[1].date)

for version, release in all_releases:
event = Event()
event.add('summary', f'Python {release.stage}')
event.add('uid', ical_uid(release.stage))
event.add('dtstart', release.date)
pep_number = python_releases.metadata[version].pep
event.add('url', f'https://peps.python.org/pep-{pep_number:04d}/')

if release.note:
event.add('description', f'Note: {release.note}')

calendar.add_component(event)

return calendar.to_ical().decode('utf-8')
67 changes: 67 additions & 0 deletions release_management/tests/test_serialize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import pytest
from icalendar import Calendar
from release_management import serialize


@pytest.mark.parametrize(
('test_input', 'expected'),
[
('3.14.0 alpha 1', 'python-3.14.0alpha1@python.org'),
('3.14.0 beta 2', 'python-3.14.0beta2@python.org'),
('3.14.0 candidate 3', 'python-3.14.0candidate3@python.org'),
('3.14.1', 'python-3.14.1@python.org'),
],
)
def test_ical_uid(test_input, expected):
assert serialize.ical_uid(test_input) == expected


def test_create_release_ical_returns_valid_icalendar():
# Act
ical_str = serialize.create_release_ical()

# Assert
# Non-empty string
assert isinstance(ical_str, str)
assert len(ical_str) > 0

# Parseable as a valid iCalendar
cal = Calendar.from_ical(ical_str)
assert cal is not None


def test_create_release_ical_has_calendar_metadata():
# Act
ical_str = serialize.create_release_ical()

# Assert
cal = Calendar.from_ical(ical_str)

# Check calendar metadata
assert cal.get('version') == '2.0'
assert cal.get('prodid') == '-//Python Software Foundation//Python releases//EN'
assert cal.get('x-wr-calname') == 'Python releases'
assert 'peps.python.org' in cal.get('x-wr-caldesc')


def test_create_release_ical_first_event():
# Act
ical_str = serialize.create_release_ical()

# Assert
cal = Calendar.from_ical(ical_str)
first_event = cal.events[0]
assert first_event.get('summary') == 'Python 1.6.0 alpha 1'
assert str(first_event.get('dtstart').dt) == '2000-03-31'
assert first_event.get('uid') == 'python-1.6.0alpha1@python.org'
assert first_event.get('url') == 'https://peps.python.org/pep-0160/'


def test_create_release_ical_sorted_by_date():
# Act
ical_str = serialize.create_release_ical()

# Assert
cal = Calendar.from_ical(ical_str)
dates = [event.get('dtstart').dt for event in cal.events]
assert dates == sorted(dates)
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ sphinx-notfound-page >= 1.0.2
pytest
pytest-cov

# For python-releases.ical
icalendar
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obligatory “supply chain” security question: do we trust icalendar maintainers and their operational security?
This could be tasty vector for running code on core dev boxes.
cc @sethmlarson


Maybe consider generating “manually”? iCal is a simple, stable text-based format.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@encukou I would recommend generating the ICS file manually within the code instead of relying on a library. The format is quite simple, especially for the use-case we need :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've proposed a simpler alternative at #4705 following this suggestion.

A


# For python-releases.toml
tomli >= 1.1.0 ; python_version < "3.11"