From d81eda293c548312a57624608f9cf67e1ea236f6 Mon Sep 17 00:00:00 2001 From: Ezio Melotti Date: Tue, 2 Jul 2024 10:22:30 +0800 Subject: [PATCH 01/29] Make 3.13 tests optional --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 388811d..ed61f74 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,9 @@ jobs: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + include: + - python-version: "3.13" + continue-on-error: true steps: - uses: actions/checkout@v4 From 93114c0cb8319369c34ff332c642d775ff0da0bc Mon Sep 17 00:00:00 2001 From: Ezio Melotti Date: Tue, 2 Jul 2024 10:32:45 +0800 Subject: [PATCH 02/29] Move the `continue-on-error` under the steps --- .github/workflows/test.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ed61f74..455718b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,9 +12,6 @@ jobs: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - include: - - python-version: "3.13" - continue-on-error: true steps: - uses: actions/checkout@v4 @@ -42,3 +39,5 @@ jobs: flags: ${{ matrix.python-version }} name: Python ${{ matrix.python-version }} token: ${{ secrets.CODECOV_ORG_TOKEN }} + + continue-on-error: ${{ matrix.python-version == '3.13' }} From a0e13d98712ef7be818f33d0cd6255163167ac59 Mon Sep 17 00:00:00 2001 From: Ezio Melotti Date: Tue, 2 Jul 2024 10:40:14 +0800 Subject: [PATCH 03/29] Set 3.13 as experimental --- .github/workflows/test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 455718b..2b41301 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,10 +8,15 @@ env: jobs: test: runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + experimental: [false] + include: + - python-version: "3.13" + experimental: true steps: - uses: actions/checkout@v4 @@ -39,5 +44,3 @@ jobs: flags: ${{ matrix.python-version }} name: Python ${{ matrix.python-version }} token: ${{ secrets.CODECOV_ORG_TOKEN }} - - continue-on-error: ${{ matrix.python-version == '3.13' }} From 499e70f51c8f987951671725d0cd185a44c68dfe Mon Sep 17 00:00:00 2001 From: Ezio Melotti Date: Tue, 2 Jul 2024 10:42:20 +0800 Subject: [PATCH 04/29] Remove 3.13 from the testing matrix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b41301..cc5da21 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] experimental: [false] include: - python-version: "3.13" From 2c6b8037c89ac2cfd64b9d98fa8ea45b64dece4a Mon Sep 17 00:00:00 2001 From: Ezio Melotti Date: Tue, 2 Jul 2024 21:29:10 +0800 Subject: [PATCH 05/29] Try to apply the `continue-on-error` to the step --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cc5da21..ddaf2c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,7 @@ jobs: python -m pip install -U tox - name: Tox tests + continue-on-error: ${{ matrix.experimental }} run: | tox -e py From 6fe73969c32a62511427d6b59a993abaedb24f90 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:45:22 +0200 Subject: [PATCH 06/29] Fix warning[artipacked]: credential persistence through GitHub Actions artifacts --- .github/workflows/lint.yml | 2 ++ .github/workflows/release.yml | 1 + .github/workflows/test.yml | 2 ++ 3 files changed, 5 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0dc0bab..2bc58cf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,6 +14,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-python@v5 with: python-version: "3.x" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4c6267e..4a0a7d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false - uses: hynek/build-and-inspect-python-package@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 276a00b..be3a04f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 From 71e679f3bb78a1554803bf8a7862754db26af187 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:57:53 +0200 Subject: [PATCH 07/29] Attestations are now on by default --- .github/workflows/release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a0a7d2..a9d7511 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,6 @@ jobs: - name: Publish to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: - attestations: true repository-url: https://test.pypi.org/legacy/ # Publish to PyPI on GitHub Releases. @@ -83,5 +82,3 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - with: - attestations: true From 4dc2e02b0dee2c393e82dd72af2334016a8050c2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:00:44 +0200 Subject: [PATCH 08/29] Lint with tox-dev/action-pre-commit-uv and test with tox-uv --- .github/workflows/lint.yml | 3 +-- .github/workflows/test.yml | 10 +++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2bc58cf..e535eb6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,5 +19,4 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.x" - cache: pip - - uses: pre-commit/action@v3.0.1 + - uses: tox-dev/action-pre-commit-uv@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be3a04f..ba5e9a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,17 +26,13 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true - cache: pip - - name: Install dependencies - run: | - python --version - python -m pip install -U pip - python -m pip install -U tox + - name: Install uv + uses: hynek/setup-cached-uv@v2 - name: Tox tests run: | - tox -e py + uvx --with tox-uv tox -e py - name: Upload coverage uses: codecov/codecov-action@v4 From 4cb4857c8cb98df1a93fc8b9074401c0ef818d47 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 9 Nov 2024 18:49:58 +0200 Subject: [PATCH 09/29] Unit test Blurbs.parse --- tests/test_blurb.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 9ff3a8d..09eb12f 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -188,3 +188,60 @@ def test_version(capfd): # Assert captured = capfd.readouterr() assert captured.out.startswith("blurb version ") + + +def test_parse(): + # Arrange + contents = ".. gh-issue: 123456\n.. section: IDLE\nHello world!" + blurbs = blurb.Blurbs() + + # Act + blurbs.parse(contents) + + # Assert + metadata, body = blurbs[0] + assert metadata["gh-issue"] == "123456" + assert metadata["section"] == "IDLE" + assert body == "Hello world!\n" + + +@pytest.mark.parametrize( + "contents, expected_error", + ( + ( + "", + r"Blurb 'body' text must not be empty!", + ), + ( + "gh-issue: Hello world!", + r"Blurb 'body' can't start with 'gh-'!", + ), + ( + "..gh-issue: 1\n..section: IDLE\nHello world!", + r"The gh-issue number must be 32426 or above, not a PR number", + ), + ( + "..bpo: one-two\n..section: IDLE\nHello world!", + r"Invalid bpo issue number! \('one-two'\)", + ), + ( + "..gh-issue: 123456\n..section: Funky Kong\nHello world!", + r"Invalid section 'Funky Kong'! You must use one of the predefined sections", + ), + ( + "..gh-issue: 123456\nHello world!", + r"No 'section' specified. You must provide one!", + ), + ( + ".. gh-issue: 123456\n.. section: IDLE\n.. section: IDLE\nHello world!", + r"Blurb metadata sets 'section' twice!", + ), + ), +) +def test_parse_no_body(contents, expected_error): + # Arrange + blurbs = blurb.Blurbs() + + # Act / Assert + with pytest.raises(blurb.BlurbError, match=expected_error): + blurbs.parse(contents) From 8e590fcd3f5abac027ddef48417dc071efcb09b3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 9 Nov 2024 18:58:14 +0200 Subject: [PATCH 10/29] Check gh-issue- is int before checking range for better error Invalid GitHub issue number! ('one-two') instead of: ValueError: invalid literal for int() with base 10: 'one-two' --- src/blurb/blurb.py | 6 +++--- tests/test_blurb.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 0af3ea9..ec0e73d 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -482,15 +482,15 @@ def finish_entry(): # we'll complain about the *first* error # we see in the blurb file, which is a # better user experience. - if key == "gh-issue" and int(value) < lowest_possible_gh_issue_number: - throw(f"The gh-issue number must be {lowest_possible_gh_issue_number} or above, not a PR number.") - if key in issue_keys: try: int(value) except (TypeError, ValueError): throw(f"Invalid {issue_keys[key]} issue number! ({value!r})") + if key == "gh-issue" and int(value) < lowest_possible_gh_issue_number: + throw(f"The gh-issue number must be {lowest_possible_gh_issue_number} or above, not a PR number.") + if key == "section": if no_changes: continue diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 09eb12f..7e898fa 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -224,6 +224,10 @@ def test_parse(): "..bpo: one-two\n..section: IDLE\nHello world!", r"Invalid bpo issue number! \('one-two'\)", ), + ( + "..gh-issue: one-two\n..section: IDLE\nHello world!", + r"Invalid GitHub issue number! \('one-two'\)", + ), ( "..gh-issue: 123456\n..section: Funky Kong\nHello world!", r"Invalid section 'Funky Kong'! You must use one of the predefined sections", From 657a617f61845c964c51ce6570c474fb09c7b97a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 9 Nov 2024 19:40:56 +0200 Subject: [PATCH 11/29] Add spaces --- tests/test_blurb.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 7e898fa..1ac7b02 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -217,23 +217,23 @@ def test_parse(): r"Blurb 'body' can't start with 'gh-'!", ), ( - "..gh-issue: 1\n..section: IDLE\nHello world!", + ".. gh-issue: 1\n.. section: IDLE\nHello world!", r"The gh-issue number must be 32426 or above, not a PR number", ), ( - "..bpo: one-two\n..section: IDLE\nHello world!", + ".. bpo: one-two\n.. section: IDLE\nHello world!", r"Invalid bpo issue number! \('one-two'\)", ), ( - "..gh-issue: one-two\n..section: IDLE\nHello world!", + ".. gh-issue: one-two\n.. section: IDLE\nHello world!", r"Invalid GitHub issue number! \('one-two'\)", ), ( - "..gh-issue: 123456\n..section: Funky Kong\nHello world!", + ".. gh-issue: 123456\n.. section: Funky Kong\nHello world!", r"Invalid section 'Funky Kong'! You must use one of the predefined sections", ), ( - "..gh-issue: 123456\nHello world!", + ".. gh-issue: 123456\nHello world!", r"No 'section' specified. You must provide one!", ), ( From 04b6913c6624610405064ac1ca99f12e0db0e02f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 9 Nov 2024 19:44:35 +0200 Subject: [PATCH 12/29] Ensure gh-issue or bpo exists in metadata --- src/blurb/blurb.py | 3 +++ tests/test_blurb.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index ec0e73d..bb09b71 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -497,6 +497,9 @@ def finish_entry(): if value not in sections: throw(f"Invalid section {value!r}! You must use one of the predefined sections.") + if "gh-issue" not in metadata and "bpo" not in metadata: + throw("'gh-issue:' or 'bpo:' must be specified in the metadata!") + if not 'section' in metadata: throw("No 'section' specified. You must provide one!") diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 1ac7b02..baebd2e 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -240,6 +240,10 @@ def test_parse(): ".. gh-issue: 123456\n.. section: IDLE\n.. section: IDLE\nHello world!", r"Blurb metadata sets 'section' twice!", ), + ( + ".. section: IDLE\nHello world!", + r"'gh-issue:' or 'bpo:' must be specified in the metadata!", + ), ), ) def test_parse_no_body(contents, expected_error): From 3213deeeee6ba93237322b1c685ac0fd05de5412 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:16:22 +0200 Subject: [PATCH 13/29] Test version handling functions --- tests/test_blurb.py | 73 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 9ff3a8d..0dd947a 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -45,6 +45,63 @@ def test_unsanitize_section_changed(section, expected): assert unsanitized == expected +@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 = ( @@ -104,6 +161,22 @@ 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", ( From ac5a10133077a655a3c8b40adaaf85ab197d7a02 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:40:39 +0200 Subject: [PATCH 14/29] Remove unused test --- tests/test_blurb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_blurb.py b/tests/test_blurb.py index baebd2e..3570e79 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -1,5 +1,4 @@ import pytest -from pyfakefs.fake_filesystem import FakeFilesystem from blurb import blurb From bf284aeae41ed5555089bb384b0485f691540e4f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:47:53 +0200 Subject: [PATCH 15/29] Move 'blurb test' subcommand into test suite --- .coveragerc | 1 + src/blurb/blurb.py | 69 +------------------------------------------- tests/test_parser.py | 35 ++++++++++++++++++++++ tox.ini | 2 -- 4 files changed, 37 insertions(+), 70 deletions(-) create mode 100644 tests/test_parser.py diff --git a/.coveragerc b/.coveragerc index dcd3739..81ba1c7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,3 +10,4 @@ exclude_also = [run] omit = **/blurb/__main__.py + **/blurb/_version.py diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 0af3ea9..c671d0c 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -57,7 +57,6 @@ import tempfile import textwrap import time -import unittest from . import __version__ @@ -639,42 +638,6 @@ def save_next(self): return filename -tests_run = 0 - -class TestParserPasses(unittest.TestCase): - directory = "tests/pass" - - def filename_test(self, filename): - b = Blurbs() - b.load(filename) - self.assertTrue(b) - if os.path.exists(filename + '.res'): - with open(filename + '.res', encoding='utf-8') as file: - expected = file.read() - self.assertEqual(str(b), expected) - - def test_files(self): - global tests_run - with pushd(self.directory): - for filename in glob.glob("*"): - if filename[-4:] == '.res': - self.assertTrue(os.path.exists(filename[:-4]), filename) - continue - self.filename_test(filename) - print(".", end="") - sys.stdout.flush() - tests_run += 1 - - -class TestParserFailures(TestParserPasses): - directory = "tests/fail" - - def filename_test(self, filename): - b = Blurbs() - with self.assertRaises(Exception): - b.load(filename) - - readme_re = re.compile(r"This is \w+ version \d+\.\d+").match def chdir_to_repo_root(): @@ -838,36 +801,6 @@ def _find_blurb_dir(): return None -@subcommand -def test(*args): - """ -Run unit tests. Only works inside source repo, not when installed. - """ - # unittest.main doesn't work because this isn't a module - # so we'll do it ourselves - - while (blurb_dir := _find_blurb_dir()) is None: - old_dir = os.getcwd() - os.chdir("..") - if old_dir == os.getcwd(): - # we reached the root and never found it! - sys.exit("Error: Couldn't find the root of your blurb repo!") - os.chdir(blurb_dir) - - print("-" * 79) - - for clsname, cls in sorted(globals().items()): - if clsname.startswith("Test") and isinstance(cls, type): - o = cls() - for fnname in sorted(dir(o)): - if fnname.startswith("test"): - fn = getattr(o, fnname) - if callable(fn): - fn() - print() - print(tests_run, "tests passed.") - - def find_editor(): for var in 'GIT_EDITOR', 'EDITOR': editor = os.environ.get(var) @@ -1224,7 +1157,7 @@ def main(): fn = get_subcommand(subcommand) # hack - if fn in (help, test, version): + if fn in (help, version): sys.exit(fn(*args)) try: diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..4bacc12 --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,35 @@ +import glob +import os +import unittest + +from blurb.blurb import Blurbs, pushd + + +class TestParserPasses(unittest.TestCase): + directory = "tests/pass" + + def filename_test(self, filename): + b = Blurbs() + b.load(filename) + self.assertTrue(b) + if os.path.exists(filename + ".res"): + with open(filename + ".res", encoding="utf-8") as file: + expected = file.read() + self.assertEqual(str(b), expected) + + def test_files(self): + with pushd(self.directory): + for filename in glob.glob("*"): + if filename[-4:] == ".res": + self.assertTrue(os.path.exists(filename[:-4]), filename) + continue + self.filename_test(filename) + + +class TestParserFailures(TestParserPasses): + directory = "tests/fail" + + def filename_test(self, filename): + b = Blurbs() + with self.assertRaises(Exception): + b.load(filename) diff --git a/tox.ini b/tox.ini index fa69718..1ed9746 100644 --- a/tox.ini +++ b/tox.ini @@ -17,9 +17,7 @@ commands = --cov-report term \ --cov-report xml \ {posargs} - blurb test blurb help blurb --version - {envpython} -I -m blurb test {envpython} -I -m blurb help {envpython} -I -m blurb version From 0444b4809c1f444001d94c779438b6059317b66d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:15:43 +0200 Subject: [PATCH 16/29] Convert to pytest style --- tests/test_parser.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 4bacc12..4b5b3f3 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,27 +1,28 @@ import glob import os -import unittest + +import pytest from blurb.blurb import Blurbs, pushd -class TestParserPasses(unittest.TestCase): +class TestParserPasses: directory = "tests/pass" def filename_test(self, filename): b = Blurbs() b.load(filename) - self.assertTrue(b) + assert b if os.path.exists(filename + ".res"): with open(filename + ".res", encoding="utf-8") as file: expected = file.read() - self.assertEqual(str(b), expected) + assert str(b) == expected def test_files(self): with pushd(self.directory): for filename in glob.glob("*"): - if filename[-4:] == ".res": - self.assertTrue(os.path.exists(filename[:-4]), filename) + if filename.endswith(".res"): + assert os.path.exists(filename[:-4]), filename continue self.filename_test(filename) @@ -31,5 +32,5 @@ class TestParserFailures(TestParserPasses): def filename_test(self, filename): b = Blurbs() - with self.assertRaises(Exception): + with pytest.raises(Exception): b.load(filename) From 60f10af6a0df9dd8d8b433536871caca00768e6e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 10 Nov 2024 11:41:57 +0200 Subject: [PATCH 17/29] Improve error wording Co-authored-by: Ezio Melotti --- src/blurb/blurb.py | 4 ++-- tests/test_blurb.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index bb09b71..25c3b87 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -486,10 +486,10 @@ def finish_entry(): try: int(value) except (TypeError, ValueError): - throw(f"Invalid {issue_keys[key]} issue number! ({value!r})") + throw(f"Invalid {issue_keys[key]} number: {value!r}") if key == "gh-issue" and int(value) < lowest_possible_gh_issue_number: - throw(f"The gh-issue number must be {lowest_possible_gh_issue_number} or above, not a PR number.") + throw(f"Invalid gh-issue number: {value!r} (must be >= {lowest_possible_gh_issue_number})") if key == "section": if no_changes: diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 3570e79..cd8f20a 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -217,15 +217,15 @@ def test_parse(): ), ( ".. gh-issue: 1\n.. section: IDLE\nHello world!", - r"The gh-issue number must be 32426 or above, not a PR number", + r"Invalid gh-issue number: '1' \(must be >= 32426\)", ), ( ".. bpo: one-two\n.. section: IDLE\nHello world!", - r"Invalid bpo issue number! \('one-two'\)", + r"Invalid bpo number: 'one-two'", ), ( ".. gh-issue: one-two\n.. section: IDLE\nHello world!", - r"Invalid GitHub issue number! \('one-two'\)", + r"Invalid GitHub number: 'one-two'", ), ( ".. gh-issue: 123456\n.. section: Funky Kong\nHello world!", From 5934d4fb4ec8b3a30c13c477555c08ab42054dcf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 10 Nov 2024 12:17:03 +0200 Subject: [PATCH 18/29] Replace safe_mkdir(path) with os.makedirs(path, exist_ok=True) --- src/blurb/blurb.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 25c3b87..f5a0f22 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -268,11 +268,6 @@ def __exit__(self, *args): os.chdir(self.previous_cwd) -def safe_mkdir(path): - if not os.path.exists(path): - os.makedirs(path) - - def version_key(element): fields = list(element.split(".")) if len(fields) == 1: @@ -560,7 +555,7 @@ def __str__(self): def save(self, path): dirname = os.path.dirname(path) - safe_mkdir(dirname) + os.makedirs(dirname, exist_ok=True) text = str(self) with open(path, "wt", encoding="utf-8") as file: @@ -1178,12 +1173,12 @@ def populate(): Creates and populates the Misc/NEWS.d directory tree. """ os.chdir("Misc") - safe_mkdir("NEWS.d/next") + os.makedirs("NEWS.d/next", exist_ok=True) for section in sections: dir_name = sanitize_section(section) dir_path = f"NEWS.d/next/{dir_name}" - safe_mkdir(dir_path) + os.makedirs(dir_path, exist_ok=True) readme_path = f"NEWS.d/next/{dir_name}/README.rst" with open(readme_path, "wt", encoding="utf-8") as readme: readme.write(f"Put news entry ``blurb`` files for the *{section}* section in this directory.\n") From db63d80e73f60fbd97be8668a51e2788c11ded69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:44:52 +0100 Subject: [PATCH 19/29] Bump codecov/codecov-action from 4 to 5 in the actions group (#39) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba5e9a9..c6eb6ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: uvx --with tox-uv tox -e py - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: flags: ${{ matrix.python-version }} name: Python ${{ matrix.python-version }} From 73bc163219be8461d7c6ad1ea2a0e3ebeb5c3922 Mon Sep 17 00:00:00 2001 From: Ezio Melotti Date: Mon, 6 Jan 2025 13:56:17 +0100 Subject: [PATCH 20/29] Remove `continue-on-error`. --- .github/workflows/test.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d4294f0..2802763 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,15 +11,10 @@ env: jobs: test: runs-on: ubuntu-latest - continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] - experimental: [false] - include: - - python-version: "3.14" - experimental: true + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 @@ -36,7 +31,6 @@ jobs: uses: hynek/setup-cached-uv@v2 - name: Tox tests - continue-on-error: ${{ matrix.experimental }} run: | uvx --with tox-uv tox -e py From 813fe8b8fdff1cce53b10792d98ee861e7b206f6 Mon Sep 17 00:00:00 2001 From: Ezio Melotti Date: Mon, 6 Jan 2025 14:09:58 +0100 Subject: [PATCH 21/29] Add 3.14 Trove classifier to pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 713dfb0..660ef86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dynamic = [ "version", From fa99f8e9f6e082fea45b240f6fa979598024395d Mon Sep 17 00:00:00 2001 From: Ezio Melotti Date: Mon, 6 Jan 2025 14:10:38 +0100 Subject: [PATCH 22/29] Add 3.14 to the env_list in tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fa69718..1631d62 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ requires = tox>=4.2 env_list = - py{313, 312, 311, 310, 39} + py{314, 313, 312, 311, 310, 39} [testenv] extras = From 165ae704fb394157b9269dd0be247a9f8c9e31d9 Mon Sep 17 00:00:00 2001 From: Ezio Melotti Date: Mon, 6 Jan 2025 14:17:42 +0100 Subject: [PATCH 23/29] Update max_supported_python to 3.14 in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 660ef86..29e9bac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,4 +50,4 @@ version-file = "src/blurb/_version.py" local_scheme = "no-local-version" [tool.pyproject-fmt] -max_supported_python = "3.13" +max_supported_python = "3.14" From 0311da4d4cf6d0e268d1a09e5fa12a22f3a8b904 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:16:13 +0200 Subject: [PATCH 24/29] Update changelog for 2.0.0 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3628254..9b810a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2.0.0 + +* Move 'blurb test' subcommand into test suite by @hugovk in https://github.com/python/blurb/pull/37 +* Add support for Python 3.14 by @ezio-melotti in https://github.com/python/blurb/pull/40 +* Validate gh-issue is int before checking range, and that gh-issue or bpo exists by @hugovk in https://github.com/python/blurb/pull/35 +* Replace `safe_mkdir(path)` with `os.makedirs(path, exist_ok=True)` by @hugovk in https://github.com/python/blurb/pull/38 +* Test version handling functions by @hugovk in https://github.com/python/blurb/pull/36 +* CI: Lint and test via uv by @hugovk in https://github.com/python/blurb/pull/32 + ## 1.3.0 * Add support for Python 3.13 by @hugovk in https://github.com/python/blurb/pull/26 From 156988c6d9366ef750e3416f23a3512b25497c49 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:50:03 +0200 Subject: [PATCH 25/29] Test textwrap_body, current_date and sortable_datetime --- pyproject.toml | 1 + tests/test_blurb.py | 61 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 29e9bac..d6f0669 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ optional-dependencies.tests = [ "pyfakefs", "pytest", "pytest-cov", + "time-machine", ] urls.Changelog = "https://github.com/python/blurb/blob/main/CHANGELOG.md" urls.Homepage = "https://github.com/python/blurb" diff --git a/tests/test_blurb.py b/tests/test_blurb.py index e9da901..8814ff1 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -1,4 +1,5 @@ import pytest +import time_machine from blurb import blurb @@ -44,6 +45,66 @@ def test_unsanitize_section_changed(section, expected): assert unsanitized == expected +@pytest.mark.parametrize( + "body, subsequent_indent, expected", + ( + ( + "This is a test of the textwrap_body function with a string. It should wrap the text to 79 characters.", + "", + ( + "This is a test of the textwrap_body function with a string. It should wrap\n" + "the text to 79 characters.\n" + ), + ), + ( + [ + "This is a test of the textwrap_body function", + "with an iterable of strings.", + "It should wrap the text to 79 characters.", + ], + "", + ( + "This is a test of the textwrap_body function with an iterable of strings. It\n" + "should wrap the text to 79 characters.\n" + ), + ), + ( + "This is a test of the textwrap_body function with a string and subsequent indent.", + " ", + ( + "This is a test of the textwrap_body function with a string and subsequent\n" + " indent.\n" + ), + ), + ( + "This is a test of the textwrap_body function with a bullet list and subsequent indent. The list should not be wrapped.\n" + "\n" + "* Item 1\n" + "* Item 2\n", + " ", + ( + "This is a test of the textwrap_body function with a bullet list and\n" + " subsequent indent. The list should not be wrapped.\n" + "\n" + " * Item 1\n" + " * Item 2\n" + ), + ), + ), +) +def test_textwrap_body(body, subsequent_indent, expected): + assert blurb.textwrap_body(body, subsequent_indent=subsequent_indent) == expected + + +@time_machine.travel("2025-01-07") +def test_current_date(): + assert blurb.current_date() == "2025-01-07" + + +@time_machine.travel("2025-01-07 16:28:41") +def test_sortable_datetime(): + assert blurb.sortable_datetime() == "2025-01-07-16-28-41" + @pytest.mark.parametrize( "version1, version2", ( From 7cb5abb6372c96ca463ed81e428c0a8702c807f3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:52:11 +0200 Subject: [PATCH 26/29] Remove unused variable --- src/blurb/blurb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 9950554..0c5e37a 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -139,7 +139,6 @@ def unsanitize_section(section): return _unsanitize_section.get(section, section) def next_filename_unsanitize_sections(filename): - s = filename for key, value in _unsanitize_section.items(): for separator in "/\\": key = f"{separator}{key}{separator}" From 1e6dd0f9d86ac20caa583d4de9dff8309e83146e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:53:27 +0200 Subject: [PATCH 27/29] Use 'not in' to test membership --- src/blurb/blurb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 0c5e37a..b3998b5 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -493,7 +493,7 @@ def finish_entry(): if "gh-issue" not in metadata and "bpo" not in metadata: throw("'gh-issue:' or 'bpo:' must be specified in the metadata!") - if not 'section' in metadata: + if 'section' not in metadata: throw("No 'section' specified. You must provide one!") self.append((metadata, text)) From ee43c4806c64369de8eb4147732ca82f8d351b6c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 7 Jan 2025 18:03:30 +0200 Subject: [PATCH 28/29] Remove extra parentheses --- tests/test_blurb.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 8814ff1..dc75497 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -51,10 +51,8 @@ def test_unsanitize_section_changed(section, expected): ( "This is a test of the textwrap_body function with a string. It should wrap the text to 79 characters.", "", - ( - "This is a test of the textwrap_body function with a string. It should wrap\n" - "the text to 79 characters.\n" - ), + "This is a test of the textwrap_body function with a string. It should wrap\n" + "the text to 79 characters.\n", ), ( [ @@ -63,18 +61,14 @@ def test_unsanitize_section_changed(section, expected): "It should wrap the text to 79 characters.", ], "", - ( - "This is a test of the textwrap_body function with an iterable of strings. It\n" - "should wrap the text to 79 characters.\n" - ), + "This is a test of the textwrap_body function with an iterable of strings. It\n" + "should wrap the text to 79 characters.\n", ), ( "This is a test of the textwrap_body function with a string and subsequent indent.", " ", - ( - "This is a test of the textwrap_body function with a string and subsequent\n" - " indent.\n" - ), + "This is a test of the textwrap_body function with a string and subsequent\n" + " indent.\n", ), ( "This is a test of the textwrap_body function with a bullet list and subsequent indent. The list should not be wrapped.\n" @@ -82,13 +76,11 @@ def test_unsanitize_section_changed(section, expected): "* Item 1\n" "* Item 2\n", " ", - ( - "This is a test of the textwrap_body function with a bullet list and\n" - " subsequent indent. The list should not be wrapped.\n" - "\n" - " * Item 1\n" - " * Item 2\n" - ), + "This is a test of the textwrap_body function with a bullet list and\n" + " subsequent indent. The list should not be wrapped.\n" + "\n" + " * Item 1\n" + " * Item 2\n", ), ), ) From 0f2c29299bdea1b637db9232ddda6b4ab88ec611 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:40:24 +0200 Subject: [PATCH 29/29] Add newline --- tests/test_blurb.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_blurb.py b/tests/test_blurb.py index dc75497..caf0f4e 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -97,6 +97,7 @@ def test_current_date(): def test_sortable_datetime(): assert blurb.sortable_datetime() == "2025-01-07-16-28-41" + @pytest.mark.parametrize( "version1, version2", (