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..618b38e --- /dev/null +++ b/src/blurb/_merge.py @@ -0,0 +1,119 @@ +import os +import sys +from pathlib import Path + +from blurb._cli import require_ok, subcommand +from blurb._versions import glob_versions, printable_version +from blurb.blurb import Blurbs, glob_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') 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..2fe2c5f 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 @@ -61,7 +58,6 @@ ) root = None # Set by chdir_to_repo_root() -original_dir = None def textwrap_body(body, *, subsequent_indent=''): @@ -157,70 +153,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) @@ -243,18 +180,6 @@ def glob_blurbs(version): 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 +458,6 @@ def _extract_next_filename(self): del metadata[name] return path - def save_next(self): assert len(self) == 1 blurb = type(self)() @@ -550,129 +474,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(): """ diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 2ade934..87801cf 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -93,63 +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 = ( @@ -209,22 +152,6 @@ def test_glob_blurbs_sort_order(fs): 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_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 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