From 601c507775a5275d721c882d0be732352c000044 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:39:19 +0200 Subject: [PATCH 1/5] ruff-check release_management --- .pre-commit-config.yaml | 4 +--- .ruff.toml | 1 + release_management/serialize.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 892c098f911..25c6417565d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,11 +63,9 @@ repos: 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/python-jsonschema/check-jsonschema diff --git a/.ruff.toml b/.ruff.toml index 5e0a0fa17f3..ac5a73556f4 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,5 +1,6 @@ output-format = "full" target-version = "py311" +fix = true [lint] ignore = [ diff --git a/release_management/serialize.py b/release_management/serialize.py index 051ed77c3ec..fae97d9fcc1 100644 --- a/release_management/serialize.py +++ b/release_management/serialize.py @@ -1,10 +1,10 @@ from __future__ import annotations -import datetime as dt import dataclasses +import datetime as dt import json -from release_management import ROOT_DIR, load_python_releases +from release_management import load_python_releases TYPE_CHECKING = False if TYPE_CHECKING: From 1dbc092f623b10f9422cd05e8eb532f2f3c2a16e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:30:12 +0300 Subject: [PATCH 2/5] Use single formatter --- .pre-commit-config.yaml | 25 ++-- .ruff.toml | 16 --- pyproject.toml | 14 +++ release_management/.ruff.toml | 5 - release_management/__init__.py | 22 ++-- release_management/__main__.py | 22 ++-- release_management/serialize.py | 68 ++++++----- .../tests/test_release_schedule_calendar.py | 40 +++--- .../update_release_schedules.py | 114 +++++++++--------- 9 files changed, 165 insertions(+), 161 deletions(-) delete mode 100644 .ruff.toml create mode 100644 pyproject.toml delete mode 100644 release_management/.ruff.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25c6417565d..b1ba63ddc4b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,22 +54,19 @@ repos: name: "Format with Black" args: - '--target-version=py311' - files: '^(peps/conf\.py|pep_sphinx_extensions/tests/.*)$' + files: '^(peps/conf\.py|pep_sphinx_extensions/tests/.*|release_management/.*)$' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.21 + rev: v0.16.6 hooks: - id: ruff-check name: "Lint with Ruff" args: - '--exit-non-zero-on-fix' files: '^(pep_sphinx_extensions/tests/|release_management/)' - - id: ruff-format - name: "Format with Ruff" - files: '^release_management/' - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.37.4 + rev: 0.38.0 hooks: - id: check-dependabot - id: check-github-workflows @@ -80,12 +77,22 @@ repos: - id: actionlint - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.26.1 + rev: v1.30.1 hooks: - id: zizmor + - repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.29.4 + hooks: + - id: pyproject-fmt + + - repo: https://github.com/abravalheri/validate-pyproject + rev: '0.26' + hooks: + - id: validate-pyproject + - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.7.2 + rev: 1.9.0 hooks: - id: tox-ini-fmt name: "Format tox.ini" @@ -112,7 +119,7 @@ repos: # Manual codespell check - repo: https://github.com/codespell-project/codespell - rev: v2.4.2 + rev: v2.4.3 hooks: - id: codespell name: "Check for common misspellings in text files" diff --git a/.ruff.toml b/.ruff.toml deleted file mode 100644 index ac5a73556f4..00000000000 --- a/.ruff.toml +++ /dev/null @@ -1,16 +0,0 @@ -output-format = "full" -target-version = "py311" -fix = true - -[lint] -ignore = [ - "E501", # Line too long -] - -select = [ - "E", # pycodestyle errors - "F", # pyflakes - "I", # isort - "PT", # flake8-pytest-style - "W", # pycodestyle warnings -] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..cd40c743a93 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[tool.ruff] +target-version = "py311" +fix = true +output-format = "full" +lint.select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "PT", # flake8-pytest-style + "W", # pycodestyle warnings +] +lint.ignore = [ + "E501", # Line too long +] diff --git a/release_management/.ruff.toml b/release_management/.ruff.toml deleted file mode 100644 index 2aeb6ec8468..00000000000 --- a/release_management/.ruff.toml +++ /dev/null @@ -1,5 +0,0 @@ -extend = "../.ruff.toml" # Inherit the project-wide settings - -[format] -preview = true -quote-style = "single" diff --git a/release_management/__init__.py b/release_management/__init__.py index 9846219fdc0..84e89493131 100644 --- a/release_management/__init__.py +++ b/release_management/__init__.py @@ -14,17 +14,17 @@ import datetime as dt from typing import Literal, TypeAlias - ReleaseState: TypeAlias = Literal['actual', 'expected'] - ReleaseSchedules: TypeAlias = dict[tuple[str, ReleaseState], list['ReleaseInfo']] + ReleaseState: TypeAlias = Literal["actual", "expected"] + ReleaseSchedules: TypeAlias = dict[tuple[str, ReleaseState], list["ReleaseInfo"]] VersionStatus: TypeAlias = Literal[ - 'feature', 'prerelease', 'bugfix', 'security', 'end-of-life' + "feature", "prerelease", "bugfix", "security", "end-of-life" ] RELEASE_DIR = Path(__file__).resolve().parent ROOT_DIR = RELEASE_DIR.parent -PEP_ROOT = ROOT_DIR / 'peps' +PEP_ROOT = ROOT_DIR / "peps" -dc_kw = {'kw_only': True, 'slots': True} if sys.version_info[:2] >= (3, 10) else {} +dc_kw = {"kw_only": True, "slots": True} if sys.version_info[:2] >= (3, 10) else {} @dataclass(frozen=True, **dc_kw) @@ -49,7 +49,7 @@ class VersionMetadata: @classmethod def from_toml(cls, data: dict[str, int | str | dt.date]): - return cls(**{k.replace('-', '_'): v for k, v in data.items()}) + return cls(**{k.replace("-", "_"): v for k, v in data.items()}) @dataclass(frozen=True, **dc_kw) @@ -59,23 +59,23 @@ class ReleaseInfo: stage: str state: ReleaseState date: dt.date - note: str = '' # optional note / comment, displayed in the schedule + note: str = "" # optional note / comment, displayed in the schedule @property def schedule_bullet(self): """Return a formatted bullet point for the schedule list.""" - return f'- {self.stage}: {self.date:%A, %Y-%m-%d}' + return f"- {self.stage}: {self.date:%A, %Y-%m-%d}" def load_python_releases() -> PythonReleases: - with open(RELEASE_DIR / 'python-releases.toml', 'rb') as f: + with open(RELEASE_DIR / "python-releases.toml", "rb") as f: python_releases = tomllib.load(f) all_metadata = { v: VersionMetadata.from_toml(metadata) - for v, metadata in python_releases['metadata'].items() + for v, metadata in python_releases["metadata"].items() } all_releases = { v: [ReleaseInfo(**r) for r in releases] - for v, releases in python_releases['release'].items() + for v, releases in python_releases["release"].items() } return PythonReleases(metadata=all_metadata, releases=all_releases) diff --git a/release_management/__main__.py b/release_management/__main__.py index 458a00ef90d..55e3fe2193f 100644 --- a/release_management/__main__.py +++ b/release_management/__main__.py @@ -3,13 +3,13 @@ import argparse commands = ( - CMD_FULL_JSON := 'full-json', - CMD_UPDATE_PEPS := 'update-peps', - CMD_RELEASE_CYCLE := 'release-cycle', - CMD_CALENDAR := 'calendar', + CMD_FULL_JSON := "full-json", + CMD_UPDATE_PEPS := "update-peps", + CMD_RELEASE_CYCLE := "release-cycle", + CMD_CALENDAR := "calendar", ) parser = argparse.ArgumentParser(allow_abbrev=False) -parser.add_argument('COMMAND', choices=commands) +parser.add_argument("COMMAND", choices=commands) args = parser.parse_args() if args.COMMAND == CMD_UPDATE_PEPS: @@ -21,22 +21,22 @@ from release_management import ROOT_DIR from release_management.serialize import create_release_json - json_path = ROOT_DIR / 'python-releases.json' - json_path.write_text(create_release_json(), encoding='utf-8') + json_path = ROOT_DIR / "python-releases.json" + json_path.write_text(create_release_json(), encoding="utf-8") raise SystemExit(0) if args.COMMAND == CMD_RELEASE_CYCLE: from release_management import ROOT_DIR from release_management.serialize import create_release_cycle - json_path = ROOT_DIR / 'release-cycle.json' - json_path.write_text(create_release_cycle(), encoding='utf-8') + json_path = ROOT_DIR / "release-cycle.json" + json_path.write_text(create_release_cycle(), encoding="utf-8") raise SystemExit(0) if args.COMMAND == CMD_CALENDAR: from release_management import ROOT_DIR from release_management.serialize import create_release_schedule_calendar - calendar_path = ROOT_DIR / 'release-schedule.ics' - calendar_path.write_text(create_release_schedule_calendar(), encoding='utf-8') + calendar_path = ROOT_DIR / "release-schedule.ics" + calendar_path.write_text(create_release_schedule_calendar(), encoding="utf-8") raise SystemExit(0) diff --git a/release_management/serialize.py b/release_management/serialize.py index fae97d9fcc1..ee4e0c33cff 100644 --- a/release_management/serialize.py +++ b/release_management/serialize.py @@ -15,12 +15,14 @@ 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', -}) +CALENDAR_ESCAPE_TEXT = str.maketrans( + { + "\\": r"\\", + ";": r"\;", + ",": r"\,", + "\n": r"\n", + } +) def create_release_json() -> str: @@ -42,24 +44,24 @@ def create_release_cycle() -> str: versions = [v for _date, v in all_versions if version_to_tuple(v) >= (2, 6)] release_cycle = {version: version_info(metadata[version]) for version in versions} rc_json = json.dumps(release_cycle, indent=2, sort_keys=False, ensure_ascii=False) - return f'{rc_json}\n' + return f"{rc_json}\n" def version_to_tuple(version: str, /) -> tuple[int, ...]: - return tuple(map(int, version.split('.'))) + return tuple(map(int, version.split("."))) def version_info(metadata: VersionMetadata, /) -> dict[str, str | int]: end_of_life = metadata.end_of_life.isoformat() - if metadata.status != 'end-of-life': - end_of_life = end_of_life.removesuffix('-01') + if metadata.status != "end-of-life": + end_of_life = end_of_life.removesuffix("-01") return { - 'branch': metadata.branch, - 'pep': metadata.pep, - 'status': metadata.status, - 'first_release': metadata.first_release.isoformat(), - 'end_of_life': end_of_life, - 'release_manager': metadata.release_manager, + "branch": metadata.branch, + "pep": metadata.pep, + "status": metadata.status, + "first_release": metadata.first_release.isoformat(), + "end_of_life": end_of_life, + "release_manager": metadata.release_manager, } @@ -75,41 +77,41 @@ def create_release_schedule_calendar() -> str: releases.append((pep_number, release)) releases.sort(key=lambda r: r[1].date) lines = release_schedule_calendar_lines(releases) - return '\r\n'.join(lines) + return "\r\n".join(lines) def release_schedule_calendar_lines( releases: list[tuple[int, ReleaseInfo]], / ) -> list[str]: - dtstamp = dt.datetime.now(dt.timezone.utc).strftime('%Y%m%dT%H%M%SZ') + dtstamp = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ") 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', + "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.replace(' ', '') + normalised_stage = release.stage.replace(" ", "") 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}',) + note = (f"DESCRIPTION:Note: {normalised_note}",) else: note = () lines += ( - 'BEGIN:VEVENT', - f'DTSTAMP:{dtstamp}', - f'UID:python-{normalised_stage}@releases.python.org', + "BEGIN:VEVENT", + f"DTSTAMP:{dtstamp}", + f"UID:python-{normalised_stage}@releases.python.org", f'DTSTART;VALUE=DATE:{release.date.strftime("%Y%m%d")}', - f'SUMMARY:Python {release.stage}', + f"SUMMARY:Python {release.stage}", *note, - f'URL:https://peps.python.org/pep-{pep_number:04d}/', - 'END:VEVENT', + f"URL:https://peps.python.org/pep-{pep_number:04d}/", + "END:VEVENT", ) lines += ( - 'END:VCALENDAR', - '', + "END:VCALENDAR", + "", ) return lines diff --git a/release_management/tests/test_release_schedule_calendar.py b/release_management/tests/test_release_schedule_calendar.py index 0d1fcff61bf..7d8a3b0abb1 100644 --- a/release_management/tests/test_release_schedule_calendar.py +++ b/release_management/tests/test_release_schedule_calendar.py @@ -3,30 +3,30 @@ from release_management import ReleaseInfo, serialize FAKE_RELEASE = ReleaseInfo( - stage='X.Y.Z final', - state='actual', + stage="X.Y.Z final", + state="actual", date=dt.date(2000, 1, 1), - note='These characters need escaping: \\ , ; \n', + 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') + 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', + "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', - '', + "END:VCALENDAR", + "", ] @@ -36,14 +36,14 @@ def test_create_release_calendar_first_event() -> None: cal_lines = serialize.release_schedule_calendar_lines(releases) # Assert - assert cal_lines[5] == 'BEGIN:VEVENT' - assert cal_lines[6].startswith('DTSTAMP:') - assert cal_lines[6].endswith('Z') - assert cal_lines[7] == 'UID:python-X.Y.Zfinal@releases.python.org' - assert cal_lines[8] == 'DTSTART;VALUE=DATE:20000101' - assert cal_lines[9] == 'SUMMARY:Python X.Y.Z final' + assert cal_lines[5] == "BEGIN:VEVENT" + assert cal_lines[6].startswith("DTSTAMP:") + assert cal_lines[6].endswith("Z") + assert cal_lines[7] == "UID:python-X.Y.Zfinal@releases.python.org" + assert cal_lines[8] == "DTSTART;VALUE=DATE:20000101" + assert cal_lines[9] == "SUMMARY:Python X.Y.Z final" assert cal_lines[10] == ( - 'DESCRIPTION:Note: These characters need escaping: \\\\ \\, \\; \\n' + "DESCRIPTION:Note: These characters need escaping: \\\\ \\, \\; \\n" ) - assert cal_lines[11] == 'URL:https://peps.python.org/pep-9999/' - assert cal_lines[12] == 'END:VEVENT' + assert cal_lines[11] == "URL:https://peps.python.org/pep-9999/" + assert cal_lines[12] == "END:VEVENT" diff --git a/release_management/update_release_schedules.py b/release_management/update_release_schedules.py index aeb33583dac..4128d61a4de 100644 --- a/release_management/update_release_schedules.py +++ b/release_management/update_release_schedules.py @@ -42,25 +42,27 @@ TODAY = dt.date.today() -SKIPPED_VERSIONS = frozenset({ - '1.6', - '2.0', - '2.1', - '2.2', - '2.3', - '2.4', - '2.5', - '2.6', - '2.7', - '3.0', - '3.1', - '3.2', - '3.3', - '3.4', - '3.5', - '3.6', - '3.7', -}) +SKIPPED_VERSIONS = frozenset( + { + "1.6", + "2.0", + "2.1", + "2.2", + "2.3", + "2.4", + "2.5", + "2.6", + "2.7", + "3.0", + "3.1", + "3.2", + "3.3", + "3.4", + "3.5", + "3.6", + "3.7", + } +) def update_peps() -> None: @@ -84,46 +86,46 @@ def create_schedules( bugfix_ends: dt.date, ) -> ReleaseSchedules: schedules: ReleaseSchedules = { - ('feature', 'actual'): [], - ('feature', 'expected'): [], - ('bugfix', 'actual'): [], - ('bugfix', 'expected'): [], - ('security', 'actual'): [], + ("feature", "actual"): [], + ("feature", "expected"): [], + ("bugfix", "actual"): [], + ("bugfix", "expected"): [], + ("security", "actual"): [], } # first entry into the dictionary - db_state: ReleaseState = 'actual' if TODAY >= start_of_development else 'expected' - schedules['feature', db_state].append( + db_state: ReleaseState = "actual" if TODAY >= start_of_development else "expected" + schedules["feature", db_state].append( ReleaseInfo( - stage=f'{version} development begins', + stage=f"{version} development begins", state=db_state, date=start_of_development, ) ) for release_info in releases: - if release_info.stage.startswith(f'{version}.0'): - schedules['feature', release_info.state].append(release_info) + if release_info.stage.startswith(f"{version}.0"): + schedules["feature", release_info.state].append(release_info) elif release_info.date <= bugfix_ends: - schedules['bugfix', release_info.state].append(release_info) + schedules["bugfix", release_info.state].append(release_info) else: - assert release_info.state == 'actual', release_info - schedules['security', release_info.state].append(release_info) + assert release_info.state == "actual", release_info + schedules["security", release_info.state].append(release_info) return schedules def update_pep(metadata: VersionMetadata, schedules: ReleaseSchedules) -> None: - pep_path = PEP_ROOT.joinpath(f'pep-{metadata.pep:0>4}.rst') - pep_lines = iter(pep_path.read_text(encoding='utf-8').splitlines()) + pep_path = PEP_ROOT.joinpath(f"pep-{metadata.pep:0>4}.rst") + pep_lines = iter(pep_path.read_text(encoding="utf-8").splitlines()) output_lines: list[str] = [] - schedule_name = '' + schedule_name = "" for line in pep_lines: output_lines.append(line) - if line.startswith('.. ') and 'schedule' in line: - assert line.startswith('.. release schedule: ') - schedule_name = line.removeprefix('.. release schedule: ') - assert schedule_name in {'feature', 'bugfix', 'security'} + if line.startswith(".. ") and "schedule" in line: + assert line.startswith(".. release schedule: ") + schedule_name = line.removeprefix(".. release schedule: ") + assert schedule_name in {"feature", "bugfix", "security"} output_lines += generate_schedule_lists( schedules, schedule_name=schedule_name, @@ -133,18 +135,18 @@ def update_pep(metadata: VersionMetadata, schedules: ReleaseSchedules) -> None: # skip source lines until the end of schedule marker while True: line = next(pep_lines, None) - if line == '.. release schedule: ends': + if line == ".. release schedule: ends": output_lines.append(line) break if line is None: - raise ValueError('No end of schedule marker found!') + raise ValueError("No end of schedule marker found!") if not schedule_name: - raise ValueError('No schedule markers found!') + raise ValueError("No schedule markers found!") - output_lines.append('') # trailing newline - with open(pep_path, 'wb') as f: - f.write(b'\n'.join(line.encode('utf-8') for line in output_lines)) + output_lines.append("") # trailing newline + with open(pep_path, "wb") as f: + f.write(b"\n".join(line.encode("utf-8") for line in output_lines)) def generate_schedule_lists( @@ -154,26 +156,26 @@ def generate_schedule_lists( feature_freeze_date: dt.date = dt.date.min, ) -> Iterator[str]: state: ReleaseState - for state in 'actual', 'expected': + for state in "actual", "expected": if not schedules.get((schedule_name, state)): continue - yield '' - if schedule_name != 'security': - yield f'{state.title()}:' - yield '' + yield "" + if schedule_name != "security": + yield f"{state.title()}:" + yield "" for release_info in schedules[schedule_name, state]: yield release_info.schedule_bullet if release_info.note: - yield f' ({release_info.note})' + yield f" ({release_info.note})" if release_info.date == feature_freeze_date: - yield ' (No new features beyond this point.)' + yield " (No new features beyond this point.)" - if schedule_name == 'bugfix': - yield ' (Final regular bugfix release with binary installers)' + if schedule_name == "bugfix": + yield " (Final regular bugfix release with binary installers)" - yield '' + yield "" -if __name__ == '__main__': +if __name__ == "__main__": update_peps() From 2de56034794b45e075cc5f261c11ca92dd671d51 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:34:53 +0300 Subject: [PATCH 3/5] Add pyupgrade and flake8-2020 rules --- pyproject.toml | 12 +++++++----- release_management/serialize.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cd40c743a93..eafd3ef7f2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,11 +3,13 @@ target-version = "py311" fix = true output-format = "full" lint.select = [ - "E", # pycodestyle errors - "F", # pyflakes - "I", # isort - "PT", # flake8-pytest-style - "W", # pycodestyle warnings + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "PT", # flake8-pytest-style + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 ] lint.ignore = [ "E501", # Line too long diff --git a/release_management/serialize.py b/release_management/serialize.py index ee4e0c33cff..950fef02679 100644 --- a/release_management/serialize.py +++ b/release_management/serialize.py @@ -83,7 +83,7 @@ def create_release_schedule_calendar() -> str: def release_schedule_calendar_lines( releases: list[tuple[int, ReleaseInfo]], / ) -> list[str]: - dtstamp = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ") + dtstamp = dt.datetime.now(dt.UTC).strftime("%Y%m%dT%H%M%SZ") lines = [ "BEGIN:VCALENDAR", From 247f033666c4f91e751b48299ce46ce71944933d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:25:33 +0300 Subject: [PATCH 4/5] Format and lint all dirs except peps/ --- .pre-commit-config.yaml | 4 +- build.py | 42 +++++++--- check-peps.py | 52 ++++++++---- pep_sphinx_extensions/__init__.py | 38 +++++---- pep_sphinx_extensions/generate_rss.py | 16 ++-- .../pep_processor/html/pep_html_builder.py | 9 +- .../pep_processor/html/pep_html_translator.py | 37 +++++--- .../parsing/pep_banner_directive.py | 19 ++--- .../pep_processor/parsing/pep_parser.py | 10 ++- .../pep_processor/parsing/pep_role.py | 9 +- .../pep_processor/transforms/pep_contents.py | 11 +-- .../pep_processor/transforms/pep_footer.py | 7 +- .../pep_processor/transforms/pep_headers.py | 42 ++++++---- .../transforms/pep_references.py | 3 +- .../pep_processor/transforms/pep_title.py | 32 ++++--- .../pep_processor/transforms/pep_zero.py | 3 +- .../pep_zero_generator/constants.py | 11 ++- .../pep_zero_generator/errors.py | 2 +- .../pep_zero_generator/parser.py | 50 +++++++---- .../pep_zero_generator/pep_index_generator.py | 35 +++++--- .../pep_zero_generator/subindices.py | 13 ++- .../pep_zero_generator/writer.py | 84 +++++++++++++------ peps/conf.py | 2 +- 23 files changed, 344 insertions(+), 187 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b1ba63ddc4b..937b7f9049e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,7 +54,7 @@ repos: name: "Format with Black" args: - '--target-version=py311' - files: '^(peps/conf\.py|pep_sphinx_extensions/tests/.*|release_management/.*)$' + exclude: '^peps/' - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.16.6 @@ -63,7 +63,7 @@ repos: name: "Lint with Ruff" args: - '--exit-non-zero-on-fix' - files: '^(pep_sphinx_extensions/tests/|release_management/)' + exclude: '^peps/' - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.38.0 diff --git a/build.py b/build.py index 04f0b5fdb05..269e616dee4 100755 --- a/build.py +++ b/build.py @@ -15,18 +15,33 @@ def create_parser(): parser = argparse.ArgumentParser(description="Build PEP documents") # alternative builders: builders = parser.add_mutually_exclusive_group() - builders.add_argument("-l", "--check-links", action="store_const", - dest="builder", const="linkcheck", - help='Check validity of links within PEP sources. ' - 'Cannot be used with "-f" or "-d".') - builders.add_argument("-f", "--build-files", action="store_const", - dest="builder", const="html", - help='Render PEPs to "pep-NNNN.html" files (default). ' - 'Cannot be used with "-d" or "-l".') - builders.add_argument("-d", "--build-dirs", action="store_const", - dest="builder", const="dirhtml", - help='Render PEPs to "index.html" files within "pep-NNNN" directories. ' - 'Cannot be used with "-f" or "-l".') + builders.add_argument( + "-l", + "--check-links", + action="store_const", + dest="builder", + const="linkcheck", + help="Check validity of links within PEP sources. " + 'Cannot be used with "-f" or "-d".', + ) + builders.add_argument( + "-f", + "--build-files", + action="store_const", + dest="builder", + const="html", + help='Render PEPs to "pep-NNNN.html" files (default). ' + 'Cannot be used with "-d" or "-l".', + ) + builders.add_argument( + "-d", + "--build-dirs", + action="store_const", + dest="builder", + const="dirhtml", + help='Render PEPs to "index.html" files within "pep-NNNN" directories. ' + 'Cannot be used with "-f" or "-l".', + ) parser.add_argument( "-o", @@ -46,7 +61,8 @@ def create_index_file(html_root: Path, builder: str) -> None: except FileNotFoundError: return None if builder == "dirhtml": - pep_zero_text = pep_zero_text.replace('="../', '="') # remove relative directory links + # remove relative directory links + pep_zero_text = pep_zero_text.replace('="../', '="') html_root.joinpath("index.html").write_text(pep_zero_text, encoding="utf-8") diff --git a/check-peps.py b/check-peps.py index 75898a13aab..20931c85a7e 100755 --- a/check-peps.py +++ b/check-peps.py @@ -41,7 +41,8 @@ "Title", "Author", "Sponsor", - "BDFL-Delegate", "PEP-Delegate", + "BDFL-Delegate", + "PEP-Delegate", "Discussions-To", "Status", "Type", @@ -57,18 +58,20 @@ REQUIRED_HEADERS = frozenset({"PEP", "Title", "Author", "Status", "Type", "Created"}) # See PEP 1 for the full list -ALL_STATUSES = frozenset({ - "Accepted", - "Active", - "April Fool!", - "Deferred", - "Draft", - "Final", - "Provisional", - "Rejected", - "Superseded", - "Withdrawn", -}) +ALL_STATUSES = frozenset( + { + "Accepted", + "Active", + "April Fool!", + "Deferred", + "Draft", + "Final", + "Provisional", + "Rejected", + "Superseded", + "Withdrawn", + } +) # PEPs that are allowed to link directly to PEPs SKIP_DIRECT_PEP_LINK_CHECK = frozenset({"0009", "0287", "0676", "0684", "8001"}) @@ -86,8 +89,12 @@ DISCOURSE_POST_PATTERN = re.compile(r"([\w\-]+/)?\d+(/\d+)?", DEFAULT_FLAGS) MAILMAN_2_PATTERN = re.compile(r"[\w\-]+/\d{4}-[a-z]+/\d+\.html", DEFAULT_FLAGS) -MAILMAN_3_THREAD_PATTERN = re.compile(r"[\w\-]+@python\.org/thread/[a-z0-9]+/?", DEFAULT_FLAGS) -MAILMAN_3_MESSAGE_PATTERN = re.compile(r"[\w\-]+@python\.org/message/[a-z0-9]+/?(#[a-z0-9]+)?", DEFAULT_FLAGS) +MAILMAN_3_THREAD_PATTERN = re.compile( + r"[\w\-]+@python\.org/thread/[a-z0-9]+/?", DEFAULT_FLAGS +) +MAILMAN_3_MESSAGE_PATTERN = re.compile( + r"[\w\-]+@python\.org/message/[a-z0-9]+/?(#[a-z0-9]+)?", DEFAULT_FLAGS +) # Controlled by the "--detailed" flag DETAILED_ERRORS = False @@ -203,7 +210,9 @@ def check_direct_links(line_num: int, line: str) -> MessageIterator: yield line_num, "Use the :rfc:`NNN` role to refer to RFCs" -def _output_error(filename: Path, lines: Sequence[str], errors: Iterable[Message]) -> int: +def _output_error( + filename: Path, lines: Sequence[str], errors: Iterable[Message] +) -> int: relative_filename = filename.relative_to(ROOT_DIR) err_count = 0 for line_num, msg in errors: @@ -501,7 +510,14 @@ def _invalid_domain(domain_part: str) -> bool: return not root.isalnum() or not root.isascii() -def _thread(line_num: int, url: str, prefix: str, *, allow_message: bool = False, discussions_to: bool = False) -> MessageIterator: +def _thread( + line_num: int, + url: str, + prefix: str, + *, + allow_message: bool = False, + discussions_to: bool = False, +) -> MessageIterator: if allow_message and discussions_to: msg = "allow_message and discussions_to cannot both be True" raise ValueError(msg) @@ -523,7 +539,7 @@ def _thread(line_num: int, url: str, prefix: str, *, allow_message: bool = False # We use ``str.rpartition`` as the topic name is optional topic_name, _, topic_id = remainder.rpartition("/") - if topic_name == '' and _is_digits(topic_id): + if topic_name == "" and _is_digits(topic_id): return topic_name = topic_name.replace("-", "0").replace("_", "0") # the topic name must not be entirely numeric diff --git a/pep_sphinx_extensions/__init__.py b/pep_sphinx_extensions/__init__.py index 109c09d7890..1c3383dfd81 100644 --- a/pep_sphinx_extensions/__init__.py +++ b/pep_sphinx_extensions/__init__.py @@ -12,7 +12,6 @@ from pep_sphinx_extensions.generate_rss import ( create_rss_feed, get_from_doctree, - pep_abstract, ) from pep_sphinx_extensions.pep_processor.html import ( pep_html_builder, @@ -23,8 +22,7 @@ pep_parser, pep_role, ) -from pep_sphinx_extensions.pep_processor.transforms import pep_footer -from pep_sphinx_extensions.pep_processor.transforms import pep_references +from pep_sphinx_extensions.pep_processor.transforms import pep_footer, pep_references from pep_sphinx_extensions.pep_zero_generator.pep_index_generator import create_pep_zero if TYPE_CHECKING: @@ -85,35 +83,45 @@ def setup(app: Sphinx) -> dict[str, bool]: app.add_source_parser(pep_parser.PEPParser) # Add PEP transforms - app.set_translator("html", pep_html_translator.PEPTranslator) # Docutils Node Visitor overrides (html builder) - app.set_translator("dirhtml", pep_html_translator.PEPTranslator) # Docutils Node Visitor overrides (dirhtml builder) + app.set_translator( + "html", pep_html_translator.PEPTranslator + ) # Docutils Node Visitor overrides (html builder) + app.set_translator( + "dirhtml", pep_html_translator.PEPTranslator + ) # Docutils Node Visitor overrides (dirhtml builder) - app.add_role("pep", pep_role.PEPRole(), override=True) # Transform PEP references to links + app.add_role( + "pep", pep_role.PEPRole(), override=True + ) # Transform PEP references to links app.add_post_transform(pep_references.PEPReferenceRoleTitleText) # Register custom directives + app.add_directive("pep-banner", pep_banner_directive.PEPBanner) + app.add_directive("canonical-doc", pep_banner_directive.CanonicalDocBanner) app.add_directive( - "pep-banner", pep_banner_directive.PEPBanner) + "canonical-pypa-spec", pep_banner_directive.CanonicalPyPASpecBanner + ) app.add_directive( - "canonical-doc", pep_banner_directive.CanonicalDocBanner) - app.add_directive( - "canonical-pypa-spec", pep_banner_directive.CanonicalPyPASpecBanner) - app.add_directive( - "canonical-typing-spec", pep_banner_directive.CanonicalTypingSpecBanner) + "canonical-typing-spec", pep_banner_directive.CanonicalTypingSpecBanner + ) app.add_directive("rejected", pep_banner_directive.RejectedBanner) app.add_directive("superseded", pep_banner_directive.SupersededBanner) app.add_directive("withdrawn", pep_banner_directive.WithdrawnBanner) # Register event callbacks - app.connect("builder-inited", _update_config_for_builder) # Update configuration values for builder used + app.connect( + "builder-inited", _update_config_for_builder + ) # Update configuration values for builder used app.connect("env-before-read-docs", create_pep_zero) # PEP 0 hook - app.connect('html-page-context', set_description) + app.connect("html-page-context", set_description) # Mathematics rendering inline_maths = HTMLTranslator.visit_math, None block_maths = HTMLTranslator.visit_math_block, None - app.add_html_math_renderer("maths_to_html", inline_maths, block_maths) # Render maths to HTML + app.add_html_math_renderer( + "maths_to_html", inline_maths, block_maths + ) # Render maths to HTML # Parallel safety: https://www.sphinx-doc.org/en/master/extdev/index.html#extension-metadata return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/pep_sphinx_extensions/generate_rss.py b/pep_sphinx_extensions/generate_rss.py index 296bb033e0e..3b8b95f73da 100644 --- a/pep_sphinx_extensions/generate_rss.py +++ b/pep_sphinx_extensions/generate_rss.py @@ -19,7 +19,7 @@ def _format_rfc_2822(datetime: dt.datetime) -> str: - datetime = datetime.replace(tzinfo=dt.timezone.utc) + datetime = datetime.replace(tzinfo=dt.UTC) return format_datetime(datetime, usegmt=True) @@ -64,14 +64,18 @@ def pep_abstract(document: nodes.document) -> str: return para_node.astext().strip().replace("\n", " ") return "" if title_node.astext() == "Introduction": - introduction = node.next_node(nodes.paragraph).astext().strip().replace("\n", " ") + introduction = ( + node.next_node(nodes.paragraph).astext().strip().replace("\n", " ") + ) return introduction def _generate_items(doctree_dir: Path): # get list of peps with creation time (from "Created:" string in pep source) - peps_with_dt = sorted((pep_creation(path), path) for path in doctree_dir.glob("pep-????.doctree")) + peps_with_dt = sorted( + (pep_creation(path), path) for path in doctree_dir.glob("pep-????.doctree") + ) # generate rss items for 10 most recent peps (in reverse order) for datetime, full_path in reversed(peps_with_dt[-10:]): @@ -86,7 +90,9 @@ def _generate_items(doctree_dir: Path): author = get_from_doctree(full_path, "Author") if "@" in author or " at " in author: parsed_authors = getaddresses([author]) - joined_authors = ", ".join(f"{name} ({email_address})" for name, email_address in parsed_authors) + joined_authors = ", ".join( + f"{name} ({email_address})" for name, email_address in parsed_authors + ) else: joined_authors = author @@ -104,7 +110,7 @@ def _generate_items(doctree_dir: Path): def create_rss_feed(doctree_dir: Path, output_dir: Path): # The rss envelope - last_build_date = _format_rfc_2822(dt.datetime.now(dt.timezone.utc)) + last_build_date = _format_rfc_2822(dt.datetime.now(dt.UTC)) items = "\n".join(_generate_items(Path(doctree_dir))) output = f"""\ diff --git a/pep_sphinx_extensions/pep_processor/html/pep_html_builder.py b/pep_sphinx_extensions/pep_processor/html/pep_html_builder.py index 467dd82670c..4790836f53a 100644 --- a/pep_sphinx_extensions/pep_processor/html/pep_html_builder.py +++ b/pep_sphinx_extensions/pep_processor/html/pep_html_builder.py @@ -1,10 +1,9 @@ from docutils import nodes from docutils.frontend import OptionParser +from sphinx.builders.dirhtml import DirectoryHTMLBuilder from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.writers.html import HTMLWriter -from sphinx.builders.dirhtml import DirectoryHTMLBuilder - class FileBuilder(StandaloneHTMLBuilder): copysource = False # Prevent unneeded source copying - we link direct to GitHub @@ -18,7 +17,9 @@ class FileBuilder(StandaloneHTMLBuilder): def prepare_writing(self, _doc_names: set[str]) -> None: self.docwriter = HTMLWriter(self) - _opt_parser = OptionParser([self.docwriter], defaults=self.env.settings, read_config_files=True) + _opt_parser = OptionParser( + [self.docwriter], defaults=self.env.settings, read_config_files=True + ) self.docsettings = _opt_parser.get_default_values() self._orig_css_files = self._orig_js_files = [] @@ -36,7 +37,7 @@ def get_doc_context(self, docname: str, body: str, _metatags: str) -> dict: if docname.startswith("pep-"): del toc_tree[0] # remove contents node from PEPs for node in toc_tree.findall(nodes.reference): - node["refuri"] = node["anchorname"] or '#' # fix targets + node["refuri"] = node["anchorname"] or "#" # fix targets toc = self.render_partial(toc_tree)["fragment"] else: toc = "" # PEPs with no sections -- 9, 210 diff --git a/pep_sphinx_extensions/pep_processor/html/pep_html_translator.py b/pep_sphinx_extensions/pep_processor/html/pep_html_translator.py index e8893790ef6..c65e2a1ad95 100644 --- a/pep_sphinx_extensions/pep_processor/html/pep_html_translator.py +++ b/pep_sphinx_extensions/pep_processor/html/pep_html_translator.py @@ -2,8 +2,8 @@ from typing import TYPE_CHECKING -from docutils import nodes import sphinx.writers.html5 as html5 +from docutils import nodes if TYPE_CHECKING: from sphinx.builders import html @@ -38,7 +38,11 @@ def should_be_compact_paragraph(node: nodes.paragraph) -> bool: # Only first paragraph can be compact (ignoring initial label & invisible nodes) first = isinstance(node.parent[0], nodes.label) - visible_siblings = [child for child in node.parent.children[first:] if not isinstance(child, nodes.Invisible)] + visible_siblings = [ + child + for child in node.parent.children[first:] + if not isinstance(child, nodes.Invisible) + ] if visible_siblings[0] is not node: return False @@ -58,13 +62,18 @@ def depart_paragraph(self, _: nodes.paragraph) -> None: self.body.append(self.context.pop()) def visit_footnote_reference(self, node): - self.body.append(self.starttag(node, "a", suffix="[", - CLASS=f"footnote-reference {self.settings.footnote_references}", - href=f"#{node['refid']}" - )) + self.body.append( + self.starttag( + node, + "a", + suffix="[", + CLASS=f"footnote-reference {self.settings.footnote_references}", + href=f"#{node['refid']}", + ) + ) def depart_footnote_reference(self, node): - self.body.append(']') + self.body.append("]") def visit_label(self, node): # pass parent node to get id into starttag: @@ -83,21 +92,29 @@ def depart_label(self, node) -> None: self.body.append(self.context.pop()) back_refs = node.parent["backrefs"] if self.settings.footnote_backlinks and len(back_refs) > 1: - back_links = ", ".join(f"{i}" for i, ref in enumerate(back_refs, start=1)) + back_links = ", ".join( + f"{i}" for i, ref in enumerate(back_refs, start=1) + ) self.body.append(f" ({back_links}) ") # Close the def tags self.body.append("\n