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
") def visit_bullet_list(self, node): - if isinstance(node.parent, nodes.section) and "contents" in node.parent["names"]: + if ( + isinstance(node.parent, nodes.section) + and "contents" in node.parent["names"] + ): self.body.append("
Table of Contents") self.context.append("
") super().visit_bullet_list(node) def depart_bullet_list(self, node): super().depart_bullet_list(node) - if isinstance(node.parent, nodes.section) and "contents" in node.parent["names"]: + if ( + isinstance(node.parent, nodes.section) + and "contents" in node.parent["names"] + ): self.body.append(self.context.pop()) def unknown_visit(self, node: nodes.Node) -> None: diff --git a/pep_sphinx_extensions/pep_processor/parsing/pep_banner_directive.py b/pep_sphinx_extensions/pep_processor/parsing/pep_banner_directive.py index 5584719dd39..12a62102692 100644 --- a/pep_sphinx_extensions/pep_processor/parsing/pep_banner_directive.py +++ b/pep_sphinx_extensions/pep_processor/parsing/pep_banner_directive.py @@ -29,13 +29,14 @@ def run(self) -> list[nodes.admonition]: if self.arguments: link_content = self.arguments[0] - pre_text = self.admonition_pre_template.format( - link_content=link_content) + pre_text = self.admonition_pre_template.format(link_content=link_content) else: pre_text = self.admonition_pre_text - close_button_node = nodes.paragraph('', '', nodes.Text('×'), classes=['close-button']) - close_button_node['classes'].append('close-button') + close_button_node = nodes.paragraph( + "", "", nodes.Text("×"), classes=["close-button"] + ) + close_button_node["classes"].append("close-button") pre_text_node = nodes.paragraph(pre_text) pre_text_node.line = self.lineno @@ -50,13 +51,13 @@ def run(self) -> list[nodes.admonition]: source_lines = [pre_text] + list(self.content or []) + [post_text] admonition_node = self.admonition_class( - "\n".join(source_lines), classes=["pep-banner"] + self.css_classes) + "\n".join(source_lines), classes=["pep-banner"] + self.css_classes + ) admonition_node.append(pre_text_node) admonition_node.append(close_button_node) if self.content: - self.state.nested_parse( - self.content, self.content_offset, admonition_node) + self.state.nested_parse(self.content, self.content_offset, admonition_node) admonition_node.append(post_text_node) return [admonition_node] @@ -74,9 +75,7 @@ class CanonicalDocBanner(PEPBanner): "This PEP is a historical document. " "The up-to-date, canonical documentation can now be found elsewhere." ) - admonition_post_text = ( - "See :pep:`1` for how to propose changes." - ) + admonition_post_text = "See :pep:`1` for how to propose changes." css_classes = ["canonical-doc", "sticky-banner"] diff --git a/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py b/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py index 2ccbd6cb857..af64a53f98b 100644 --- a/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py +++ b/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py @@ -4,10 +4,12 @@ from sphinx import parsers -from pep_sphinx_extensions.pep_processor.transforms import pep_contents -from pep_sphinx_extensions.pep_processor.transforms import pep_footer -from pep_sphinx_extensions.pep_processor.transforms import pep_headers -from pep_sphinx_extensions.pep_processor.transforms import pep_title +from pep_sphinx_extensions.pep_processor.transforms import ( + pep_contents, + pep_footer, + pep_headers, + pep_title, +) if TYPE_CHECKING: from docutils import transforms diff --git a/pep_sphinx_extensions/pep_processor/parsing/pep_role.py b/pep_sphinx_extensions/pep_processor/parsing/pep_role.py index be1683a33ac..1760e284630 100644 --- a/pep_sphinx_extensions/pep_processor/parsing/pep_role.py +++ b/pep_sphinx_extensions/pep_processor/parsing/pep_role.py @@ -11,7 +11,9 @@ def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]: try: pep_num = int(pep_str) except ValueError: - msg = self.inliner.reporter.error(f'invalid PEP number {self.target}', line=self.lineno) + msg = self.inliner.reporter.error( + f"invalid PEP number {self.target}", line=self.lineno + ) prb = self.inliner.problematic(self.rawtext, self.rawtext, msg) return [prb], [msg] pep_base = self.inliner.document.settings.pep_url.format(pep_num) @@ -30,10 +32,11 @@ def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]: return [ nodes.reference( - "", title, + "", + title, internal=True, refuri=ref_uri, classes=["pep"], - _title_tuple=(pep_num, fragment) + _title_tuple=(pep_num, fragment), ) ], [] diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_contents.py b/pep_sphinx_extensions/pep_processor/transforms/pep_contents.py index 3810429ac78..4cd262f906f 100644 --- a/pep_sphinx_extensions/pep_processor/transforms/pep_contents.py +++ b/pep_sphinx_extensions/pep_processor/transforms/pep_contents.py @@ -2,8 +2,7 @@ from pathlib import Path -from docutils import nodes -from docutils import transforms +from docutils import nodes, transforms from docutils.transforms import parts @@ -37,6 +36,7 @@ def apply(self) -> None: class Contents(parts.Contents): """Build Table of Contents from document.""" + def __init__(self, document: nodes.document, startnode: nodes.Node | None = None): super().__init__(document, startnode) @@ -45,7 +45,8 @@ def __init__(self, document: nodes.document, startnode: nodes.Node | None = None self.backlinks = None def apply(self) -> None: - contents = self.build_contents(self.document[0][4:]) # skip PEP title, headers,
, and contents + # skip PEP title, headers,
, and contents + contents = self.build_contents(self.document[0][4:]) if contents: self.startnode.replace_self(contents) else: @@ -65,7 +66,7 @@ def build_contents(self, node: nodes.Node | list[nodes.Node], _level: None = Non # remove all pre-existing hyperlinks in the title (e.g. PEP references) while (link_node := title.next_node(nodes.reference)) is not None: link_node.replace_self(link_node[0]) - ref_id = section['ids'][0] + ref_id = section["ids"][0] title["refid"] = ref_id # Add a link to self entry_text = self.copy_and_filter(title) reference = nodes.reference("", "", refid=ref_id, *entry_text) @@ -74,5 +75,5 @@ def build_contents(self, node: nodes.Node | list[nodes.Node], _level: None = Non item += self.build_contents(section) # recurse to add sub-sections entries.append(item) if entries: - return nodes.bullet_list('', *entries) + return nodes.bullet_list("", *entries) return [] diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py b/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py index df96405479a..fbc3d6ace03 100644 --- a/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py +++ b/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py @@ -1,15 +1,14 @@ +import subprocess import time from pathlib import Path -import subprocess -from docutils import nodes -from docutils import transforms +from docutils import nodes, transforms class PEPFooter(transforms.Transform): """Footer transforms for PEPs. - - Remove the References/Footnotes section if it is empty when rendered. + - Remove the References/Footnotes section if it is empty when rendered. """ # Uses same priority as docutils.transforms.TargetNotes diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py index 34ae7cfc623..fb76e4f58a4 100644 --- a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py +++ b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py @@ -1,8 +1,7 @@ -from pathlib import Path import re +from pathlib import Path -from docutils import nodes -from docutils import transforms +from docutils import nodes, transforms from sphinx import errors from pep_sphinx_extensions.pep_processor.transforms import pep_zero @@ -44,6 +43,7 @@ "community process, workflow or governance", } + class PEPParsingError(errors.SphinxError): pass @@ -63,8 +63,13 @@ def apply(self) -> None: raise PEPParsingError("Document tree is empty.") header = self.document[0] - if not isinstance(header, nodes.field_list) or "rfc2822" not in header["classes"]: - raise PEPParsingError("Document does not begin with an RFC-2822 header; it is not a PEP.") + if ( + not isinstance(header, nodes.field_list) + or "rfc2822" not in header["classes"] + ): + raise PEPParsingError( + "Document does not begin with an RFC-2822 header; it is not a PEP." + ) # PEP number should be the first field pep_field = header[0] @@ -76,7 +81,9 @@ def apply(self) -> None: try: pep_num = int(pep_num_str) except ValueError: - raise PEPParsingError(f"PEP header must contain an integer. '{pep_num_str}' is invalid!") + raise PEPParsingError( + f"PEP header must contain an integer. '{pep_num_str}' is invalid!" + ) # Special processing for PEP 0. if pep_num == 0: @@ -116,8 +123,7 @@ def apply(self) -> None: elif name in {"discussions-to", "resolution", "post-history"}: # Prettify mailing list and Discourse links for node in para: - if (not isinstance(node, nodes.reference) - or not node["refuri"]): + if not isinstance(node, nodes.reference) or not node["refuri"]: continue # If the Resolution header is already a link, don't prettify it if name == "resolution" and node["refuri"] != node[0]: @@ -140,7 +146,10 @@ def apply(self) -> None: target = self.document.settings.pep_url.format(int(pep_str)) if self.document.settings.builder == "dirhtml": target = f"../{target}" - new_body += [nodes.reference("", pep_str, refuri=target), nodes.Text(", ")] + new_body += [ + nodes.reference("", pep_str, refuri=target), + nodes.Text(", "), + ] para[:] = new_body[:-1] # drop trailing space elif name == "topic": new_body = [] @@ -216,16 +225,14 @@ def _process_list_url(parts: list[str]) -> tuple[str, str]: # HyperKitty (Mailman3) archive structure is # https://mail.python.org/archives/list//thread/ if "archives" in parts: - list_name = ( - parts[parts.index("archives") + 2].removesuffix("@python.org")) + list_name = parts[parts.index("archives") + 2].removesuffix("@python.org") if len(parts) > 6 and parts[6] in {"message", "thread"}: item_type = parts[6] # Mailman3 list info structure is # https://mail.python.org/mailman3/lists/.python.org/ elif "mailman3" in parts: - list_name = ( - parts[parts.index("mailman3") + 2].removesuffix(".python.org")) + list_name = parts[parts.index("mailman3") + 2].removesuffix(".python.org") # Pipermail (Mailman) archive structure is # https://mail.python.org/pipermail//-/ @@ -240,8 +247,7 @@ def _process_list_url(parts: list[str]) -> tuple[str, str]: # Not a link to a mailing list, message or thread else: - raise ValueError( - f"{'/'.join(parts)} not a link to a list, message or thread") + raise ValueError(f"{'/'.join(parts)} not a link to a list, message or thread") return list_name, item_type @@ -251,7 +257,8 @@ def _process_discourse_url(parts: list[str]) -> tuple[str, str]: if len(parts) < 5 or ("t" not in parts and "c" not in parts): raise ValueError( - f"{'/'.join(parts)} not a link to a Discourse thread or category") + f"{'/'.join(parts)} not a link to a Discourse thread or category" + ) first_subpart = parts[4] has_title = not first_subpart.isnumeric() @@ -279,7 +286,8 @@ def _process_pretty_url(url: str) -> tuple[str, str]: item_name, item_type = LINK_PRETTIFIERS[parts[2]](parts) except KeyError as error: raise ValueError( - f"{url} not a link to a recognized domain to prettify") from error + f"{url} not a link to a recognized domain to prettify" + ) from error item_name = item_name.title().replace("Sig", "SIG").replace("Pep", "PEP") return item_name, item_type diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_references.py b/pep_sphinx_extensions/pep_processor/transforms/pep_references.py index 1e00e84cb20..cc2840e9208 100644 --- a/pep_sphinx_extensions/pep_processor/transforms/pep_references.py +++ b/pep_sphinx_extensions/pep_processor/transforms/pep_references.py @@ -1,7 +1,6 @@ from pathlib import Path -from docutils import nodes -from docutils import transforms +from docutils import nodes, transforms class PEPReferenceRoleTitleText(transforms.Transform): diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_title.py b/pep_sphinx_extensions/pep_processor/transforms/pep_title.py index de3ce4466a1..8665175d404 100644 --- a/pep_sphinx_extensions/pep_processor/transforms/pep_title.py +++ b/pep_sphinx_extensions/pep_processor/transforms/pep_title.py @@ -1,10 +1,7 @@ from pathlib import Path -from docutils import nodes -from docutils import transforms -from docutils import utils -from docutils.parsers.rst import roles -from docutils.parsers.rst import states +from docutils import nodes, transforms, utils +from docutils.parsers.rst import roles, states class PEPTitle(transforms.Transform): @@ -27,7 +24,9 @@ def apply(self) -> None: for field in self.document[0]: # Hold details of the attribute's tag against its details row_attributes = {sub.tagname: sub.rawsource for sub in field} - pep_header_details[row_attributes["field_name"]] = row_attributes["field_body"] + pep_header_details[row_attributes["field_name"]] = row_attributes[ + "field_body" + ] # Store the redundant fields in the table for removal if row_attributes["field_name"] in desired_fields: @@ -40,11 +39,17 @@ def apply(self) -> None: # Create the title string for the PEP pep_number = int(pep_header_details["PEP"]) pep_title = pep_header_details["Title"] - pep_title_string = f"PEP {pep_number} -- {pep_title}" # double hyphen for en dash + pep_title_string = ( + f"PEP {pep_number} -- {pep_title}" # double hyphen for en dash + ) # Generate the title section node and its properties title_nodes = _line_to_nodes(pep_title_string) - pep_title_node = nodes.section("", nodes.title("", "", *title_nodes, classes=["page-title"]), names=["pep-content"]) + pep_title_node = nodes.section( + "", + nodes.title("", "", *title_nodes, classes=["page-title"]), + names=["pep-content"], + ) # Insert the title node as the root element, move children down document_children = self.document.children @@ -60,7 +65,12 @@ def apply(self) -> None: def _line_to_nodes(text: str) -> list[nodes.Node]: """Parse RST string to nodes.""" document = utils.new_document("") - document.settings.pep_references = document.settings.rfc_references = False # patch settings - states.RSTStateMachine(state_classes=states.state_classes, initial_state="Body").run([text], document) # do parsing - roles._roles.pop("", None) # restore the "default" default role after parsing a document + # patch settings + document.settings.pep_references = document.settings.rfc_references = False + # do parsing + states.RSTStateMachine( + state_classes=states.state_classes, initial_state="Body" + ).run([text], document) + # restore the "default" default role after parsing a document + roles._roles.pop("", None) return document[0].children diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_zero.py b/pep_sphinx_extensions/pep_processor/transforms/pep_zero.py index 18a34b03090..824067f2f32 100644 --- a/pep_sphinx_extensions/pep_processor/transforms/pep_zero.py +++ b/pep_sphinx_extensions/pep_processor/transforms/pep_zero.py @@ -1,7 +1,6 @@ from __future__ import annotations -from docutils import nodes -from docutils import transforms +from docutils import nodes, transforms class PEPZero(transforms.Transform): diff --git a/pep_sphinx_extensions/pep_zero_generator/constants.py b/pep_sphinx_extensions/pep_zero_generator/constants.py index 9ce1c4af555..17062e33703 100644 --- a/pep_sphinx_extensions/pep_zero_generator/constants.py +++ b/pep_sphinx_extensions/pep_zero_generator/constants.py @@ -12,8 +12,15 @@ # Valid values for the Status header. STATUS_VALUES = { - STATUS_ACCEPTED, STATUS_PROVISIONAL, STATUS_REJECTED, STATUS_WITHDRAWN, - STATUS_DEFERRED, STATUS_FINAL, STATUS_ACTIVE, STATUS_DRAFT, STATUS_SUPERSEDED, + STATUS_ACCEPTED, + STATUS_PROVISIONAL, + STATUS_REJECTED, + STATUS_WITHDRAWN, + STATUS_DEFERRED, + STATUS_FINAL, + STATUS_ACTIVE, + STATUS_DRAFT, + STATUS_SUPERSEDED, } # Map of invalid/special statuses to their valid counterparts SPECIAL_STATUSES = { diff --git a/pep_sphinx_extensions/pep_zero_generator/errors.py b/pep_sphinx_extensions/pep_zero_generator/errors.py index deb12021ac9..a0b8bc291a3 100644 --- a/pep_sphinx_extensions/pep_zero_generator/errors.py +++ b/pep_sphinx_extensions/pep_zero_generator/errors.py @@ -10,7 +10,7 @@ def __init__(self, error: str, pep_file: Path, pep_number: int | None = None): self.number = pep_number def __str__(self): - error_msg = super(PEPError, self).__str__() + error_msg = super().__str__() error_msg = f"({self.filename}): {error_msg}" pep_str = f"PEP {self.number}" return f"{pep_str} {error_msg}" if self.number is not None else error_msg diff --git a/pep_sphinx_extensions/pep_zero_generator/parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py index 6877d680606..f36b57fcb12 100644 --- a/pep_sphinx_extensions/pep_zero_generator/parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/parser.py @@ -7,20 +7,23 @@ from email.parser import HeaderParser from pathlib import Path -from pep_sphinx_extensions.pep_zero_generator.constants import ACTIVE_ALLOWED -from pep_sphinx_extensions.pep_zero_generator.constants import HIDE_STATUS -from pep_sphinx_extensions.pep_zero_generator.constants import SPECIAL_STATUSES -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES -from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_STANDARDS -from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES +from pep_sphinx_extensions.pep_zero_generator.constants import ( + ACTIVE_ALLOWED, + HIDE_STATUS, + SPECIAL_STATUSES, + STATUS_ACTIVE, + STATUS_PROVISIONAL, + STATUS_VALUES, + TYPE_STANDARDS, + TYPE_VALUES, +) from pep_sphinx_extensions.pep_zero_generator.errors import PEPError @dataclasses.dataclass(order=True, frozen=True) class _Author: """Represent PEP authors.""" + full_name: str # The author's name. email: str # The author's email address. @@ -53,7 +56,9 @@ def __init__(self, filename: Path): metadata = HeaderParser().parsestr(pep_text) required_header_misses = PEP.required_headers - set(metadata.keys()) if required_header_misses: - _raise_pep_error(self, f"PEP is missing required headers {required_header_misses}") + _raise_pep_error( + self, f"PEP is missing required headers {required_header_misses}" + ) try: self.number = int(metadata["PEP"]) @@ -62,7 +67,9 @@ def __init__(self, filename: Path): # Check PEP number matches filename if self.number != int(filename.stem[4:]): - _raise_pep_error(self, f"PEP number does not match file name ({filename})", pep_num=True) + _raise_pep_error( + self, f"PEP number does not match file name ({filename})", pep_num=True + ) # Title self.title: str = metadata["Title"] @@ -70,14 +77,18 @@ def __init__(self, filename: Path): # Type self.pep_type: str = metadata["Type"] if self.pep_type not in TYPE_VALUES: - _raise_pep_error(self, f"{self.pep_type} is not a valid Type value", pep_num=True) + _raise_pep_error( + self, f"{self.pep_type} is not a valid Type value", pep_num=True + ) # Status status = metadata["Status"] if status in SPECIAL_STATUSES: status = SPECIAL_STATUSES[status] if status not in STATUS_VALUES: - _raise_pep_error(self, f"{status} is not a valid Status value", pep_num=True) + _raise_pep_error( + self, f"{status} is not a valid Status value", pep_num=True + ) # Special case for Active PEPs. if status == STATUS_ACTIVE and self.pep_type not in ACTIVE_ALLOWED: @@ -97,7 +108,9 @@ def __init__(self, filename: Path): # Topic (for sub-indices) _topic = metadata.get("Topic", "").lower().split(",") - self.topic: set[str] = {topic for topic_raw in _topic if (topic := topic_raw.strip())} + self.topic: set[str] = { + topic for topic_raw in _topic if (topic := topic_raw.strip()) + } # Other headers self.created = metadata["Created"] @@ -187,11 +200,14 @@ def _parse_author(data: str) -> list[_Author]: """Return a list of author names and emails.""" author_list = [] - data = (data.replace("\n", " ") - .replace(", Jr", jr_placeholder) - .rstrip().removesuffix(",")) + data = ( + data.replace("\n", " ") + .replace(", Jr", jr_placeholder) + .rstrip() + .removesuffix(",") + ) for author_email in data.split(", "): - if ' <' in author_email: + if " <" in author_email: author, email = author_email.removesuffix(">").split(" <") else: author, email = author_email, "" diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py index a9f21d11940..8b70b10055d 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py @@ -15,6 +15,7 @@ to allow it to be processed as normal. """ + from __future__ import annotations import json @@ -22,11 +23,13 @@ from pathlib import Path from typing import TYPE_CHECKING -from pep_sphinx_extensions.pep_zero_generator import parser -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 import parser, subindices, writer from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC -from release_management.serialize import create_release_cycle, create_release_schedule_calendar, create_release_json +from release_management.serialize import ( + create_release_cycle, + create_release_json, + create_release_schedule_calendar, +) if TYPE_CHECKING: from sphinx.application import Sphinx @@ -81,14 +84,14 @@ def create_pep_zero(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> release_peps = build_release_peps(peps) - numerical_index_text = writer.PEPZeroWriter( - release_peps - ).write_numerical_index(peps) + numerical_index_text = writer.PEPZeroWriter(release_peps).write_numerical_index( + peps + ) subindices.update_sphinx("numerical", numerical_index_text, docnames, env) - pep0_text = writer.PEPZeroWriter( - release_peps - ).write_pep0(peps, builder=env.settings["builder"]) + pep0_text = writer.PEPZeroWriter(release_peps).write_pep0( + peps, builder=env.settings["builder"] + ) pep0_path = subindices.update_sphinx("pep-0000", pep0_text, docnames, env) peps.append(parser.PEP(pep0_path)) @@ -103,10 +106,16 @@ def create_pep_zero(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> write_peps_json(peps, Path(app.outdir)) release_cycle = create_release_cycle() - app.outdir.joinpath('api/release-cycle.json').write_text(release_cycle, encoding="utf-8") + app.outdir.joinpath("api/release-cycle.json").write_text( + release_cycle, encoding="utf-8" + ) release_json = create_release_json() - app.outdir.joinpath('api/python-releases.json').write_text(release_json, encoding="utf-8") + app.outdir.joinpath("api/python-releases.json").write_text( + release_json, encoding="utf-8" + ) release_ical = create_release_schedule_calendar() - app.outdir.joinpath('release-schedule.ics').write_text(release_ical, encoding="utf-8") + app.outdir.joinpath("release-schedule.ics").write_text( + release_ical, encoding="utf-8" + ) diff --git a/pep_sphinx_extensions/pep_zero_generator/subindices.py b/pep_sphinx_extensions/pep_zero_generator/subindices.py index fdf167d6049..b6d7c9d9eca 100644 --- a/pep_sphinx_extensions/pep_zero_generator/subindices.py +++ b/pep_sphinx_extensions/pep_zero_generator/subindices.py @@ -14,7 +14,9 @@ from pep_sphinx_extensions.pep_zero_generator.parser import PEP -def update_sphinx(filename: str, text: str, docnames: list[str], env: BuildEnvironment) -> Path: +def update_sphinx( + filename: str, text: str, docnames: list[str], env: BuildEnvironment +) -> Path: file_path = Path(env.srcdir, f"{filename}.rst") # Only write and schedule for rebuild if content actually changed try: @@ -69,7 +71,9 @@ def generate_subindices( def generate_topic_contents(docnames: list[str], env: BuildEnvironment): - update_sphinx("topic/index", """\ + update_sphinx( + "topic/index", + """\ .. _topic-index: Topic Index @@ -83,4 +87,7 @@ def generate_topic_contents(docnames: list[str], env: BuildEnvironment): :glob: * -""", docnames, env) +""", + docnames, + env, + ) diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index a5033149bb5..037ab7d878e 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -2,25 +2,29 @@ from __future__ import annotations -from typing import TYPE_CHECKING import unicodedata +from typing import TYPE_CHECKING -from pep_sphinx_extensions.pep_processor.transforms.pep_headers import ABBREVIATED_STATUSES -from pep_sphinx_extensions.pep_processor.transforms.pep_headers import ABBREVIATED_TYPES -from pep_sphinx_extensions.pep_zero_generator.constants import DEAD_STATUSES -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACCEPTED -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DEFERRED -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DRAFT -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_FINAL -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_REJECTED -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_WITHDRAWN -from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC -from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_INFO -from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_PROCESS -from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES +from pep_sphinx_extensions.pep_processor.transforms.pep_headers import ( + ABBREVIATED_STATUSES, + ABBREVIATED_TYPES, +) +from pep_sphinx_extensions.pep_zero_generator.constants import ( + DEAD_STATUSES, + STATUS_ACCEPTED, + STATUS_ACTIVE, + STATUS_DEFERRED, + STATUS_DRAFT, + STATUS_FINAL, + STATUS_PROVISIONAL, + STATUS_REJECTED, + STATUS_VALUES, + STATUS_WITHDRAWN, + SUBINDICES_BY_TOPIC, + TYPE_INFO, + TYPE_PROCESS, + TYPE_VALUES, +) from pep_sphinx_extensions.pep_zero_generator.errors import PEPError if TYPE_CHECKING: @@ -210,11 +214,24 @@ def write_pep0( # PEPs by category self.emit_title("Index by Category") - meta, info, provisional, accepted, open_, finished, historical, deferred, dead = _classify_peps(peps) + ( + meta, + info, + provisional, + accepted, + open_, + finished, + historical, + deferred, + dead, + ) = _classify_peps(peps) pep_categories = [ ("Process and Meta-PEPs", meta), ("Other Informational PEPs", info), - ("Provisional PEPs (provisionally accepted; interface may still change)", provisional), + ( + "Provisional PEPs (provisionally accepted; interface may still change)", + provisional, + ), ("Accepted PEPs (accepted; may not be implemented yet)", accepted), ("Open PEPs (under consideration)", open_), ("Finished PEPs (done, with a stable interface)", finished), @@ -222,7 +239,7 @@ def write_pep0( ("Deferred PEPs (postponed pending further research or updates)", deferred), ("Rejected, Superseded, and Withdrawn PEPs", dead), ] - for (category, peps_in_category) in pep_categories: + for category, peps_in_category in pep_categories: # For sub-indices, only emit categories with entries. # For PEP 0, emit every category, but only with a table when it has entries. if len(peps_in_category) > 0: @@ -285,7 +302,9 @@ def write_pep0( for author_name in _sort_authors(authors_dict): # Use the email from authors_dict instead of the one from "author" as # the author instance may have an empty email. - self.emit_text(f"{author_name:{max_name_len}} {authors_dict[author_name]}") + self.emit_text( + f"{author_name:{max_name_len}} {authors_dict[author_name]}" + ) self.emit_author_table_separator(max_name_len) self.emit_newline() self.emit_newline() @@ -326,7 +345,10 @@ def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]: # Hack until the conflict between the use of "Final" # for both API definition PEPs and other (actually # obsolete) PEPs is addressed - if pep.status == STATUS_ACTIVE or "release schedule" not in pep.title.lower(): + if ( + pep.status == STATUS_ACTIVE + or "release schedule" not in pep.title.lower() + ): info.append(pep) else: historical.append(pep) @@ -337,8 +359,20 @@ def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]: elif pep.status == STATUS_FINAL: finished.append(pep) else: - raise PEPError(f"Unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number) - return meta, info, provisional, accepted, open_, finished, historical, deferred, dead + raise PEPError( + f"Unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number + ) + return ( + meta, + info, + provisional, + accepted, + open_, + finished, + historical, + deferred, + dead, + ) def _verify_email_addresses(peps: list[PEP]) -> dict[str, str]: @@ -360,7 +394,7 @@ def _verify_email_addresses(peps: list[PEP]) -> dict[str, str]: # Combine multiple email addresses with commas. Since peps is # sorted by PEP number, this should produce a deterministic # output. - return {name: ', '.join(emails) for name, emails in authors_dict.items()} + return {name: ", ".join(emails) for name, emails in authors_dict.items()} def _sort_authors(authors_dict: dict[str, str]) -> list[str]: diff --git a/peps/conf.py b/peps/conf.py index 6daec648954..a8e986869ab 100644 --- a/peps/conf.py +++ b/peps/conf.py @@ -4,8 +4,8 @@ """Configuration for building PEPs using Sphinx.""" import os -from pathlib import Path import sys +from pathlib import Path _ROOT = Path(__file__).resolve().parent.parent sys.path.append(os.fspath(_ROOT)) From ab50db5cc3cbf051b164a171371106155614dfa6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:29:20 +0300 Subject: [PATCH 5/5] Add check-readthedocs --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 937b7f9049e..c0a3a207949 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,6 +70,7 @@ repos: hooks: - id: check-dependabot - id: check-github-workflows + - id: check-readthedocs - repo: https://github.com/rhysd/actionlint rev: v1.7.12