From c9662f0a4880a54efee5934ccabeb6e8edea6bf3 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 13 Aug 2025 01:48:56 +0100 Subject: [PATCH 1/4] Move 'blurb merge' to ``blurb._merge`` --- src/blurb/_cli.py | 12 ++- src/blurb/_merge.py | 146 ++++++++++++++++++++++++++ src/blurb/_versions.py | 67 ++++++++++++ src/blurb/blurb.py | 226 +---------------------------------------- 4 files changed, 224 insertions(+), 227 deletions(-) create mode 100644 src/blurb/_merge.py create mode 100644 src/blurb/_versions.py diff --git a/src/blurb/_cli.py b/src/blurb/_cli.py index 7e214a1..352964e 100644 --- a/src/blurb/_cli.py +++ b/src/blurb/_cli.py @@ -27,6 +27,14 @@ 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 subcommand(fn: CommandFunc): global subcommands subcommands[fn.__name__] = fn @@ -138,7 +146,6 @@ def _blurb_help() -> None: def main() -> None: - global original_dir args = sys.argv[1:] @@ -157,8 +164,9 @@ def main() -> None: if fn in (help, version): raise SystemExit(fn(*args)) + import blurb._merge + blurb._merge.original_dir = os.getcwd() try: - original_dir = os.getcwd() chdir_to_repo_root() # map keyword arguments to options diff --git a/src/blurb/_merge.py b/src/blurb/_merge.py new file mode 100644 index 0000000..443fd4e --- /dev/null +++ b/src/blurb/_merge.py @@ -0,0 +1,146 @@ +import glob +import os +import sys +from pathlib import Path + +from blurb._cli import require_ok, subcommand +from blurb._template import ( + next_filename_unsanitize_sections, sanitize_section, + sanitize_section_legacy, sections, +) +from blurb._versions import glob_versions, printable_version +from blurb.blurb import Blurbs, textwrap_body + +original_dir: str = os.getcwd() + + +@subcommand +def merge(output: str | None = None, *, forced: bool = False) -> None: + """Merge all blurbs together into a single Misc/NEWS file. + + Optional output argument specifies where to write to. + Default is /Misc/NEWS. + + If overwriting, blurb merge will prompt you to make sure it's okay. + To force it to overwrite, use -f. + """ + if output: + output = os.path.join(original_dir, output) + else: + output = 'Misc/NEWS' + + versions = glob_versions() + if not versions: + sys.exit("You literally don't have ANY blurbs to merge together!") + + if os.path.exists(output) and not forced: + print(f'You already have a {output!r} file.') + require_ok('Type ok to overwrite') + + write_news(output, versions=versions) + + +def write_news(output: str, *, versions: list[str]) -> None: + buff = [] + + def prnt(msg: str = '', /): + buff.append(msg) + + prnt(""" ++++++++++++ +Python News ++++++++++++ + +""".strip()) + + for version in versions: + filenames = glob_blurbs(version) + + blurbs = Blurbs() + if version == 'next': + for filename in filenames: + if os.path.basename(filename) == 'README.rst': + continue + blurbs.load_next(filename) + if not blurbs: + continue + metadata = blurbs[0][0] + metadata['release date'] = 'XXXX-XX-XX' + else: + assert len(filenames) == 1 + blurbs.load(filenames[0]) + + header = f"What's New in Python {printable_version(version)}?" + prnt() + prnt(header) + prnt('=' * len(header)) + prnt() + + metadata, body = blurbs[0] + release_date = metadata['release date'] + + prnt(f'*Release date: {release_date}*') + prnt() + + if 'no changes' in metadata: + prnt(body) + prnt() + continue + + last_section = None + for metadata, body in blurbs: + section = metadata['section'] + if last_section != section: + last_section = section + prnt(section) + prnt('-' * len(section)) + prnt() + if metadata.get('gh-issue'): + issue_number = metadata['gh-issue'] + if int(issue_number): + body = f'gh-{issue_number}: {body}' + elif metadata.get('bpo'): + issue_number = metadata['bpo'] + if int(issue_number): + body = f'bpo-{issue_number}: {body}' + + body = f'- {body}' + text = textwrap_body(body, subsequent_indent=' ') + prnt(text) + prnt() + prnt('**(For information about older versions, consult the HISTORY file.)**') + + new_contents = '\n'.join(buff) + + # Only write in `output` if the contents are different + # This speeds up subsequent Sphinx builds + try: + previous_contents = Path(output).read_text(encoding='utf-8') + except (FileNotFoundError, UnicodeError): + previous_contents = None + if new_contents != previous_contents: + Path(output).write_text(new_contents, encoding='utf-8') + else: + print(output, 'is already up to date') + + +def glob_blurbs(version: str) -> list[str]: + filenames = [] + base = os.path.join('Misc', 'NEWS.d', version) + if version != 'next': + wildcard = f'{base}.rst' + filenames.extend(glob.glob(wildcard)) + else: + sanitized_sections = ( + {sanitize_section(section) for section in sections} | + {sanitize_section_legacy(section) for section in sections} + ) + for section in sanitized_sections: + wildcard = os.path.join(base, section, '*.rst') + entries = glob.glob(wildcard) + deletables = [x for x in entries if x.endswith('/README.rst')] + for filename in deletables: + entries.remove(filename) + filenames.extend(entries) + filenames.sort(reverse=True, key=next_filename_unsanitize_sections) + return filenames diff --git a/src/blurb/_versions.py b/src/blurb/_versions.py new file mode 100644 index 0000000..2b56cd0 --- /dev/null +++ b/src/blurb/_versions.py @@ -0,0 +1,67 @@ +import glob +import sys + +if sys.version_info[:2] >= (3, 11): + from contextlib import chdir +else: + import os + + class chdir: + def __init__(self, path: str, /) -> None: + self.path = path + + def __enter__(self) -> None: + self.previous_cwd = os.getcwd() + os.chdir(self.path) + + def __exit__(self, *args) -> None: + os.chdir(self.previous_cwd) + + +def glob_versions() -> list[str]: + versions = [] + with chdir('Misc/NEWS.d'): + for wildcard in ('2.*.rst', '3.*.rst', 'next'): + versions += [x.partition('.rst')[0] for x in glob.glob(wildcard)] + versions.sort(key=version_key, reverse=True) + return versions + + +def version_key(element: str, /) -> str: + fields = list(element.split('.')) + if len(fields) == 1: + return element + + # in sorted order, + # 3.5.0a1 < 3.5.0b1 < 3.5.0rc1 < 3.5.0 + # so for sorting purposes we transform + # "3.5." and "3.5.0" into "3.5.0zz0" + last = fields.pop() + for s in ('a', 'b', 'rc'): + if s in last: + last, stage, stage_version = last.partition(s) + break + else: + stage = 'zz' + stage_version = '0' + + fields.append(last) + while len(fields) < 3: + fields.append('0') + + fields.extend([stage, stage_version]) + fields = [s.rjust(6, '0') for s in fields] + + return '.'.join(fields) + + +def printable_version(version: str, /) -> str: + if version == 'next': + return version + if 'a' in version: + return version.replace('a', ' alpha ') + if 'b' in version: + return version.replace('b', ' beta ') + if 'rc' in version: + return version.replace('rc', ' release candidate ') + return version + ' final' diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 3fff901..3cd4822 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -40,13 +40,10 @@ # automatic git adds and removes import base64 -import builtins import glob import hashlib -import io import itertools import os -from pathlib import Path import re import shutil import sys @@ -55,13 +52,9 @@ from blurb._cli import main, subcommand from blurb._git import git_add_files, flush_git_add_files -from blurb._template import ( - next_filename_unsanitize_sections, sanitize_section, - sanitize_section_legacy, sections, unsanitize_section, -) +from blurb._template import sanitize_section, sections, unsanitize_section root = None # Set by chdir_to_repo_root() -original_dir = None def textwrap_body(body, *, subsequent_indent=''): @@ -157,104 +150,11 @@ def sortable_datetime(): return time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime()) -def require_ok(prompt): - prompt = f"[{prompt}> " - while True: - s = input(prompt).strip() - if s == 'ok': - return s - -class pushd: - def __init__(self, path): - self.path = path - - def __enter__(self): - self.previous_cwd = os.getcwd() - os.chdir(self.path) - - def __exit__(self, *args): - os.chdir(self.previous_cwd) - - -def version_key(element): - fields = list(element.split(".")) - if len(fields) == 1: - return element - - # in sorted order, - # 3.5.0a1 < 3.5.0b1 < 3.5.0rc1 < 3.5.0 - # so for sorting purposes we transform - # "3.5." and "3.5.0" into "3.5.0zz0" - last = fields.pop() - for s in ("a", "b", "rc"): - if s in last: - last, stage, stage_version = last.partition(s) - break - else: - stage = 'zz' - stage_version = "0" - - fields.append(last) - while len(fields) < 3: - fields.append("0") - - fields.extend([stage, stage_version]) - fields = [s.rjust(6, "0") for s in fields] - - return ".".join(fields) - - def nonceify(body): digest = hashlib.md5(body.encode("utf-8")).digest() return base64.urlsafe_b64encode(digest)[0:6].decode('ascii') -def glob_versions(): - with pushd("Misc/NEWS.d"): - versions = [] - for wildcard in ("2.*.rst", "3.*.rst", "next"): - files = [x.partition(".rst")[0] for x in glob.glob(wildcard)] - versions.extend(files) - xform = [version_key(x) for x in versions] - xform.sort(reverse=True) - versions = sorted(versions, key=version_key, reverse=True) - return versions - - -def glob_blurbs(version): - filenames = [] - base = os.path.join("Misc", "NEWS.d", version) - if version != "next": - wildcard = base + ".rst" - filenames.extend(glob.glob(wildcard)) - else: - sanitized_sections = ( - {sanitize_section(section) for section in sections} | - {sanitize_section_legacy(section) for section in sections} - ) - for section in sanitized_sections: - wildcard = os.path.join(base, section, "*.rst") - entries = glob.glob(wildcard) - deletables = [x for x in entries if x.endswith("/README.rst")] - for filename in deletables: - entries.remove(filename) - filenames.extend(entries) - filenames.sort(reverse=True, key=next_filename_unsanitize_sections) - return filenames - - -def printable_version(version): - if version == "next": - return version - if "a" in version: - return version.replace("a", " alpha ") - if "b" in version: - return version.replace("b", " beta ") - if "rc" in version: - return version.replace("rc", " release candidate ") - return version + " final" - - class BlurbError(RuntimeError): pass @@ -533,7 +433,6 @@ def _extract_next_filename(self): del metadata[name] return path - def save_next(self): assert len(self) == 1 blurb = type(self)() @@ -550,129 +449,6 @@ def error(*a): sys.exit("Error: " + s) -def _find_blurb_dir(): - if os.path.isdir("blurb"): - return "blurb" - for path in glob.iglob("blurb-*"): - if os.path.isdir(path): - return path - return None - - -@subcommand -def merge(output=None, *, forced=False): - """ -Merge all blurbs together into a single Misc/NEWS file. - -Optional output argument specifies where to write to. -Default is /Misc/NEWS. - -If overwriting, blurb merge will prompt you to make sure it's okay. -To force it to overwrite, use -f. - """ - if output: - output = os.path.join(original_dir, output) - else: - output = "Misc/NEWS" - - versions = glob_versions() - if not versions: - sys.exit("You literally don't have ANY blurbs to merge together!") - - if os.path.exists(output) and not forced: - builtins.print("You already have a", repr(output), "file.") - require_ok("Type ok to overwrite") - - write_news(output, versions=versions) - - -def write_news(output, *, versions): - buff = io.StringIO() - - def print(*a, sep=" "): - s = sep.join(str(x) for x in a) - return builtins.print(s, file=buff) - - print (""" -+++++++++++ -Python News -+++++++++++ - -""".strip()) - - for version in versions: - filenames = glob_blurbs(version) - - blurbs = Blurbs() - if version == "next": - for filename in filenames: - if os.path.basename(filename) == "README.rst": - continue - blurbs.load_next(filename) - if not blurbs: - continue - metadata = blurbs[0][0] - metadata['release date'] = "XXXX-XX-XX" - else: - assert len(filenames) == 1 - blurbs.load(filenames[0]) - - header = "What's New in Python " + printable_version(version) + "?" - print() - print(header) - print("=" * len(header)) - print() - - - metadata, body = blurbs[0] - release_date = metadata["release date"] - - print(f"*Release date: {release_date}*") - print() - - if "no changes" in metadata: - print(body) - print() - continue - - last_section = None - for metadata, body in blurbs: - section = metadata['section'] - if last_section != section: - last_section = section - print(section) - print("-" * len(section)) - print() - if metadata.get("gh-issue"): - issue_number = metadata['gh-issue'] - if int(issue_number): - body = "gh-" + issue_number + ": " + body - elif metadata.get("bpo"): - issue_number = metadata['bpo'] - if int(issue_number): - body = "bpo-" + issue_number + ": " + body - - body = "- " + body - text = textwrap_body(body, subsequent_indent=' ') - print(text) - print() - print("**(For information about older versions, consult the HISTORY file.)**") - - - new_contents = buff.getvalue() - - # Only write in `output` if the contents are different - # This speeds up subsequent Sphinx builds - try: - previous_contents = Path(output).read_text(encoding="UTF-8") - except (FileNotFoundError, UnicodeError): - previous_contents = None - if new_contents != previous_contents: - Path(output).write_text(new_contents, encoding="UTF-8") - else: - builtins.print(output, "is already up to date") - - @subcommand def populate(): """ From aa3bf0577daaea72d273e3bd1f59036a7ab34290 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 13 Aug 2025 01:52:24 +0100 Subject: [PATCH 2/4] Split tests --- tests/test_blurb.py | 132 ----------------------------------------- tests/test_merge.py | 60 +++++++++++++++++++ tests/test_versions.py | 76 ++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 132 deletions(-) create mode 100644 tests/test_merge.py create mode 100644 tests/test_versions.py diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 2ade934..97f8d94 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -93,138 +93,6 @@ def test_sortable_datetime(): assert blurb.sortable_datetime() == "2025-01-07-16-28-41" -@pytest.mark.parametrize( - "version1, version2", - ( - ("2", "3"), - ("3.5.0a1", "3.5.0b1"), - ("3.5.0a1", "3.5.0rc1"), - ("3.5.0a1", "3.5.0"), - ("3.6.0b1", "3.6.0b2"), - ("3.6.0b1", "3.6.0rc1"), - ("3.6.0b1", "3.6.0"), - ("3.7.0rc1", "3.7.0rc2"), - ("3.7.0rc1", "3.7.0"), - ("3.8", "3.8.1"), - ), -) -def test_version_key(version1, version2): - # Act - key1 = blurb.version_key(version1) - key2 = blurb.version_key(version2) - - # Assert - assert key1 < key2 - - -def test_glob_versions(fs): - # Arrange - fake_version_blurbs = ( - "Misc/NEWS.d/3.7.0.rst", - "Misc/NEWS.d/3.7.0a1.rst", - "Misc/NEWS.d/3.7.0a2.rst", - "Misc/NEWS.d/3.7.0b1.rst", - "Misc/NEWS.d/3.7.0b2.rst", - "Misc/NEWS.d/3.7.0rc1.rst", - "Misc/NEWS.d/3.7.0rc2.rst", - "Misc/NEWS.d/3.9.0b1.rst", - "Misc/NEWS.d/3.12.0a1.rst", - ) - for fn in fake_version_blurbs: - fs.create_file(fn) - - # Act - versions = blurb.glob_versions() - - # Assert - assert versions == [ - "3.12.0a1", - "3.9.0b1", - "3.7.0", - "3.7.0rc2", - "3.7.0rc1", - "3.7.0b2", - "3.7.0b1", - "3.7.0a2", - "3.7.0a1", - ] - - -def test_glob_blurbs_next(fs): - # Arrange - fake_news_entries = ( - "Misc/NEWS.d/next/Library/2022-04-11-18-34-33.gh-issue-11111.pC7gnM.rst", - "Misc/NEWS.d/next/Core and Builtins/2023-03-17-12-09-45.gh-issue-33333.Pf_BI7.rst", - "Misc/NEWS.d/next/Tools-Demos/2023-03-21-01-27-07.gh-issue-44444.2F1Byz.rst", - "Misc/NEWS.d/next/C API/2023-03-27-22-09-07.gh-issue-66666.3SN8Bs.rst", - ) - fake_readmes = ( - "Misc/NEWS.d/next/Library/README.rst", - "Misc/NEWS.d/next/Core and Builtins/README.rst", - "Misc/NEWS.d/next/Tools-Demos/README.rst", - "Misc/NEWS.d/next/C API/README.rst", - ) - for fn in fake_news_entries + fake_readmes: - fs.create_file(fn) - - # Act - filenames = blurb.glob_blurbs("next") - - # Assert - assert set(filenames) == set(fake_news_entries) - - -def test_glob_blurbs_sort_order(fs): - """ - It shouldn't make a difference to sorting whether - section names have spaces or underscores. - """ - # Arrange - fake_news_entries = ( - "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-01-00.gh-issue-33331.Pf_BI1.rst", - "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-02-00.gh-issue-33332.Pf_BI2.rst", - "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-03-00.gh-issue-33333.Pf_BI3.rst", - "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-04-00.gh-issue-33334.Pf_BI4.rst", - ) - # As fake_news_entries, but reverse sorted by *filename* only - expected = [ - "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-04-00.gh-issue-33334.Pf_BI4.rst", - "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-03-00.gh-issue-33333.Pf_BI3.rst", - "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-02-00.gh-issue-33332.Pf_BI2.rst", - "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-01-00.gh-issue-33331.Pf_BI1.rst", - ] - fake_readmes = ( - "Misc/NEWS.d/next/Library/README.rst", - "Misc/NEWS.d/next/Core and Builtins/README.rst", - "Misc/NEWS.d/next/Tools-Demos/README.rst", - "Misc/NEWS.d/next/C API/README.rst", - ) - for fn in fake_news_entries + fake_readmes: - fs.create_file(fn) - - # Act - filenames = blurb.glob_blurbs("next") - - # Assert - assert filenames == expected - - -@pytest.mark.parametrize( - "version, expected", - ( - ("next", "next"), - ("3.12.0a1", "3.12.0 alpha 1"), - ("3.12.0b2", "3.12.0 beta 2"), - ("3.12.0rc2", "3.12.0 release candidate 2"), - ("3.12.0", "3.12.0 final"), - ("3.12.1", "3.12.1 final"), - ), -) -def test_printable_version(version, expected): - # Act / Assert - assert blurb.printable_version(version) == expected - - @pytest.mark.parametrize( "news_entry, expected_section", ( diff --git a/tests/test_merge.py b/tests/test_merge.py new file mode 100644 index 0000000..9271643 --- /dev/null +++ b/tests/test_merge.py @@ -0,0 +1,60 @@ +from blurb._merge import glob_blurbs + + +def test_glob_blurbs_next(fs): + # Arrange + fake_news_entries = ( + "Misc/NEWS.d/next/Library/2022-04-11-18-34-33.gh-issue-11111.pC7gnM.rst", + "Misc/NEWS.d/next/Core and Builtins/2023-03-17-12-09-45.gh-issue-33333.Pf_BI7.rst", + "Misc/NEWS.d/next/Tools-Demos/2023-03-21-01-27-07.gh-issue-44444.2F1Byz.rst", + "Misc/NEWS.d/next/C API/2023-03-27-22-09-07.gh-issue-66666.3SN8Bs.rst", + ) + fake_readmes = ( + "Misc/NEWS.d/next/Library/README.rst", + "Misc/NEWS.d/next/Core and Builtins/README.rst", + "Misc/NEWS.d/next/Tools-Demos/README.rst", + "Misc/NEWS.d/next/C API/README.rst", + ) + for fn in fake_news_entries + fake_readmes: + fs.create_file(fn) + + # Act + filenames = glob_blurbs("next") + + # Assert + assert set(filenames) == set(fake_news_entries) + + +def test_glob_blurbs_sort_order(fs): + """ + It shouldn't make a difference to sorting whether + section names have spaces or underscores. + """ + # Arrange + fake_news_entries = ( + "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-01-00.gh-issue-33331.Pf_BI1.rst", + "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-02-00.gh-issue-33332.Pf_BI2.rst", + "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-03-00.gh-issue-33333.Pf_BI3.rst", + "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-04-00.gh-issue-33334.Pf_BI4.rst", + ) + # As fake_news_entries, but reverse sorted by *filename* only + expected = [ + "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-04-00.gh-issue-33334.Pf_BI4.rst", + "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-03-00.gh-issue-33333.Pf_BI3.rst", + "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-02-00.gh-issue-33332.Pf_BI2.rst", + "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-01-00.gh-issue-33331.Pf_BI1.rst", + ] + fake_readmes = ( + "Misc/NEWS.d/next/Library/README.rst", + "Misc/NEWS.d/next/Core and Builtins/README.rst", + "Misc/NEWS.d/next/Tools-Demos/README.rst", + "Misc/NEWS.d/next/C API/README.rst", + ) + for fn in fake_news_entries + fake_readmes: + fs.create_file(fn) + + # Act + filenames = glob_blurbs("next") + + # Assert + assert filenames == expected diff --git a/tests/test_versions.py b/tests/test_versions.py new file mode 100644 index 0000000..8f34882 --- /dev/null +++ b/tests/test_versions.py @@ -0,0 +1,76 @@ +import pytest + +from blurb._versions import glob_versions, printable_version, version_key + + +@pytest.mark.parametrize( + "version1, version2", + ( + ("2", "3"), + ("3.5.0a1", "3.5.0b1"), + ("3.5.0a1", "3.5.0rc1"), + ("3.5.0a1", "3.5.0"), + ("3.6.0b1", "3.6.0b2"), + ("3.6.0b1", "3.6.0rc1"), + ("3.6.0b1", "3.6.0"), + ("3.7.0rc1", "3.7.0rc2"), + ("3.7.0rc1", "3.7.0"), + ("3.8", "3.8.1"), + ), +) +def test_version_key(version1, version2): + # Act + key1 = version_key(version1) + key2 = version_key(version2) + + # Assert + assert key1 < key2 + + +def test_glob_versions(fs): + # Arrange + fake_version_blurbs = ( + "Misc/NEWS.d/3.7.0.rst", + "Misc/NEWS.d/3.7.0a1.rst", + "Misc/NEWS.d/3.7.0a2.rst", + "Misc/NEWS.d/3.7.0b1.rst", + "Misc/NEWS.d/3.7.0b2.rst", + "Misc/NEWS.d/3.7.0rc1.rst", + "Misc/NEWS.d/3.7.0rc2.rst", + "Misc/NEWS.d/3.9.0b1.rst", + "Misc/NEWS.d/3.12.0a1.rst", + ) + for fn in fake_version_blurbs: + fs.create_file(fn) + + # Act + versions = glob_versions() + + # Assert + assert versions == [ + "3.12.0a1", + "3.9.0b1", + "3.7.0", + "3.7.0rc2", + "3.7.0rc1", + "3.7.0b2", + "3.7.0b1", + "3.7.0a2", + "3.7.0a1", + ] + + +@pytest.mark.parametrize( + "version, expected", + ( + ("next", "next"), + ("3.12.0a1", "3.12.0 alpha 1"), + ("3.12.0b2", "3.12.0 beta 2"), + ("3.12.0rc2", "3.12.0 release candidate 2"), + ("3.12.0", "3.12.0 final"), + ("3.12.1", "3.12.1 final"), + ), +) +def test_printable_version(version, expected): + # Act / Assert + assert printable_version(version) == expected From 2bc32347785cc0c3eb92a28152d710cd87f51d8d Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 13 Aug 2025 01:56:17 +0100 Subject: [PATCH 3/4] Undo moving glob_blurbs --- src/blurb/_merge.py | 29 +--------------------- src/blurb/blurb.py | 27 +++++++++++++++++++- tests/test_blurb.py | 59 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_merge.py | 60 --------------------------------------------- 4 files changed, 86 insertions(+), 89 deletions(-) delete mode 100644 tests/test_merge.py diff --git a/src/blurb/_merge.py b/src/blurb/_merge.py index 443fd4e..618b38e 100644 --- a/src/blurb/_merge.py +++ b/src/blurb/_merge.py @@ -1,15 +1,10 @@ -import glob import os import sys from pathlib import Path from blurb._cli import require_ok, subcommand -from blurb._template import ( - next_filename_unsanitize_sections, sanitize_section, - sanitize_section_legacy, sections, -) from blurb._versions import glob_versions, printable_version -from blurb.blurb import Blurbs, textwrap_body +from blurb.blurb import Blurbs, glob_blurbs, textwrap_body original_dir: str = os.getcwd() @@ -122,25 +117,3 @@ def prnt(msg: str = '', /): Path(output).write_text(new_contents, encoding='utf-8') else: print(output, 'is already up to date') - - -def glob_blurbs(version: str) -> list[str]: - filenames = [] - base = os.path.join('Misc', 'NEWS.d', version) - if version != 'next': - wildcard = f'{base}.rst' - filenames.extend(glob.glob(wildcard)) - else: - sanitized_sections = ( - {sanitize_section(section) for section in sections} | - {sanitize_section_legacy(section) for section in sections} - ) - for section in sanitized_sections: - wildcard = os.path.join(base, section, '*.rst') - entries = glob.glob(wildcard) - deletables = [x for x in entries if x.endswith('/README.rst')] - for filename in deletables: - entries.remove(filename) - filenames.extend(entries) - filenames.sort(reverse=True, key=next_filename_unsanitize_sections) - return filenames diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 3cd4822..2fe2c5f 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -52,7 +52,10 @@ from blurb._cli import main, subcommand from blurb._git import git_add_files, flush_git_add_files -from blurb._template import sanitize_section, sections, unsanitize_section +from blurb._template import ( + next_filename_unsanitize_sections, sanitize_section, + sanitize_section_legacy, sections, unsanitize_section, +) root = None # Set by chdir_to_repo_root() @@ -155,6 +158,28 @@ def nonceify(body): return base64.urlsafe_b64encode(digest)[0:6].decode('ascii') +def glob_blurbs(version): + filenames = [] + base = os.path.join("Misc", "NEWS.d", version) + if version != "next": + wildcard = base + ".rst" + filenames.extend(glob.glob(wildcard)) + else: + sanitized_sections = ( + {sanitize_section(section) for section in sections} | + {sanitize_section_legacy(section) for section in sections} + ) + for section in sanitized_sections: + wildcard = os.path.join(base, section, "*.rst") + entries = glob.glob(wildcard) + deletables = [x for x in entries if x.endswith("/README.rst")] + for filename in deletables: + entries.remove(filename) + filenames.extend(entries) + filenames.sort(reverse=True, key=next_filename_unsanitize_sections) + return filenames + + class BlurbError(RuntimeError): pass diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 97f8d94..87801cf 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -93,6 +93,65 @@ def test_sortable_datetime(): assert blurb.sortable_datetime() == "2025-01-07-16-28-41" +def test_glob_blurbs_next(fs): + # Arrange + fake_news_entries = ( + "Misc/NEWS.d/next/Library/2022-04-11-18-34-33.gh-issue-11111.pC7gnM.rst", + "Misc/NEWS.d/next/Core and Builtins/2023-03-17-12-09-45.gh-issue-33333.Pf_BI7.rst", + "Misc/NEWS.d/next/Tools-Demos/2023-03-21-01-27-07.gh-issue-44444.2F1Byz.rst", + "Misc/NEWS.d/next/C API/2023-03-27-22-09-07.gh-issue-66666.3SN8Bs.rst", + ) + fake_readmes = ( + "Misc/NEWS.d/next/Library/README.rst", + "Misc/NEWS.d/next/Core and Builtins/README.rst", + "Misc/NEWS.d/next/Tools-Demos/README.rst", + "Misc/NEWS.d/next/C API/README.rst", + ) + for fn in fake_news_entries + fake_readmes: + fs.create_file(fn) + + # Act + filenames = blurb.glob_blurbs("next") + + # Assert + assert set(filenames) == set(fake_news_entries) + + +def test_glob_blurbs_sort_order(fs): + """ + It shouldn't make a difference to sorting whether + section names have spaces or underscores. + """ + # Arrange + fake_news_entries = ( + "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-01-00.gh-issue-33331.Pf_BI1.rst", + "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-02-00.gh-issue-33332.Pf_BI2.rst", + "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-03-00.gh-issue-33333.Pf_BI3.rst", + "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-04-00.gh-issue-33334.Pf_BI4.rst", + ) + # As fake_news_entries, but reverse sorted by *filename* only + expected = [ + "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-04-00.gh-issue-33334.Pf_BI4.rst", + "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-03-00.gh-issue-33333.Pf_BI3.rst", + "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-02-00.gh-issue-33332.Pf_BI2.rst", + "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-01-00.gh-issue-33331.Pf_BI1.rst", + ] + fake_readmes = ( + "Misc/NEWS.d/next/Library/README.rst", + "Misc/NEWS.d/next/Core and Builtins/README.rst", + "Misc/NEWS.d/next/Tools-Demos/README.rst", + "Misc/NEWS.d/next/C API/README.rst", + ) + for fn in fake_news_entries + fake_readmes: + fs.create_file(fn) + + # Act + filenames = blurb.glob_blurbs("next") + + # Assert + assert filenames == expected + + @pytest.mark.parametrize( "news_entry, expected_section", ( diff --git a/tests/test_merge.py b/tests/test_merge.py deleted file mode 100644 index 9271643..0000000 --- a/tests/test_merge.py +++ /dev/null @@ -1,60 +0,0 @@ -from blurb._merge import glob_blurbs - - -def test_glob_blurbs_next(fs): - # Arrange - fake_news_entries = ( - "Misc/NEWS.d/next/Library/2022-04-11-18-34-33.gh-issue-11111.pC7gnM.rst", - "Misc/NEWS.d/next/Core and Builtins/2023-03-17-12-09-45.gh-issue-33333.Pf_BI7.rst", - "Misc/NEWS.d/next/Tools-Demos/2023-03-21-01-27-07.gh-issue-44444.2F1Byz.rst", - "Misc/NEWS.d/next/C API/2023-03-27-22-09-07.gh-issue-66666.3SN8Bs.rst", - ) - fake_readmes = ( - "Misc/NEWS.d/next/Library/README.rst", - "Misc/NEWS.d/next/Core and Builtins/README.rst", - "Misc/NEWS.d/next/Tools-Demos/README.rst", - "Misc/NEWS.d/next/C API/README.rst", - ) - for fn in fake_news_entries + fake_readmes: - fs.create_file(fn) - - # Act - filenames = glob_blurbs("next") - - # Assert - assert set(filenames) == set(fake_news_entries) - - -def test_glob_blurbs_sort_order(fs): - """ - It shouldn't make a difference to sorting whether - section names have spaces or underscores. - """ - # Arrange - fake_news_entries = ( - "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-01-00.gh-issue-33331.Pf_BI1.rst", - "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-02-00.gh-issue-33332.Pf_BI2.rst", - "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-03-00.gh-issue-33333.Pf_BI3.rst", - "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-04-00.gh-issue-33334.Pf_BI4.rst", - ) - # As fake_news_entries, but reverse sorted by *filename* only - expected = [ - "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-04-00.gh-issue-33334.Pf_BI4.rst", - "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-03-00.gh-issue-33333.Pf_BI3.rst", - "Misc/NEWS.d/next/Core_and_Builtins/2023-07-23-12-02-00.gh-issue-33332.Pf_BI2.rst", - "Misc/NEWS.d/next/Core and Builtins/2023-07-23-12-01-00.gh-issue-33331.Pf_BI1.rst", - ] - fake_readmes = ( - "Misc/NEWS.d/next/Library/README.rst", - "Misc/NEWS.d/next/Core and Builtins/README.rst", - "Misc/NEWS.d/next/Tools-Demos/README.rst", - "Misc/NEWS.d/next/C API/README.rst", - ) - for fn in fake_news_entries + fake_readmes: - fs.create_file(fn) - - # Act - filenames = glob_blurbs("next") - - # Assert - assert filenames == expected From e8064b342bc5e4604d1bfafa486e2ecdfdd39d73 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 13 Aug 2025 01:58:32 +0100 Subject: [PATCH 4/4] fixup! Move 'blurb merge' to ``blurb._merge`` --- tests/test_parser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 4b5b3f3..ca6e724 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,7 +3,8 @@ import pytest -from blurb.blurb import Blurbs, pushd +from blurb._versions import chdir +from blurb.blurb import Blurbs class TestParserPasses: @@ -19,7 +20,7 @@ def filename_test(self, filename): assert str(b) == expected def test_files(self): - with pushd(self.directory): + with chdir(self.directory): for filename in glob.glob("*"): if filename.endswith(".res"): assert os.path.exists(filename[:-4]), filename