From 05a491f56eda4f2753e053f431763a712cd109da Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:38:55 +0300 Subject: [PATCH 01/12] str.isdecimal() guarantees int() succeeds --- src/blurb/_add.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/blurb/_add.py b/src/blurb/_add.py index 0c8388f..ce81f9b 100644 --- a/src/blurb/_add.py +++ b/src/blurb/_add.py @@ -182,20 +182,14 @@ def _extract_issue_number(issue: str | None, /) -> int | None: stripped = issue[3:] else: stripped = issue.removeprefix('#') - try: - if stripped.isdecimal(): - return int(stripped) - except ValueError: - pass + if stripped.isdecimal(): + return int(stripped) # Allow GitHub URL with or without the scheme stripped = issue.removeprefix('https://') stripped = stripped.removeprefix('github.com/python/cpython/issues/') - try: - if stripped.isdecimal(): - return int(stripped) - except ValueError: - pass + if stripped.isdecimal(): + return int(stripped) raise SystemExit(f'Invalid GitHub issue number: {issue}') From e52f7ce370c6f983a6537a8a6c30ef0676c5c8a1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:44:29 +0300 Subject: [PATCH 02/12] Derive _unsanitize_section from _sanitize_section instead of maintaining both --- src/blurb/_template.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/blurb/_template.py b/src/blurb/_template.py index f4e9618..cf3e869 100644 --- a/src/blurb/_template.py +++ b/src/blurb/_template.py @@ -60,12 +60,7 @@ 'Unsupported platforms': 'Unsupported_platforms', } -_unsanitize_section = { - 'C_API': 'C API', - 'Core_and_Builtins': 'Core and Builtins', - 'Tools-Demos': 'Tools/Demos', - 'Unsupported_platforms': 'Unsupported platforms', -} +_unsanitize_section = {v: k for k, v in _sanitize_section.items()} def sanitize_section(section: str, /) -> str: From 08e4dbbdbe2c44aac1bfc71b94f5dd788daa4e77 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:47:50 +0300 Subject: [PATCH 03/12] Remove redundant list call --- src/blurb/_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blurb/_versions.py b/src/blurb/_versions.py index e4d326e..bd6680e 100644 --- a/src/blurb/_versions.py +++ b/src/blurb/_versions.py @@ -14,7 +14,7 @@ def glob_versions() -> list[str]: def version_key(element: str, /) -> str: - fields = list(element.split('.')) + fields = element.split('.') if len(fields) == 1: return element From 276333300566035c6e9dbed3670165f0facd3400 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:50:00 +0300 Subject: [PATCH 04/12] Remove unused return and deduplicate --- src/blurb/_cli.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/blurb/_cli.py b/src/blurb/_cli.py index 7fb16e0..aaf40e6 100644 --- a/src/blurb/_cli.py +++ b/src/blurb/_cli.py @@ -23,12 +23,9 @@ def prompt(prompt: str, /) -> str: return input(f'[{prompt}> ') -def require_ok(prompt: str, /) -> str: - prompt = f'[{prompt}> ' - while True: - s = input(prompt).strip() - if s == 'ok': - return s +def require_ok(message: str, /) -> None: + while prompt(message).strip() != 'ok': + pass def build_parser() -> argparse.ArgumentParser: From 20ead92ef48ecf8e8ffe3ab168f06847bf1f6e67 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:06:41 +0300 Subject: [PATCH 05/12] Hoist helper function to avoid redefinition in loop --- src/blurb/_cli.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/blurb/_cli.py b/src/blurb/_cli.py index aaf40e6..edb22df 100644 --- a/src/blurb/_cli.py +++ b/src/blurb/_cli.py @@ -10,6 +10,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Callable from typing import NoReturn readme_re = re.compile(r'This is \w+ version \d+\.\d+').match @@ -134,6 +135,14 @@ def main(argv: list[str] | None = None) -> None: ns.func(ns) +def test_first_line(filename: str, test: Callable[[str], object]) -> bool: + if not os.path.exists(filename): + return False + with open(filename, encoding='utf-8') as file: + lines = file.read().split('\n') + return bool(lines and test(lines[0])) + + def chdir_to_repo_root() -> str: # find the root of the local CPython repo # note that we can't ask git, because we might @@ -150,15 +159,6 @@ def chdir_to_repo_root() -> str: os.chdir(path) - def test_first_line(filename, test): - if not os.path.exists(filename): - return False - with open(filename, encoding='utf-8') as file: - lines = file.read().split('\n') - if not (lines and test(lines[0])): - return False - return True - if not test_first_line('README.rst', readme_re): continue From b3c212ef254b4f47dc4811383beec82a50ef9031 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:12:01 +0300 Subject: [PATCH 06/12] Remove duplicate variable --- src/blurb/_release.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/blurb/_release.py b/src/blurb/_release.py index 148d56f..d64238c 100644 --- a/src/blurb/_release.py +++ b/src/blurb/_release.py @@ -34,6 +34,7 @@ def release(version: str) -> None: output = f'Misc/NEWS.d/{version}.rst' filenames = glob_blurbs('next') + count = len(filenames) blurbs = Blurbs() date = current_date() @@ -49,7 +50,6 @@ def release(version: str) -> None: } blurbs.append((metadata, body)) else: - count = len(filenames) print(f'Merging {count} blurbs to "{output}".') for filename in filenames: @@ -66,8 +66,7 @@ def release(version: str) -> None: git_add_files.append(output) flush_git_add_files() - how_many = len(filenames) - print(f"Removing {how_many} 'next' files from git.") + print(f"Removing {count} 'next' files from git.") git_rm_files.extend(filenames) flush_git_rm_files() From 68ce09fa0035188d9daf06d2ec8978f5165b12e7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:14:36 +0300 Subject: [PATCH 07/12] Merge imports --- src/blurb/_release.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/blurb/_release.py b/src/blurb/_release.py index d64238c..b0b1f14 100644 --- a/src/blurb/_release.py +++ b/src/blurb/_release.py @@ -3,8 +3,7 @@ import os import time -import blurb._blurb_file -from blurb._blurb_file import Blurbs +from blurb import _blurb_file from blurb._cli import error from blurb._git import ( flush_git_add_files, @@ -24,7 +23,7 @@ def release(version: str) -> None: if version == '.': # harvest version number from dirname of repo # I remind you, we're in the Misc subdir right now - version = os.path.basename(blurb._blurb_file.root) + version = os.path.basename(_blurb_file.root) existing_filenames = glob_blurbs(version) if existing_filenames: @@ -35,7 +34,7 @@ def release(version: str) -> None: output = f'Misc/NEWS.d/{version}.rst' filenames = glob_blurbs('next') count = len(filenames) - blurbs = Blurbs() + blurbs = _blurb_file.Blurbs() date = current_date() if not filenames: @@ -71,7 +70,7 @@ def release(version: str) -> None: flush_git_rm_files() # sanity check: ensuring that saving/reloading the merged blurb file works. - blurbs2 = Blurbs() + blurbs2 = _blurb_file.Blurbs() blurbs2.load(output) assert blurbs2 == blurbs, f"Reloading {output} isn't reproducible?!" From 639671791be16ebdbc933a5666ab70124a5d2230 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:15:33 +0300 Subject: [PATCH 08/12] Remove unnecessary 'nonlocal self' --- src/blurb/_blurb_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/blurb/_blurb_file.py b/src/blurb/_blurb_file.py index 48c0899..03bd48b 100644 --- a/src/blurb/_blurb_file.py +++ b/src/blurb/_blurb_file.py @@ -144,7 +144,6 @@ def finish_entry() -> None: nonlocal body nonlocal in_metadata nonlocal metadata - nonlocal self if not body: throw("Blurb 'body' text must not be empty!") From 3337d62ef94caf4a6b5b636850fd6ea695d63013 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:16:27 +0300 Subject: [PATCH 09/12] Fix comments --- src/blurb/_blurb_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blurb/_blurb_file.py b/src/blurb/_blurb_file.py index 03bd48b..e943ac0 100644 --- a/src/blurb/_blurb_file.py +++ b/src/blurb/_blurb_file.py @@ -78,7 +78,7 @@ * "release date" is the day a particular version of Python was released. * "no changes", if present, notes that there were no actual changes for this version. When used, there are two more things that must be - true about the the blurb file: + true about the blurb file: * There should only be one entry inside the blurb file. * That entry's gh-issue number must be 0. @@ -340,7 +340,7 @@ def _extract_next_filename(self) -> str: metadata ) elif int(metadata['bpo']) > 0: - # assume it's a GH issue number + # legacy bugs.python.org issue number path = '{root}/Misc/NEWS.d/next/{section}/{date}.bpo-{bpo}.{nonce}.rst'.format_map( metadata ) From 33efd7569fa97c02e2abb61abe201089fb16f353 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:18:11 +0300 Subject: [PATCH 10/12] Replace loop with generator expression --- src/blurb/_utils/text.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/blurb/_utils/text.py b/src/blurb/_utils/text.py index 39c0399..661481d 100644 --- a/src/blurb/_utils/text.py +++ b/src/blurb/_utils/text.py @@ -26,10 +26,7 @@ def textwrap_body(body: str | Iterable[str], *, subsequent_indent: str = '') -> # step 1: remove trailing whitespace from individual lines # (this means that empty lines will just have \n, no invisible whitespace) - lines = [] - for line in text.split('\n'): - lines.append(line.rstrip()) - text = '\n'.join(lines) + text = '\n'.join(line.rstrip() for line in text.split('\n')) # step 2: break into paragraphs and wrap those paragraphs = text.split('\n\n') paragraphs2 = [] From a34ac110b8a1fba8e8c874413e7769e54a2e97be Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:20:47 +0300 Subject: [PATCH 11/12] Rename test_first_line to first_line_matches --- src/blurb/_cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/blurb/_cli.py b/src/blurb/_cli.py index edb22df..d0aec20 100644 --- a/src/blurb/_cli.py +++ b/src/blurb/_cli.py @@ -135,7 +135,7 @@ def main(argv: list[str] | None = None) -> None: ns.func(ns) -def test_first_line(filename: str, test: Callable[[str], object]) -> bool: +def first_line_matches(filename: str, test: Callable[[str], object]) -> bool: if not os.path.exists(filename): return False with open(filename, encoding='utf-8') as file: @@ -159,10 +159,10 @@ def chdir_to_repo_root() -> str: os.chdir(path) - if not test_first_line('README.rst', readme_re): + if not first_line_matches('README.rst', readme_re): continue - if not test_first_line('LICENSE', 'A. HISTORY OF THE SOFTWARE'.__eq__): + if not first_line_matches('LICENSE', 'A. HISTORY OF THE SOFTWARE'.__eq__): continue if not os.path.exists('Include/Python.h'): continue From 925c3a0faf594cc2efce46951dbd4e4ead6352a6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:46:42 +0300 Subject: [PATCH 12/12] Remove rogue whitespace Co-authored-by: Stan Ulbrych --- src/blurb/_cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/blurb/_cli.py b/src/blurb/_cli.py index d0aec20..0ce6b8f 100644 --- a/src/blurb/_cli.py +++ b/src/blurb/_cli.py @@ -161,7 +161,6 @@ def chdir_to_repo_root() -> str: if not first_line_matches('README.rst', readme_re): continue - if not first_line_matches('LICENSE', 'A. HISTORY OF THE SOFTWARE'.__eq__): continue if not os.path.exists('Include/Python.h'):