From 7b3a7c6124d5036cdafad9e956867879eaf9159d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 00:35:23 +0000 Subject: [PATCH 01/49] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) - [github.com/tox-dev/pyproject-fmt: 1.7.0 → 2.1.3](https://github.com/tox-dev/pyproject-fmt/compare/1.7.0...2.1.3) - [github.com/abravalheri/validate-pyproject: v0.16 → v0.18](https://github.com/abravalheri/validate-pyproject/compare/v0.16...v0.18) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cf530b..c036731 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-case-conflict - id: check-merge-conflict @@ -11,13 +11,13 @@ repos: - id: trailing-whitespace - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.7.0 + rev: 2.1.3 hooks: - id: pyproject-fmt additional_dependencies: [tox] - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.16 + rev: v0.18 hooks: - id: validate-pyproject From b1b45b3ce9f218d5cfaf507fe79ab9ed5ce1978a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 00:35:54 +0000 Subject: [PATCH 02/49] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyproject.toml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 22371b6..6f7b722 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,12 @@ requires = [ name = "blurb" description = "Command-line tool to manage CPython Misc/NEWS.d entries." readme = "README.md" -maintainers = [{name = "Python Core Developers", email="core-workflow@mail.python.org"}] -authors = [{ name="Larry Hastings", email="larry@hastings.org"}] +maintainers = [ + { name = "Python Core Developers", email = "core-workflow@mail.python.org" }, +] +authors = [ + { name = "Larry Hastings", email = "larry@hastings.org" }, +] requires-python = ">=3.8" classifiers = [ "Intended Audience :: Developers", @@ -21,23 +25,19 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", ] dynamic = [ "version", ] -[project.optional-dependencies] -tests = [ +optional-dependencies.tests = [ "pyfakefs", "pytest", "pytest-cov", ] -[project.urls] -Changelog = "https://github.com/python/blurb/blob/main/README.md#changelog" -Homepage = "https://github.com/python/blurb" -Source = "https://github.com/python/blurb" -[project.scripts] -blurb = "blurb.blurb:main" +urls.Changelog = "https://github.com/python/blurb/blob/main/README.md#changelog" +urls.Homepage = "https://github.com/python/blurb" +urls.Source = "https://github.com/python/blurb" +scripts.blurb = "blurb.blurb:main" [tool.hatch] version.source = "vcs" From d81eda293c548312a57624608f9cf67e1ea236f6 Mon Sep 17 00:00:00 2001 From: Ezio Melotti Date: Tue, 2 Jul 2024 10:22:30 +0800 Subject: [PATCH 03/49] 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 04/49] 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 05/49] 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 06/49] 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 e1cf3429921cc1e6150ee32325cdc4f361168835 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 2 Jul 2024 08:52:10 +0300 Subject: [PATCH 07/49] Configure pyproject-fmt for 3.13 Trove classifier --- .pre-commit-config.yaml | 1 - pyproject.toml | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c036731..a8986c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,6 @@ repos: rev: 2.1.3 hooks: - id: pyproject-fmt - additional_dependencies: [tox] - repo: https://github.com/abravalheri/validate-pyproject rev: v0.18 diff --git a/pyproject.toml b/pyproject.toml index 6f7b722..35c7dd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dynamic = [ "version", @@ -44,3 +45,6 @@ version.source = "vcs" [tool.hatch.version.raw-options] local_scheme = "no-local-version" + +[tool.pyproject-fmt] +max_supported_python = "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 08/49] 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 4ca574902dd4245296d867270f554e72fbb36269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Mon, 8 Jul 2024 18:35:24 +0200 Subject: [PATCH 09/49] Undocument blurb split The command was removed in 1.2.0. --- README.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/README.md b/README.md index 74bf91b..9dd8ffe 100644 --- a/README.md +++ b/README.md @@ -191,32 +191,6 @@ uses the name of the directory CPython is checked out to. version I'm releasing, and using this shortcut saves me some typing.) -### blurb split - -`blurb split` only needs to be run once per-branch, ever. -It reads in `Misc/NEWS` -and splits it into individual `.rst` files. -The text files are stored as follows:: - - Misc/NEWS.d/.rst - -`` is the version number of Python where the -change was committed. Pre-release versions are denoted -with an abbreviation: `a` for alphas, `b` for betas, -and `rc` for release candidates. - -The individual `.rst` files actually (usually) -contain multiple entries. Each entry is delimited by a -single line containing `..` by itself. - -The assumption is, at the point we convert over to *blurb*, -we'll run `blurb split` on each active branch, -remove `Misc/NEWS` from the repo entirely, -never run `blurb split` ever again, -and ride off into the sunset, confident that the world is now -a better place. - - ## The "next" directory From 9baee80e01625fa7a297942c2031ea529cb76462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sviatoslav=20Sydorenko=20=28=D0=A1=D0=B2=D1=8F=D1=82=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1=D0=B8=D0=B4=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE=29?= Date: Wed, 17 Jul 2024 00:13:51 +0200 Subject: [PATCH 10/49] Fix `blurb` import in the runpy interface When `blurb` was being restructured, the import paths moved to deeper levels of the project layout. It was updated in the console scripts so invoking just the `blurb` command worked but `python -m blurb` didn't. This resulted in breaking the MSI installer smoke testing workflow in CPython's CI [[1]]. Blurb's own CI never invokes said interface, so it had no chance of catching the regression. This patch corrects the import but does not make an attempt to improve testing. [1]: https://github.com/python/cpython/issues/121879 --- src/blurb/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blurb/__main__.py b/src/blurb/__main__.py index 6aff72b..a173b94 100644 --- a/src/blurb/__main__.py +++ b/src/blurb/__main__.py @@ -1,5 +1,5 @@ -"""Run blurb using `python3 blurb/`.""" -import blurb +"""Run blurb using ``python3 -m blurb``.""" +from blurb import blurb if __name__ == '__main__': From 3529a9ea3b5d850f4aaa1acb4f72f6b76314a725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sviatoslav=20Sydorenko=20=28=D0=A1=D0=B2=D1=8F=D1=82=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1=D0=B8=D0=B4=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE=29?= Date: Wed, 17 Jul 2024 00:19:49 +0200 Subject: [PATCH 11/49] Integrate a runpy smoke test in tox This just adds simple `python -m blurb` invocations, nothing fancy. --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 29d8a8a..793f226 100644 --- a/tox.ini +++ b/tox.ini @@ -19,3 +19,5 @@ commands = {posargs} blurb test blurb help + {envpython} -I -m blurb test + {envpython} -I -m blurb help From 9b54fc9c086a6ab0e5e13b9de350f0db5511d837 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 17 Jul 2024 11:01:31 +0300 Subject: [PATCH 12/49] Move changelog to own file --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ README.md | 33 ++++----------------------------- pyproject.toml | 2 +- 3 files changed, 33 insertions(+), 30 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..364e743 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## 1.2.0 + +- Replace spaces with underscores in news directory. +- Drop support for Python 3.7. +- Remove `blurb split` command. +- Replace `gh-issue-NNNN:` with `gh-NNNN:` in the output. +- Accept GitHub issues numbered only 32426 or above. +- Improve error checking when parsing a Blurb. +- Loosen README check for CPython forks. +- Move code from `python/core-workflow` to own `python/blurb` repo. +- Deploy to PyPI via Trusted Publishers. + +## 1.1.0 + +- Support GitHub Issues in addition to b.p.o (bugs.python.org). + If "gh-issue" is in the metadata, then the filename will contain + "gh-issue-" instead of "bpo-". + +## 1.0.7 + +- When word wrapping, don't break on long words or hyphens. +- Use the `-f` flag when adding **blurb** files to a Git + commit. This forces them to be added, even when the files + might normally be ignored based on a `.gitignore` directive. +- Explicitly support the `-help` command-line option. +- Fix Travis CI integration. diff --git a/README.md b/README.md index 9dd8ffe..d477343 100644 --- a/README.md +++ b/README.md @@ -211,36 +211,11 @@ the right thing. If `NEWS` entries were already written to the final version directory, you'd have to move those around as part of the cherry-picking process. -## Changelog - -### 1.2.0 - -- Replace spaces with underscores in news directory. -- Drop support for Python 3.7. -- Remove `blurb split` command. -- Replace `gh-issue-NNNN:` with `gh-NNNN:` in the output. -- Accept GitHub issues numbered only 32426 or above. -- Improve error checking when parsing a Blurb. -- Loosen README check for CPython forks. -- Move code from `python/core-workflow` to own `python/blurb` repo. -- Deploy to PyPI via Trusted Publishers. - -### 1.1.0 - -- Support GitHub Issues in addition to b.p.o (bugs.python.org). - If "gh-issue" is in the metadata, then the filename will contain - "gh-issue-" instead of "bpo-". - -### 1.0.7 - -- When word wrapping, don't break on long words or hyphens. -- Use the `-f` flag when adding **blurb** files to a Git - commit. This forces them to be added, even when the files - might normally be ignored based on a `.gitignore` directive. -- Explicitly support the `-help` command-line option. -- Fix Travis CI integration. - ## Copyright **blurb** is Copyright 2015-2018 by Larry Hastings. Licensed to the PSF under a contributor agreement. + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md). diff --git a/pyproject.toml b/pyproject.toml index 35c7dd9..0843f61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ optional-dependencies.tests = [ "pytest", "pytest-cov", ] -urls.Changelog = "https://github.com/python/blurb/blob/main/README.md#changelog" +urls.Changelog = "https://github.com/python/blurb/blob/main/CHANGELOG.md" urls.Homepage = "https://github.com/python/blurb" urls.Source = "https://github.com/python/blurb" scripts.blurb = "blurb.blurb:main" From c79ec986e52f5d73952f0f2e55de9dce73b13470 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 17 Jul 2024 11:02:07 +0300 Subject: [PATCH 13/49] Prepare 1.2.1 release --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 364e743..f15c356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.2.1 + +- Fix `python3 -m blurb`. +- Undocument removed `blurb split`. + ## 1.2.0 - Replace spaces with underscores in news directory. From 025766dcce03bc743b8a4b96cfb7a08ca054f77f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:29:49 +0300 Subject: [PATCH 14/49] Improve formatting Co-authored-by: Ezio Melotti --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f15c356..4783afd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,8 @@ ## 1.1.0 - Support GitHub Issues in addition to b.p.o (bugs.python.org). - If "gh-issue" is in the metadata, then the filename will contain - "gh-issue-" instead of "bpo-". + If `gh-issue` is in the metadata, then the filename will contain + `gh-issue-` instead of `bpo-`. ## 1.0.7 From 984c4d21f8c7a74ce44c3c0b832859ac7de12e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Mon, 22 Jul 2024 10:58:55 +0200 Subject: [PATCH 15/49] Allow running blurb test from blurb-* directories Fixes https://github.com/python/blurb/issues/21 --- src/blurb/blurb.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 4930bb4..d54a950 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -642,7 +642,7 @@ def save_next(self): tests_run = 0 class TestParserPasses(unittest.TestCase): - directory = "blurb/tests/pass" + directory = "tests/pass" def filename_test(self, filename): b = Blurbs() @@ -667,7 +667,7 @@ def test_files(self): class TestParserFailures(TestParserPasses): - directory = "blurb/tests/fail" + directory = "tests/fail" def filename_test(self, filename): b = Blurbs() @@ -820,6 +820,15 @@ def help(subcommand=None): subcommands["--help"] = help +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 test(*args): """ @@ -828,12 +837,13 @@ def test(*args): # unittest.main doesn't work because this isn't a module # so we'll do it ourselves - while not os.path.isdir("blurb"): + 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) From 9cf6a3f2b9c42d9310b85cec0ec895ef228bfabf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 4 Aug 2024 21:37:57 +0300 Subject: [PATCH 16/49] Test Python 3.13 --- .github/workflows/test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c2d790..388811d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,9 +11,7 @@ jobs: strategy: fail-fast: false matrix: - # Temporarily remove 3.13 pending: - # https://github.com/pytest-dev/pyfakefs/issues/1017 - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 From 95bdb6be08f4e1a3f194f08b958428f88500c371 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:23:57 -0700 Subject: [PATCH 17/49] Drop support for Python 3.8 --- .github/workflows/test.yml | 2 +- README.md | 2 -- pyproject.toml | 3 +-- tox.ini | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 388811d..ff31dc6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index d477343..c5e6abc 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,6 @@ and automatically uses the correct file paths. You can install **blurb** from PyPI using `pip`. Alternatively, simply add `blurb` to a directory on your path. -**blurb**'s only dependency is Python 3.8+. - ## Files used by blurb diff --git a/pyproject.toml b/pyproject.toml index 0843f61..cad458f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,12 +15,11 @@ maintainers = [ authors = [ { name = "Larry Hastings", email = "larry@hastings.org" }, ] -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", diff --git a/tox.ini b/tox.ini index 793f226..4956937 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ requires = tox>=4.2 env_list = - py{313, 312, 311, 310, 39, 38} + py{313, 312, 311, 310, 39} [testenv] extras = From 6ed4c79ba187d8ee56b4d57594540fbc0bbab1da Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:41:36 -0700 Subject: [PATCH 18/49] Generate digital attestations for PyPI (PEP 740) --- .github/workflows/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index befd5af..4c6267e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,9 @@ on: permissions: contents: read +env: + FORCE_COLOR: 1 + jobs: # Always build & lint package. build-package: @@ -47,6 +50,7 @@ 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. @@ -78,3 +82,5 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + with: + attestations: true From 0615781b61af502e9e97e7eb0596702412b927a6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:44:25 -0700 Subject: [PATCH 19/49] Add more linting --- .pre-commit-config.yaml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a8986c4..c076739 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,26 +2,39 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: + - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-toml - id: check-yaml - id: debug-statements - id: end-of-file-fixer + - id: forbid-submodules - id: trailing-whitespace + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.29.2 + hooks: + - id: check-dependabot + - id: check-github-workflows + + - repo: https://github.com/rhysd/actionlint + rev: v1.7.2 + hooks: + - id: actionlint + - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.1.3 + rev: 2.2.4 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.18 + rev: v0.20.2 hooks: - id: validate-pyproject - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.3.1 + rev: 1.4.1 hooks: - id: tox-ini-fmt From 83199ea32f9d365be2ed812f263cb3a29499af81 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:27:23 -0700 Subject: [PATCH 20/49] Restrict permissions --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff31dc6..276a00b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,9 @@ name: Test on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + env: FORCE_COLOR: 1 From 97049c2b358dd57529a30ada92851e28886b1ab7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:52:45 +0300 Subject: [PATCH 21/49] Add version subcommand --- src/blurb/blurb.py | 13 ++++++++++++- tox.ini | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 4930bb4..27c3e3d 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -742,6 +742,15 @@ def get_subcommand(subcommand): +@subcommand +def version(): + """ +Print blurb version. + """ + print("blurb version", __version__) + + + @subcommand def help(subcommand=None): """ @@ -818,6 +827,8 @@ def help(subcommand=None): # Make "blurb --help" work. subcommands["--help"] = help +subcommands["--version"] = version +subcommands["-v"] = version @subcommand @@ -1205,7 +1216,7 @@ def main(): fn = get_subcommand(subcommand) # hack - if fn in (test, help): + if fn in (help, test, version): sys.exit(fn(*args)) try: diff --git a/tox.ini b/tox.ini index 4956937..fa69718 100644 --- a/tox.ini +++ b/tox.ini @@ -19,5 +19,7 @@ commands = {posargs} blurb test blurb help + blurb --version {envpython} -I -m blurb test {envpython} -I -m blurb help + {envpython} -I -m blurb version From a6aefbfd5e686eaeb405f7ff0c307f0dd9d2a509 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:05:45 +0300 Subject: [PATCH 22/49] Generate __version__ at build to avoid slow importlib.metadata import --- .gitignore | 3 +++ pyproject.toml | 3 +++ src/blurb/__init__.py | 4 +--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 009c104..353ced5 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ ENV/ # pytest .pytest_cache/ + +# hatch-vcs +src/*/_version.py diff --git a/pyproject.toml b/pyproject.toml index cad458f..713dfb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,9 @@ scripts.blurb = "blurb.blurb:main" [tool.hatch] version.source = "vcs" +[tool.hatch.build.hooks.vcs] +version-file = "src/blurb/_version.py" + [tool.hatch.version.raw-options] local_scheme = "no-local-version" diff --git a/src/blurb/__init__.py b/src/blurb/__init__.py index 63dfcac..8dee4bf 100644 --- a/src/blurb/__init__.py +++ b/src/blurb/__init__.py @@ -1,3 +1 @@ -import importlib.metadata - -__version__ = importlib.metadata.version(__name__) +from ._version import __version__ From 56bba411e210b94b5a485c5300856adfbe623fca Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:53:17 +0300 Subject: [PATCH 23/49] Apply suggestions from code review Co-authored-by: Ezio Melotti Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- src/blurb/blurb.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/blurb/blurb.py b/src/blurb/blurb.py index 27c3e3d..faefd10 100755 --- a/src/blurb/blurb.py +++ b/src/blurb/blurb.py @@ -744,9 +744,7 @@ def get_subcommand(subcommand): @subcommand def version(): - """ -Print blurb version. - """ + """Print blurb version.""" print("blurb version", __version__) @@ -825,10 +823,10 @@ def help(subcommand=None): print(doc) sys.exit(0) -# Make "blurb --help" work. +# Make "blurb --help/--version/-V" work. subcommands["--help"] = help subcommands["--version"] = version -subcommands["-v"] = version +subcommands["-V"] = version @subcommand From 1f876646a7cb6765475f30454fe6fc01ae5014db Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 11 Oct 2024 20:00:13 +0300 Subject: [PATCH 24/49] Test version() --- tests/test_blurb.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_blurb.py b/tests/test_blurb.py index 1c18a9f..9ff3a8d 100644 --- a/tests/test_blurb.py +++ b/tests/test_blurb.py @@ -179,3 +179,12 @@ def test_extract_next_filename(news_entry, expected_path, fs): # Assert assert path == expected_path + + +def test_version(capfd): + # Act + blurb.version() + + # Assert + captured = capfd.readouterr() + assert captured.out.startswith("blurb version ") From 350fb26767445ac0d3deb582838fa2f4d0186371 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 12 Oct 2024 18:33:17 +0300 Subject: [PATCH 25/49] Update changelog for 1.3.0 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4783afd..3628254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.3.0 + +* Add support for Python 3.13 by @hugovk in https://github.com/python/blurb/pull/26 +* Drop support for Python 3.8 by @hugovk in https://github.com/python/blurb/pull/27 +* Generate digital attestations for PyPI (PEP 740) by @hugovk in https://github.com/python/blurb/pull/28 +* Allow running blurb test from blurb-* directories by @hroncok in https://github.com/python/blurb/pull/24 +* Add `version` subcommand by @hugovk in https://github.com/python/blurb/pull/29 +* Generate `__version__` at build to avoid slow `importlib.metadata` import by @hugovk in https://github.com/python/blurb/pull/30 + ## 1.2.1 - Fix `python3 -m blurb`. 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 26/49] 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 27/49] 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 28/49] 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 29/49] 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 30/49] 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 31/49] 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 32/49] 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 33/49] 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 34/49] 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 35/49] 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 36/49] 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 37/49] 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 38/49] 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 39/49] 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 40/49] 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 41/49] 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 42/49] 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 43/49] 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 44/49] 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 45/49] 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 46/49] 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 47/49] 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 48/49] 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 49/49] 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", ( pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy