From 8f42595ca65133aeb4b75f38183233c27b2e6247 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 28 Feb 2025 00:19:07 +0100 Subject: [PATCH 01/68] Enable ruff rules ISC001/ISC002 (jaraco/skeleton#158) Starting with ruff 0.9.1, they are compatible with the ruff formatter when used together. --- ruff.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/ruff.toml b/ruff.toml index b52a6d7c..2b679267 100644 --- a/ruff.toml +++ b/ruff.toml @@ -43,8 +43,6 @@ ignore = [ "Q003", "COM812", "COM819", - "ISC001", - "ISC002", # local ] From b7d4b6ee00804bef36a8c398676e207813540c3b Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 4 Mar 2025 03:24:14 -0500 Subject: [PATCH 02/68] remove extra spaces in ruff.toml (jaraco/skeleton#164) --- ruff.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ruff.toml b/ruff.toml index 2b679267..1e952846 100644 --- a/ruff.toml +++ b/ruff.toml @@ -4,13 +4,13 @@ extend = "pyproject.toml" [lint] extend-select = [ # upstream - + "C901", # complex-structure "I", # isort "PERF401", # manual-list-comprehension "W", # pycodestyle Warning - - # Ensure modern type annotation syntax and best practices + + # Ensure modern type annotation syntax and best practices # Not including those covered by type-checkers or exclusive to Python 3.11+ "FA", # flake8-future-annotations "F404", # late-future-import @@ -26,7 +26,7 @@ extend-select = [ ] ignore = [ # upstream - + # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, # irrelevant to this project. "PYI011", # typed-argument-default-in-stub @@ -44,7 +44,7 @@ ignore = [ "COM812", "COM819", - # local + # local ] [format] From b00e9dd730423a399c1d3c3d5621687adff0c5a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Mar 2025 09:05:55 -0500 Subject: [PATCH 03/68] Remove pycodestyle warnings, no longer meaningful when using ruff formatter. Ref https://github.com/jaraco/skeleton/commit/d1c5444126aeacefee3949b30136446ab99979d8#commitcomment-153409678 --- ruff.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 1e952846..267a1ba1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -8,7 +8,6 @@ extend-select = [ "C901", # complex-structure "I", # isort "PERF401", # manual-list-comprehension - "W", # pycodestyle Warning # Ensure modern type annotation syntax and best practices # Not including those covered by type-checkers or exclusive to Python 3.11+ From d587ff737ee89778cf6f4bbd249e770c965fee06 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 7 Mar 2025 15:08:11 +0100 Subject: [PATCH 04/68] Update to the latest ruff version (jaraco/skeleton#166) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04870d16..633e3648 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.1 + rev: v0.9.9 hooks: - id: ruff args: [--fix, --unsafe-fixes] From ad84110008b826efd6e39bcc39b9998b4f1cc767 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 21 Mar 2025 00:14:38 +0000 Subject: [PATCH 05/68] Remove deprecated license classifier (PEP 639) (jaraco/skeleton#170) --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 328b98cb..71b1a7da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ readme = "README.rst" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] From 1ebb559a507f97ece7342d7f1532a49188cade33 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 Mar 2025 20:56:31 -0400 Subject: [PATCH 06/68] Remove workaround and update badge. Closes jaraco/skeleton#155 --- README.rst | 2 +- ruff.toml | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 4d3cabee..3000f5ab 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ :target: https://github.com/PROJECT_PATH/actions?query=workflow%3A%22tests%22 :alt: tests -.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff diff --git a/ruff.toml b/ruff.toml index 267a1ba1..63c0825f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,6 +1,3 @@ -# extend pyproject.toml for requires-python (workaround astral-sh/ruff#10299) -extend = "pyproject.toml" - [lint] extend-select = [ # upstream From 979e626055ab60095b37be04555a01a40f62e470 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Mar 2025 05:33:58 -0400 Subject: [PATCH 07/68] Remove PIP_NO_PYTHON_VERSION_WARNING. Ref pypa/pip#13154 --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5841cc37..928acf2c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,6 @@ env: # Suppress noisy pip warnings PIP_DISABLE_PIP_VERSION_CHECK: 'true' - PIP_NO_PYTHON_VERSION_WARNING: 'true' PIP_NO_WARN_SCRIPT_LOCATION: 'true' # Ensure tests can sense settings about the environment From 9a81db3c77bc106017dcd4b0853a5a94f43ae33c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 3 May 2025 03:57:47 -0400 Subject: [PATCH 08/68] Replace copy of license with an SPDX identifier. (jaraco/skeleton#171) --- LICENSE | 17 ----------------- pyproject.toml | 1 + 2 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1bb5a443..00000000 --- a/LICENSE +++ /dev/null @@ -1,17 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. diff --git a/pyproject.toml b/pyproject.toml index 71b1a7da..fa0c801f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", ] requires-python = ">=3.9" +license = "MIT" dependencies = [ ] dynamic = ["version"] From 867396152fcb99055795120750dfda53f85bb414 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sun, 4 May 2025 22:06:52 +0200 Subject: [PATCH 09/68] Python 3 is the default nowadays (jaraco/skeleton#173) --- .github/workflows/main.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 928acf2c..80294970 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,7 +63,7 @@ jobs: sudo apt update sudo apt install -y libxml2-dev libxslt-dev - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} allow-prereleases: true @@ -85,9 +85,7 @@ jobs: with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: 3.x + uses: actions/setup-python@v5 - name: Install tox run: python -m pip install tox - name: Eval ${{ matrix.job }} @@ -119,9 +117,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: 3.x + uses: actions/setup-python@v5 - name: Install tox run: python -m pip install tox - name: Run From d2b8d7750f78e870def98c4e04053af4acc86e29 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 10 May 2025 12:32:22 -0400 Subject: [PATCH 10/68] Add coherent.licensed plugin to inject license texts into the build. Closes jaraco/skeleton#174 --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fa0c801f..bda001a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,10 @@ [build-system] -requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.1"] +requires = [ + "setuptools>=61.2", + "setuptools_scm[toml]>=3.4.1", + # jaraco/skeleton#174 + "coherent.licensed", +] build-backend = "setuptools.build_meta" [project] From b535e75e95389eb8a16e34b238e2483f498593c8 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sat, 10 May 2025 18:47:43 +0200 Subject: [PATCH 11/68] Revert "Python 3 is the default nowadays (jaraco/skeleton#173)" (jaraco/skeleton#175) This reverts commit 867396152fcb99055795120750dfda53f85bb414. Removing `python-version` falls back on the Python bundled with the runner, making actions/setup-python a no-op. Here, the maintainer prefers using the latest release of Python 3. This is what `3.x` means: use the latest release of Python 3. --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 80294970..53513eee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -86,6 +86,8 @@ jobs: fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 + with: + python-version: 3.x - name: Install tox run: python -m pip install tox - name: Eval ${{ matrix.job }} @@ -118,6 +120,8 @@ jobs: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 + with: + python-version: 3.x - name: Install tox run: python -m pip install tox - name: Run From 5a6c1532c206871bc2913349d97dda06e01b9963 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 11 May 2025 23:20:37 -0400 Subject: [PATCH 12/68] Bump to setuptools 77 or later. Closes jaraco/skeleton#176 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bda001a4..ce6c1709 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] requires = [ - "setuptools>=61.2", + "setuptools>=77", "setuptools_scm[toml]>=3.4.1", # jaraco/skeleton#174 "coherent.licensed", From 04ff5549ee93f907bcebb1db570ad291ae55fd29 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 22 Jun 2025 13:49:02 +0100 Subject: [PATCH 13/68] Update pre-commit ruff (jaraco/skeleton#181) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 633e3648..fa559241 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.9 + rev: v0.12.0 hooks: - id: ruff args: [--fix, --unsafe-fixes] From 8c5810ed39f431598f8498499e7e8fa38a8ed455 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 22 Jun 2025 08:50:30 -0400 Subject: [PATCH 14/68] Log filenames when running pytest-mypy (jaraco/skeleton#177) --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ce6c1709..e916f46b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,12 +58,12 @@ cover = [ ] enabler = [ - "pytest-enabler >= 2.2", + "pytest-enabler >= 3.4", ] type = [ # upstream - "pytest-mypy", + "pytest-mypy >= 1.0.1", # local ] From 07349287790543c73ba8c38a6eb427ca9554f336 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:12:40 +0300 Subject: [PATCH 15/68] Remove redundant compatibility code --- pyproject.toml | 2 -- tests/fixtures.py | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 530f173f..2daf7922 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ requires-python = ">=3.9" license = "Apache-2.0" dependencies = [ "zipp>=3.20", - 'typing-extensions>=3.6.4; python_version < "3.8"', ] dynamic = ["version"] @@ -37,7 +36,6 @@ test = [ "pytest >= 6, != 8.1.*", # local - 'importlib_resources>=1.3; python_version < "3.9"', "packaging", "pyfakefs", "flufl.flake8", diff --git a/tests/fixtures.py b/tests/fixtures.py index 8e692f86..021eb811 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -6,17 +6,13 @@ import shutil import sys import textwrap +from importlib import resources from . import _path from ._path import FilesSpec from .compat.py39 import os_helper from .compat.py312 import import_helper -if sys.version_info >= (3, 9): - from importlib import resources -else: - import importlib_resources as resources - @contextlib.contextmanager def tmp_path(): From d47a969ed4567bbdee26034ccaaa8b8169f44fcf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 19 Oct 2025 13:06:02 -0400 Subject: [PATCH 16/68] Specify the directory for news fragments. Uses the default as found on towncrier prior to 25 and sets to a predictable value. Fixes jaraco/skeleton#184 --- towncrier.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/towncrier.toml b/towncrier.toml index 6fa480e4..577e87a7 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,2 +1,3 @@ [tool.towncrier] title_format = "{version}" +directory = "newsfragments" # jaraco/skeleton#184 From fc3f315445454c82ff1412770243430ac72fd316 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:33:30 +0200 Subject: [PATCH 17/68] Replace zipp dependency with stdlib --- importlib_metadata/__init__.py | 2 +- mypy.ini | 4 ---- pyproject.toml | 3 --- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index cdfc1f62..534330d4 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -821,7 +821,7 @@ def children(self): def zip_children(self): # deferred for performance (python/importlib_metadata#502) - from zipp.compat.overlay import zipfile + import zipfile zip_path = zipfile.Path(self.root) names = zip_path.root.namelist() diff --git a/mypy.ini b/mypy.ini index feac94cc..bfb6db30 100644 --- a/mypy.ini +++ b/mypy.ini @@ -18,10 +18,6 @@ disable_error_code = [mypy-pytest_perf.*] ignore_missing_imports = True -# jaraco/zipp#123 -[mypy-zipp.*] -ignore_missing_imports = True - # jaraco/jaraco.test#7 [mypy-jaraco.test.*] ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 2daf7922..9c949e83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,9 +22,6 @@ classifiers = [ ] requires-python = ">=3.9" license = "Apache-2.0" -dependencies = [ - "zipp>=3.20", -] dynamic = ["version"] [project.urls] From 372be3842f8e2d22ebd5968a115ac5cc0eeee604 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Dec 2025 12:06:11 -0500 Subject: [PATCH 18/68] Revert "Replace zipp dependency with stdlib" This reverts commit fc3f315445454c82ff1412770243430ac72fd316. --- importlib_metadata/__init__.py | 2 +- mypy.ini | 4 ++++ pyproject.toml | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 534330d4..cdfc1f62 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -821,7 +821,7 @@ def children(self): def zip_children(self): # deferred for performance (python/importlib_metadata#502) - import zipfile + from zipp.compat.overlay import zipfile zip_path = zipfile.Path(self.root) names = zip_path.root.namelist() diff --git a/mypy.ini b/mypy.ini index bfb6db30..feac94cc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -18,6 +18,10 @@ disable_error_code = [mypy-pytest_perf.*] ignore_missing_imports = True +# jaraco/zipp#123 +[mypy-zipp.*] +ignore_missing_imports = True + # jaraco/jaraco.test#7 [mypy-jaraco.test.*] ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 9c949e83..2daf7922 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,9 @@ classifiers = [ ] requires-python = ">=3.9" license = "Apache-2.0" +dependencies = [ + "zipp>=3.20", +] dynamic = ["version"] [project.urls] From 49427ed6129e350d9b5eff6dac94486c38c2b04a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Dec 2025 12:07:36 -0500 Subject: [PATCH 19/68] Add news fragment. --- newsfragments/524.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/524.bugfix.rst diff --git a/newsfragments/524.bugfix.rst b/newsfragments/524.bugfix.rst new file mode 100644 index 00000000..80527a0c --- /dev/null +++ b/newsfragments/524.bugfix.rst @@ -0,0 +1 @@ +Removed cruft from Python 3.8. From 40bb485b7fda162c503e2d70eb00a89321bd5fa3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 03:35:30 -0500 Subject: [PATCH 20/68] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/__init__.py | 3 ++- importlib_metadata/_adapters.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index cdfc1f62..03031190 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -636,7 +636,8 @@ def _read_files_egginfo_installed(self): return paths = ( - py311.relative_fix((subdir / name).resolve()) + py311 + .relative_fix((subdir / name).resolve()) .relative_to(self.locate_file('').resolve(), walk_up=True) .as_posix() for name in text.splitlines() diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index f5b30dd9..dede395d 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -9,7 +9,8 @@ class RawPolicy(email.policy.EmailPolicy): def fold(self, name, value): folded = self.linesep.join( - textwrap.indent(value, prefix=' ' * 8, predicate=lambda line: True) + textwrap + .indent(value, prefix=' ' * 8, predicate=lambda line: True) .lstrip() .splitlines() ) From 8f3d95e7db0114e26e57dd95932b141ead74f7c5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:04:12 -0500 Subject: [PATCH 21/68] Pin mypy on PyPy. Closes jaraco/skeleton#188. Ref python/mypy#20454. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e916f46b..987b802c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,9 @@ type = [ # upstream "pytest-mypy >= 1.0.1", + ## workaround for python/mypy#20454 + "mypy < 1.19; python_implementation == 'PyPy'", + # local ] From cbc721bfacd0ce396dba55235703525a8feaf0ac Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 8 Jun 2025 14:34:45 +0200 Subject: [PATCH 22/68] Fix errors with multiprocessing Before, one could get OSError 22 and BadZipFile errors due to re-used file pointers in forked subprocesses. Fixes #520 --- importlib_metadata/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 03031190..79f356a8 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -803,6 +803,7 @@ class FastPath: True """ + # The following cache is cleared at fork, see os.register_at_fork below @functools.lru_cache() # type: ignore[misc] def __new__(cls, root): return super().__new__(cls) @@ -843,6 +844,10 @@ def mtime(self): def lookup(self, mtime): return Lookup(self) +# Clear FastPath.__new__ cache when forked, avoids trying to re-useing open +# file pointers from zipp.Path/zipfile.Path objects in forked process +os.register_at_fork(after_in_child=FastPath.__new__.cache_clear) + class Lookup: """ From 339d7a57c7190d462c81ee12e60875c69d60f925 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Dec 2025 12:47:03 -0500 Subject: [PATCH 23/68] Added decorator to encapsulate the fork multiprocessing workaround. --- importlib_metadata/__init__.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 79f356a8..a8bf1c93 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -787,6 +787,24 @@ def find_distributions(self, context=Context()) -> Iterable[Distribution]: """ +def _clear_lru_cache_after_fork(func): + """Wrap ``func`` with ``functools.lru_cache`` and clear it after ``fork``. + + ``FastPath`` caches zip-backed ``pathlib.Path`` objects that keep a + reference to the parent's open ``ZipFile`` handle. Re-using a cached + instance in a forked child can therefore resurrect invalid file pointers + and trigger ``BadZipFile``/``OSError`` failures (python/importlib_metadata#520). + Registering ``cache_clear`` with ``os.register_at_fork`` ensures every + process gets a pristine cache and opens its own archive handles. + """ + + cached = functools.lru_cache()(func) + register = getattr(os, 'register_at_fork', None) + if register is not None: + register(after_in_child=cached.cache_clear) + return cached + + class FastPath: """ Micro-optimized class for searching a root for children. @@ -803,8 +821,7 @@ class FastPath: True """ - # The following cache is cleared at fork, see os.register_at_fork below - @functools.lru_cache() # type: ignore[misc] + @_clear_lru_cache_after_fork # type: ignore[misc] def __new__(cls, root): return super().__new__(cls) @@ -844,11 +861,6 @@ def mtime(self): def lookup(self, mtime): return Lookup(self) -# Clear FastPath.__new__ cache when forked, avoids trying to re-useing open -# file pointers from zipp.Path/zipfile.Path objects in forked process -os.register_at_fork(after_in_child=FastPath.__new__.cache_clear) - - class Lookup: """ A micro-optimized class for searching a (fast) path for metadata. From 104265b037f8994588992ebfbdd316cc78e3d457 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Dec 2025 13:19:44 -0500 Subject: [PATCH 24/68] Add test capturing missed expectation. --- tests/test_zip.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_zip.py b/tests/test_zip.py index d4f8e2f0..aeb91e79 100644 --- a/tests/test_zip.py +++ b/tests/test_zip.py @@ -1,7 +1,10 @@ +import multiprocessing +import os import sys import unittest from importlib_metadata import ( + FastPath, PackageNotFoundError, distribution, distributions, @@ -47,6 +50,37 @@ def test_one_distribution(self): dists = list(distributions(path=sys.path[:1])) assert len(dists) == 1 + @unittest.skipUnless( + hasattr(os, 'register_at_fork') + and 'fork' in multiprocessing.get_all_start_methods(), + 'requires fork-based multiprocessing support', + ) + def test_fastpath_cache_cleared_in_forked_child(self): + zip_path = sys.path[0] + + FastPath(zip_path) + self.assertEqual(FastPath.__new__.cache_info().currsize, 1) + + ctx = multiprocessing.get_context('fork') + parent_conn, child_conn = ctx.Pipe() + + def child(conn, root): + try: + before = FastPath.__new__.cache_info().currsize + FastPath(root) + after = FastPath.__new__.cache_info().currsize + conn.send((before, after)) + finally: + conn.close() + + proc = ctx.Process(target=child, args=(child_conn, zip_path)) + proc.start() + child_conn.close() + cache_sizes = parent_conn.recv() + proc.join() + + self.assertEqual(cache_sizes, (0, 1)) + class TestEgg(TestZip): def setUp(self): From 6a30ab96290b18c0b9805268a201ca5011c1feae Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 03:27:23 -0500 Subject: [PATCH 25/68] Allow initial currsize to be greater than one (as happens when running the test suite). --- tests/test_zip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_zip.py b/tests/test_zip.py index aeb91e79..165aa6dd 100644 --- a/tests/test_zip.py +++ b/tests/test_zip.py @@ -59,7 +59,7 @@ def test_fastpath_cache_cleared_in_forked_child(self): zip_path = sys.path[0] FastPath(zip_path) - self.assertEqual(FastPath.__new__.cache_info().currsize, 1) + assert FastPath.__new__.cache_info().currsize >= 1 ctx = multiprocessing.get_context('fork') parent_conn, child_conn = ctx.Pipe() From 4e962a8498990ba82120e7a58ce71abedefa0003 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 03:27:37 -0500 Subject: [PATCH 26/68] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index a8bf1c93..3e436d24 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -861,6 +861,7 @@ def mtime(self): def lookup(self, mtime): return Lookup(self) + class Lookup: """ A micro-optimized class for searching a (fast) path for metadata. From a1c25d8f2dc50abec65e4cf6d733b15d73c2f3b1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 03:29:36 -0500 Subject: [PATCH 27/68] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 3e436d24..68f9b5f9 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -821,7 +821,7 @@ class FastPath: True """ - @_clear_lru_cache_after_fork # type: ignore[misc] + @_clear_lru_cache_after_fork def __new__(cls, root): return super().__new__(cls) From 1da3f456ab53832fd6e1236f2338388d9ea0b0c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:09:52 -0500 Subject: [PATCH 28/68] Add news fragment. --- newsfragments/520.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/520.bugfix.rst diff --git a/newsfragments/520.bugfix.rst b/newsfragments/520.bugfix.rst new file mode 100644 index 00000000..1fbe7cec --- /dev/null +++ b/newsfragments/520.bugfix.rst @@ -0,0 +1 @@ +Fixed errors in FastPath under fork-multiprocessing. From 8dd2937cf852eb0d9ad96d4e45ed3470e80c1463 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:17:14 -0500 Subject: [PATCH 29/68] Decouple clear_after_fork from lru_cache and then compose. --- importlib_metadata/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 68f9b5f9..15ecb0b2 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -787,18 +787,17 @@ def find_distributions(self, context=Context()) -> Iterable[Distribution]: """ -def _clear_lru_cache_after_fork(func): - """Wrap ``func`` with ``functools.lru_cache`` and clear it after ``fork``. +def _clear_after_fork(cached): + """Ensure ``func`` clears cached state after ``fork`` when supported. - ``FastPath`` caches zip-backed ``pathlib.Path`` objects that keep a + ``FastPath`` caches zip-backed ``pathlib.Path`` objects that retain a reference to the parent's open ``ZipFile`` handle. Re-using a cached instance in a forked child can therefore resurrect invalid file pointers and trigger ``BadZipFile``/``OSError`` failures (python/importlib_metadata#520). - Registering ``cache_clear`` with ``os.register_at_fork`` ensures every - process gets a pristine cache and opens its own archive handles. + Registering ``cache_clear`` with ``os.register_at_fork`` keeps each process + on its own cache. """ - cached = functools.lru_cache()(func) register = getattr(os, 'register_at_fork', None) if register is not None: register(after_in_child=cached.cache_clear) @@ -821,7 +820,8 @@ class FastPath: True """ - @_clear_lru_cache_after_fork + @_clear_after_fork + @functools.lru_cache() def __new__(cls, root): return super().__new__(cls) From a36bab926643dcd67513851d5bebc285ef9ac681 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:22:17 -0500 Subject: [PATCH 30/68] Avoid if block. --- importlib_metadata/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 15ecb0b2..22824be8 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -797,10 +797,9 @@ def _clear_after_fork(cached): Registering ``cache_clear`` with ``os.register_at_fork`` keeps each process on its own cache. """ - - register = getattr(os, 'register_at_fork', None) - if register is not None: - register(after_in_child=cached.cache_clear) + getattr(os, 'register_at_fork', lambda **kw: None)( + after_in_child=cached.cache_clear, + ) return cached From 3c9510bf848fd4031e76028da0c9f60129047546 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:28:24 -0500 Subject: [PATCH 31/68] Prefer noop for degenerate behavior. --- importlib_metadata/__init__.py | 6 ++---- importlib_metadata/_functools.py | 9 +++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 22824be8..df9ff61a 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -35,7 +35,7 @@ NullFinder, install, ) -from ._functools import method_cache, pass_none +from ._functools import method_cache, noop, pass_none from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath from ._typing import md_none @@ -797,9 +797,7 @@ def _clear_after_fork(cached): Registering ``cache_clear`` with ``os.register_at_fork`` keeps each process on its own cache. """ - getattr(os, 'register_at_fork', lambda **kw: None)( - after_in_child=cached.cache_clear, - ) + getattr(os, 'register_at_fork', noop)(after_in_child=cached.cache_clear) return cached diff --git a/importlib_metadata/_functools.py b/importlib_metadata/_functools.py index 5dda6a21..8dcec720 100644 --- a/importlib_metadata/_functools.py +++ b/importlib_metadata/_functools.py @@ -102,3 +102,12 @@ def wrapper(param, *args, **kwargs): return func(param, *args, **kwargs) return wrapper + + +# From jaraco.functools 4.4 +def noop(*args, **kwargs): + """ + A no-operation function that does nothing. + + >>> noop(1, 2, three=3) + """ From f6eee5671a3e9e1cb56a6d3a6219145c19518713 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:48:18 -0500 Subject: [PATCH 32/68] Rely on passthrough to designate a wrapper for its side effect. --- importlib_metadata/__init__.py | 6 +++--- importlib_metadata/_functools.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index df9ff61a..508b02e4 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -35,7 +35,7 @@ NullFinder, install, ) -from ._functools import method_cache, noop, pass_none +from ._functools import method_cache, noop, pass_none, passthrough from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath from ._typing import md_none @@ -787,6 +787,7 @@ def find_distributions(self, context=Context()) -> Iterable[Distribution]: """ +@passthrough def _clear_after_fork(cached): """Ensure ``func`` clears cached state after ``fork`` when supported. @@ -798,7 +799,6 @@ def _clear_after_fork(cached): on its own cache. """ getattr(os, 'register_at_fork', noop)(after_in_child=cached.cache_clear) - return cached class FastPath: @@ -817,7 +817,7 @@ class FastPath: True """ - @_clear_after_fork + @_clear_after_fork # type: ignore[misc] @functools.lru_cache() def __new__(cls, root): return super().__new__(cls) diff --git a/importlib_metadata/_functools.py b/importlib_metadata/_functools.py index 8dcec720..b1fd04a8 100644 --- a/importlib_metadata/_functools.py +++ b/importlib_metadata/_functools.py @@ -1,5 +1,6 @@ import functools import types +from typing import Callable, TypeVar # from jaraco.functools 3.3 @@ -111,3 +112,24 @@ def noop(*args, **kwargs): >>> noop(1, 2, three=3) """ + + +_T = TypeVar('_T') + + +# From jaraco.functools 4.4 +def passthrough(func: Callable[..., object]) -> Callable[[_T], _T]: + """ + Wrap the function to always return the first parameter. + + >>> passthrough(print)('3') + 3 + '3' + """ + + @functools.wraps(func) + def wrapper(first: _T, *args, **kwargs) -> _T: + func(first, *args, **kwargs) + return first + + return wrapper # type: ignore[return-value] From 84e9028d39062af975d0659c0e987c28bcc808a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:54:12 -0500 Subject: [PATCH 33/68] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/520.bugfix.rst | 1 - newsfragments/524.bugfix.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/520.bugfix.rst delete mode 100644 newsfragments/524.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 4d0c4bdc..1a92cd19 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v8.7.1 +====== + +Bugfixes +-------- + +- Fixed errors in FastPath under fork-multiprocessing. (#520) +- Removed cruft from Python 3.8. (#524) + + v8.7.0 ====== diff --git a/newsfragments/520.bugfix.rst b/newsfragments/520.bugfix.rst deleted file mode 100644 index 1fbe7cec..00000000 --- a/newsfragments/520.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed errors in FastPath under fork-multiprocessing. diff --git a/newsfragments/524.bugfix.rst b/newsfragments/524.bugfix.rst deleted file mode 100644 index 80527a0c..00000000 --- a/newsfragments/524.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Removed cruft from Python 3.8. From d8a7576dedb16de480e1d8798d2a02771f8eb844 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 29 Dec 2025 11:40:24 -0500 Subject: [PATCH 34/68] Remove dependency on flufl.flake8 Closes #527 --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a367f162..b71b9a9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ test = [ # local "packaging", "pyfakefs", - "flufl.flake8", "pytest-perf >= 0.9.2", "jaraco.test >= 5.4", ] From 6702b62cca03e463a51eacdf487615cbc71d016e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:15:04 +0200 Subject: [PATCH 35/68] Drop support for EOL Python 3.9 --- .github/workflows/main.yml | 4 +- importlib_metadata/__init__.py | 6 +-- importlib_metadata/_functools.py | 3 +- importlib_metadata/compat/py39.py | 42 ------------------ pyproject.toml | 2 +- tests/compat/py312.py | 6 ++- tests/compat/py39.py | 8 ---- tests/compat/test_py39_compat.py | 74 ------------------------------- tests/fixtures.py | 7 ++- tests/test_main.py | 2 +- 10 files changed, 19 insertions(+), 135 deletions(-) delete mode 100644 importlib_metadata/compat/py39.py delete mode 100644 tests/compat/py39.py delete mode 100644 tests/compat/test_py39_compat.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 53513eee..2a7899c0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,15 +34,13 @@ jobs: # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - - "3.9" + - "3.10" - "3.13" platform: - ubuntu-latest - macos-latest - windows-latest include: - - python: "3.10" - platform: ubuntu-latest - python: "3.11" platform: ubuntu-latest - python: "3.12" diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 508b02e4..334a0916 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -39,7 +39,7 @@ from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath from ._typing import md_none -from .compat import py39, py311 +from .compat import py311 __all__ = [ 'Distribution', @@ -340,7 +340,7 @@ def select(self, **params) -> EntryPoints: Select entry points from self that match the given parameters (typically group and/or name). """ - return EntryPoints(ep for ep in self if py39.ep_matches(ep, **params)) + return EntryPoints(ep for ep in self if ep.matches(**params)) @property def names(self) -> set[str]: @@ -1088,7 +1088,7 @@ def version(distribution_name: str) -> str: _unique = functools.partial( unique_everseen, - key=py39.normalized_name, + key=operator.attrgetter('_normalized_name'), ) """ Wrapper for ``distributions`` to return unique distributions by name. diff --git a/importlib_metadata/_functools.py b/importlib_metadata/_functools.py index b1fd04a8..c159b46e 100644 --- a/importlib_metadata/_functools.py +++ b/importlib_metadata/_functools.py @@ -1,6 +1,7 @@ import functools import types -from typing import Callable, TypeVar +from collections.abc import Callable +from typing import TypeVar # from jaraco.functools 3.3 diff --git a/importlib_metadata/compat/py39.py b/importlib_metadata/compat/py39.py deleted file mode 100644 index 3eb9c01e..00000000 --- a/importlib_metadata/compat/py39.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Compatibility layer with Python 3.8/3.9 -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: # pragma: no cover - # Prevent circular imports on runtime. - from .. import Distribution, EntryPoint -else: - Distribution = EntryPoint = Any - -from .._typing import md_none - - -def normalized_name(dist: Distribution) -> str | None: - """ - Honor name normalization for distributions that don't provide ``_normalized_name``. - """ - try: - return dist._normalized_name - except AttributeError: - from .. import Prepared # -> delay to prevent circular imports. - - return Prepared.normalize( - getattr(dist, "name", None) or md_none(dist.metadata)['Name'] - ) - - -def ep_matches(ep: EntryPoint, **params) -> bool: - """ - Workaround for ``EntryPoint`` objects without the ``matches`` method. - """ - try: - return ep.matches(**params) - except AttributeError: - from .. import EntryPoint # -> delay to prevent circular imports. - - # Reconstruct the EntryPoint object to make sure it is compatible. - return EntryPoint(ep.name, ep.value, ep.group).matches(**params) diff --git a/pyproject.toml b/pyproject.toml index b71b9a9b..1e83bde9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] -requires-python = ">=3.9" +requires-python = ">=3.10" license = "Apache-2.0" dependencies = [ "zipp>=3.20", diff --git a/tests/compat/py312.py b/tests/compat/py312.py index ea9a58ba..c246641d 100644 --- a/tests/compat/py312.py +++ b/tests/compat/py312.py @@ -1,6 +1,10 @@ import contextlib -from .py39 import import_helper +from jaraco.test.cpython import from_test_support, try_import + +import_helper = try_import('import_helper') or from_test_support( + 'modules_setup', 'modules_cleanup' +) @contextlib.contextmanager diff --git a/tests/compat/py39.py b/tests/compat/py39.py deleted file mode 100644 index 4e45d7cc..00000000 --- a/tests/compat/py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from jaraco.test.cpython import from_test_support, try_import - -os_helper = try_import('os_helper') or from_test_support( - 'FS_NONASCII', 'skip_unless_symlink', 'temp_dir' -) -import_helper = try_import('import_helper') or from_test_support( - 'modules_setup', 'modules_cleanup' -) diff --git a/tests/compat/test_py39_compat.py b/tests/compat/test_py39_compat.py deleted file mode 100644 index db9fb1b7..00000000 --- a/tests/compat/test_py39_compat.py +++ /dev/null @@ -1,74 +0,0 @@ -import pathlib -import sys -import unittest - -from importlib_metadata import ( - distribution, - distributions, - entry_points, - metadata, - version, -) - -from .. import fixtures - - -class OldStdlibFinderTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): - def setUp(self): - if sys.version_info >= (3, 10): - self.skipTest("Tests specific for Python 3.8/3.9") - super().setUp() - - def _meta_path_finder(self): - from importlib.metadata import ( - Distribution, - DistributionFinder, - PathDistribution, - ) - from importlib.util import spec_from_file_location - - path = pathlib.Path(self.site_dir) - - class CustomDistribution(Distribution): - def __init__(self, name, path): - self.name = name - self._path_distribution = PathDistribution(path) - - def read_text(self, filename): - return self._path_distribution.read_text(filename) - - def locate_file(self, path): - return self._path_distribution.locate_file(path) - - class CustomFinder: - @classmethod - def find_spec(cls, fullname, _path=None, _target=None): - candidate = pathlib.Path(path, *fullname.split(".")).with_suffix(".py") - if candidate.exists(): - return spec_from_file_location(fullname, candidate) - - @classmethod - def find_distributions(self, context=DistributionFinder.Context()): - for dist_info in path.glob("*.dist-info"): - yield PathDistribution(dist_info) - name, _, _ = str(dist_info).partition("-") - yield CustomDistribution(name + "_custom", dist_info) - - return CustomFinder - - def test_compatibility_with_old_stdlib_path_distribution(self): - """ - Given a custom finder that uses Python 3.8/3.9 importlib.metadata is installed, - when importlib_metadata functions are called, there should be no exceptions. - Ref python/importlib_metadata#396. - """ - self.fixtures.enter_context(fixtures.install_finder(self._meta_path_finder())) - - assert list(distributions()) - assert distribution("distinfo_pkg") - assert distribution("distinfo_pkg_custom") - assert version("distinfo_pkg") > "0" - assert version("distinfo_pkg_custom") > "0" - assert list(metadata("distinfo_pkg")) - assert list(metadata("distinfo_pkg_custom")) - assert list(entry_points(group="entries")) diff --git a/tests/fixtures.py b/tests/fixtures.py index 021eb811..bf4f8c40 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -8,11 +8,16 @@ import textwrap from importlib import resources +from jaraco.test.cpython import from_test_support, try_import + from . import _path from ._path import FilesSpec -from .compat.py39 import os_helper from .compat.py312 import import_helper +os_helper = try_import('os_helper') or from_test_support( + 'FS_NONASCII', 'skip_unless_symlink', 'temp_dir' +) + @contextlib.contextmanager def tmp_path(): diff --git a/tests/test_main.py b/tests/test_main.py index 5ed08c89..f4ae69a7 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -20,7 +20,7 @@ from . import fixtures from ._path import Symlink -from .compat.py39 import os_helper +from .fixtures import os_helper class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): From ede3fcca524bdb335bfad673e169fa9ceab8405f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 7 Mar 2026 10:19:25 -0500 Subject: [PATCH 36/68] Raise MetadataNotFound when no metadata file was found. Ref python/cpython#143387 --- NEWS.rst | 11 +++++++++ importlib_metadata/__init__.py | 37 +++++++++++++++++++++++-------- importlib_metadata/_typing.py | 15 ------------- importlib_metadata/compat/py39.py | 6 +---- newsfragments/+.removal.rst | 1 + tests/test_main.py | 11 +++++---- 6 files changed, 48 insertions(+), 33 deletions(-) delete mode 100644 importlib_metadata/_typing.py create mode 100644 newsfragments/+.removal.rst diff --git a/NEWS.rst b/NEWS.rst index 1a92cd19..83944755 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,14 @@ +v8.8.0 +====== + +Features +-------- + +- Added ``MetadataNotFound`` (subclass of ``FileNotFoundError``) and updated + ``Distribution.metadata``/``metadata()`` to raise it when the metadata files + are missing instead of returning ``None`` (python/cpython#143387). + + v8.7.1 ====== diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 508b02e4..b321b307 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -38,7 +38,6 @@ from ._functools import method_cache, noop, pass_none, passthrough from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath -from ._typing import md_none from .compat import py39, py311 __all__ = [ @@ -46,6 +45,7 @@ 'DistributionFinder', 'PackageMetadata', 'PackageNotFoundError', + 'MetadataNotFound', 'SimplePath', 'distribution', 'distributions', @@ -70,6 +70,10 @@ def name(self) -> str: # type: ignore[override] # make readonly return name +class MetadataNotFound(FileNotFoundError): + """No metadata file is present in the distribution.""" + + class Sectioned: """ A simple entry point config parser for performance @@ -491,7 +495,14 @@ def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]: Ref python/importlib_resources#489. """ - buckets = bucket(dists, lambda dist: bool(dist.metadata)) + + def has_metadata(dist: Distribution) -> bool: + with suppress(MetadataNotFound): + dist.metadata + return True + return False + + buckets = bucket(dists, has_metadata) return itertools.chain(buckets[True], buckets[False]) @staticmethod @@ -512,7 +523,7 @@ def _discover_resolvers(): return filter(None, declared) @property - def metadata(self) -> _meta.PackageMetadata | None: + def metadata(self) -> _meta.PackageMetadata: """Return the parsed metadata for this Distribution. The returned object will have keys that name the various bits of @@ -521,6 +532,8 @@ def metadata(self) -> _meta.PackageMetadata | None: Custom providers may provide the METADATA file or override this property. + + :raises MetadataNotFound: If no metadata file is present. """ text = ( @@ -531,20 +544,25 @@ def metadata(self) -> _meta.PackageMetadata | None: # (which points to the egg-info file) attribute unchanged. or self.read_text('') ) - return self._assemble_message(text) + return self._assemble_message(self._ensure_metadata_present(text)) @staticmethod - @pass_none def _assemble_message(text: str) -> _meta.PackageMetadata: # deferred for performance (python/cpython#109829) from . import _adapters return _adapters.Message(email.message_from_string(text)) + def _ensure_metadata_present(self, text: str | None) -> str: + if text is not None: + return text + + raise MetadataNotFound('No package metadata was found.') + @property def name(self) -> str: """Return the 'Name' metadata for the distribution package.""" - return md_none(self.metadata)['Name'] + return self.metadata['Name'] @property def _normalized_name(self): @@ -554,7 +572,7 @@ def _normalized_name(self): @property def version(self) -> str: """Return the 'Version' metadata for the distribution package.""" - return md_none(self.metadata)['Version'] + return self.metadata['Version'] @property def entry_points(self) -> EntryPoints: @@ -1067,11 +1085,12 @@ def distributions(**kwargs) -> Iterable[Distribution]: return Distribution.discover(**kwargs) -def metadata(distribution_name: str) -> _meta.PackageMetadata | None: +def metadata(distribution_name: str) -> _meta.PackageMetadata: """Get the metadata for the named package. :param distribution_name: The name of the distribution package to query. :return: A PackageMetadata containing the parsed metadata. + :raises MetadataNotFound: If no metadata file is present in the distribution. """ return Distribution.from_name(distribution_name).metadata @@ -1142,7 +1161,7 @@ def packages_distributions() -> Mapping[str, list[str]]: pkg_to_dist = collections.defaultdict(list) for dist in distributions(): for pkg in _top_level_declared(dist) or _top_level_inferred(dist): - pkg_to_dist[pkg].append(md_none(dist.metadata)['Name']) + pkg_to_dist[pkg].append(dist.metadata['Name']) return dict(pkg_to_dist) diff --git a/importlib_metadata/_typing.py b/importlib_metadata/_typing.py deleted file mode 100644 index 32b1d2b9..00000000 --- a/importlib_metadata/_typing.py +++ /dev/null @@ -1,15 +0,0 @@ -import functools -import typing - -from ._meta import PackageMetadata - -md_none = functools.partial(typing.cast, PackageMetadata) -""" -Suppress type errors for optional metadata. - -Although Distribution.metadata can return None when metadata is corrupt -and thus None, allow callers to assume it's not None and crash if -that's the case. - -# python/importlib_metadata#493 -""" diff --git a/importlib_metadata/compat/py39.py b/importlib_metadata/compat/py39.py index 3eb9c01e..2592436d 100644 --- a/importlib_metadata/compat/py39.py +++ b/importlib_metadata/compat/py39.py @@ -12,8 +12,6 @@ else: Distribution = EntryPoint = Any -from .._typing import md_none - def normalized_name(dist: Distribution) -> str | None: """ @@ -24,9 +22,7 @@ def normalized_name(dist: Distribution) -> str | None: except AttributeError: from .. import Prepared # -> delay to prevent circular imports. - return Prepared.normalize( - getattr(dist, "name", None) or md_none(dist.metadata)['Name'] - ) + return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name']) def ep_matches(ep: EntryPoint, **params) -> bool: diff --git a/newsfragments/+.removal.rst b/newsfragments/+.removal.rst new file mode 100644 index 00000000..a7dfb18d --- /dev/null +++ b/newsfragments/+.removal.rst @@ -0,0 +1 @@ +- Removed the internal ``md_none`` typing helper since ``Distribution.metadata`` now always returns ``PackageMetadata`` and raises ``MetadataNotFound`` when absent (python/cpython#143387). diff --git a/tests/test_main.py b/tests/test_main.py index 5ed08c89..92084df1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -9,6 +9,7 @@ from importlib_metadata import ( Distribution, EntryPoint, + MetadataNotFound, PackageNotFoundError, _unique, distributions, @@ -157,13 +158,15 @@ def test_valid_dists_preferred(self): def test_missing_metadata(self): """ - Dists with a missing metadata file should return None. + Dists with a missing metadata file should raise ``MetadataNotFound``. - Ref python/importlib_metadata#493. + Ref python/importlib_metadata#493 and python/cpython#143387. """ fixtures.build_files(self.make_pkg('foo-4.3', files={}), self.site_dir) - assert Distribution.from_name('foo').metadata is None - assert metadata('foo') is None + with self.assertRaises(MetadataNotFound): + Distribution.from_name('foo').metadata + with self.assertRaises(MetadataNotFound): + metadata('foo') class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): From 18a676486f8679438a6b16992177dee66f61bcaa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 7 Mar 2026 10:49:25 -0500 Subject: [PATCH 37/68] Re-use ExceptionTrap for trapping exceptions. --- importlib_metadata/__init__.py | 9 ++- importlib_metadata/_context.py | 118 +++++++++++++++++++++++++++++++++ newsfragments/+.removal.rst | 1 + 3 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 importlib_metadata/_context.py diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index b321b307..4e945775 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -35,6 +35,7 @@ NullFinder, install, ) +from ._context import ExceptionTrap from ._functools import method_cache, noop, pass_none, passthrough from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath @@ -496,11 +497,9 @@ def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]: Ref python/importlib_resources#489. """ - def has_metadata(dist: Distribution) -> bool: - with suppress(MetadataNotFound): - dist.metadata - return True - return False + has_metadata = ExceptionTrap(MetadataNotFound).passes( + operator.attrgetter('metadata') + ) buckets = bucket(dists, has_metadata) return itertools.chain(buckets[True], buckets[False]) diff --git a/importlib_metadata/_context.py b/importlib_metadata/_context.py new file mode 100644 index 00000000..2635b164 --- /dev/null +++ b/importlib_metadata/_context.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +import functools +import operator + + +# from jaraco.context 6.1 +class ExceptionTrap: + """ + A context manager that will catch certain exceptions and provide an + indication they occurred. + + >>> with ExceptionTrap() as trap: + ... raise Exception() + >>> bool(trap) + True + + >>> with ExceptionTrap() as trap: + ... pass + >>> bool(trap) + False + + >>> with ExceptionTrap(ValueError) as trap: + ... raise ValueError("1 + 1 is not 3") + >>> bool(trap) + True + >>> trap.value + ValueError('1 + 1 is not 3') + >>> trap.tb + + + >>> with ExceptionTrap(ValueError) as trap: + ... raise Exception() + Traceback (most recent call last): + ... + Exception + + >>> bool(trap) + False + """ + + exc_info = None, None, None + + def __init__(self, exceptions=(Exception,)): + self.exceptions = exceptions + + def __enter__(self): + return self + + @property + def type(self): + return self.exc_info[0] + + @property + def value(self): + return self.exc_info[1] + + @property + def tb(self): + return self.exc_info[2] + + def __exit__(self, *exc_info): + type = exc_info[0] + matches = type and issubclass(type, self.exceptions) + if matches: + self.exc_info = exc_info + return matches + + def __bool__(self): + return bool(self.type) + + def raises(self, func, *, _test=bool): + """ + Wrap func and replace the result with the truth + value of the trap (True if an exception occurred). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> raises = ExceptionTrap(ValueError).raises + + Now decorate a function that always fails. + + >>> @raises + ... def fail(): + ... raise ValueError('failed') + >>> fail() + True + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + with ExceptionTrap(self.exceptions) as trap: + func(*args, **kwargs) + return _test(trap) + + return wrapper + + def passes(self, func): + """ + Wrap func and replace the result with the truth + value of the trap (True if no exception). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> passes = ExceptionTrap(ValueError).passes + + Now decorate a function that always fails. + + >>> @passes + ... def fail(): + ... raise ValueError('failed') + + >>> fail() + False + """ + return self.raises(func, _test=operator.not_) diff --git a/newsfragments/+.removal.rst b/newsfragments/+.removal.rst index a7dfb18d..cbbb2158 100644 --- a/newsfragments/+.removal.rst +++ b/newsfragments/+.removal.rst @@ -1 +1,2 @@ - Removed the internal ``md_none`` typing helper since ``Distribution.metadata`` now always returns ``PackageMetadata`` and raises ``MetadataNotFound`` when absent (python/cpython#143387). +- Vendored ``ExceptionTrap`` from ``jaraco.context`` (as ``_context``) and now rely on its ``passes`` helper when checking for missing metadata, keeping behavior aligned without adding dependencies. From d5c6862e8d8291aec83aeea8261191e491a63d68 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:48:11 +0200 Subject: [PATCH 38/68] Update pre-commit ruff legacy alias (jaraco/skeleton#183) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa559241..54cc8303 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,6 +2,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.12.0 hooks: - - id: ruff + - id: ruff-check args: [--fix, --unsafe-fixes] - id: ruff-format From d9b029be3925b99d3b0d2ef529d79d0a1b9d2c52 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 13 Mar 2026 10:56:44 -0400 Subject: [PATCH 39/68] Don't install (nor run) mypy on PyPy (librt build failures) (jaraco/skeleton#187) --------- Co-authored-by: Jason R. Coombs --- pyproject.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 987b802c..cdf82cfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,10 +63,9 @@ enabler = [ type = [ # upstream - "pytest-mypy >= 1.0.1", - - ## workaround for python/mypy#20454 - "mypy < 1.19; python_implementation == 'PyPy'", + + # Exclude PyPy from type checks (python/mypy#20454 jaraco/skeleton#187) + "pytest-mypy >= 1.0.1; platform_python_implementation != 'PyPy'", # local ] From 16fb289d38af0d510e39afcbbd43bace2d6d8dd9 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 13 Mar 2026 10:59:27 -0400 Subject: [PATCH 40/68] Bump `pytest-checkdocs` to `>= 2.14` to resolve deprecation warnings (jaraco/skeleton#189) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cdf82cfb..5b2a8a82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ doc = [ ] check = [ - "pytest-checkdocs >= 2.4", + "pytest-checkdocs >= 2.14", "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", ] From 07389c4c4609a49826ea9ed510419c2e32eccee9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:18:41 -0400 Subject: [PATCH 41/68] Bump Python versions: drop 3.9 (EOL), add 3.15 (jaraco/skeleton#193) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jaraco <308610+jaraco@users.noreply.github.com> --- .github/workflows/main.yml | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 53513eee..d40c74ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,31 +34,31 @@ jobs: # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - - "3.9" + - "3.10" - "3.13" platform: - ubuntu-latest - macos-latest - windows-latest include: - - python: "3.10" - platform: ubuntu-latest - python: "3.11" platform: ubuntu-latest - python: "3.12" platform: ubuntu-latest - python: "3.14" platform: ubuntu-latest + - python: "3.15" + platform: ubuntu-latest - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.14' }} + continue-on-error: ${{ matrix.python == '3.15' }} steps: - uses: actions/checkout@v4 - name: Install build dependencies # Install dependencies for building packages on pre-release Pythons # jaraco/skeleton#161 - if: matrix.python == '3.14' && matrix.platform == 'ubuntu-latest' + if: matrix.python == '3.15' && matrix.platform == 'ubuntu-latest' run: | sudo apt update sudo apt install -y libxml2-dev libxslt-dev diff --git a/pyproject.toml b/pyproject.toml index 5b2a8a82..a25e78ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] -requires-python = ">=3.9" +requires-python = ">=3.10" license = "MIT" dependencies = [ ] From 606a7a5f999e6a43480015460be604a77f16ce68 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:20:06 +0200 Subject: [PATCH 42/68] Fix CI warning in diffcov report (jaraco/skeleton#194) UserWarning: The --html-report option is deprecated. Use --format html:diffcov.html instead. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 14243051..e05a3d4a 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ deps = diff-cover commands = pytest {posargs} --cov-report xml - diff-cover coverage.xml --compare-branch=origin/main --html-report diffcov.html + diff-cover coverage.xml --compare-branch=origin/main --format html:diffcov.html diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 [testenv:docs] From 4b01b306a89ebcbd40d2fe782a5ef6bdb0534737 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 08:45:22 -0400 Subject: [PATCH 43/68] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/_functools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/_functools.py b/importlib_metadata/_functools.py index b1fd04a8..c159b46e 100644 --- a/importlib_metadata/_functools.py +++ b/importlib_metadata/_functools.py @@ -1,6 +1,7 @@ import functools import types -from typing import Callable, TypeVar +from collections.abc import Callable +from typing import TypeVar # from jaraco.functools 3.3 From 16dcf12e6a14d1b4087d0d7ec350dfefbf717264 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 08:55:04 -0400 Subject: [PATCH 44/68] Import import_helper directly --- tests/compat/py312.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/compat/py312.py b/tests/compat/py312.py index c246641d..904446b1 100644 --- a/tests/compat/py312.py +++ b/tests/compat/py312.py @@ -1,10 +1,6 @@ import contextlib -from jaraco.test.cpython import from_test_support, try_import - -import_helper = try_import('import_helper') or from_test_support( - 'modules_setup', 'modules_cleanup' -) +from test.support import import_helper @contextlib.contextmanager From 996a0ce99b9ea2c2cec47aac5a1d819b341f3ad5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 08:58:12 -0400 Subject: [PATCH 45/68] Fix issue with missing type stubs for test.support. --- tests/compat/py312.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/compat/py312.py b/tests/compat/py312.py index 904446b1..ef2f0495 100644 --- a/tests/compat/py312.py +++ b/tests/compat/py312.py @@ -1,6 +1,6 @@ import contextlib -from test.support import import_helper +from test.support import import_helper # type: ignore[import-untyped] @contextlib.contextmanager From 7a1444af94bf6f881c91572cbfa2c3e36e30b7e1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 09:00:11 -0400 Subject: [PATCH 46/68] Import os_helper directly. --- tests/fixtures.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index bf4f8c40..c2211739 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -8,16 +8,12 @@ import textwrap from importlib import resources -from jaraco.test.cpython import from_test_support, try_import +from test.support import os_helper # type: ignore[import-untyped] from . import _path from ._path import FilesSpec from .compat.py312 import import_helper -os_helper = try_import('os_helper') or from_test_support( - 'FS_NONASCII', 'skip_unless_symlink', 'temp_dir' -) - @contextlib.contextmanager def tmp_path(): From d25e5614bb6f0311e7835cc5e5113fefd1c226ad Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 09:02:33 -0400 Subject: [PATCH 47/68] Removed jaraco.test dependency, no longer needed. --- mypy.ini | 4 ---- pyproject.toml | 1 - 2 files changed, 5 deletions(-) diff --git a/mypy.ini b/mypy.ini index feac94cc..1b0b1d8d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -21,7 +21,3 @@ ignore_missing_imports = True # jaraco/zipp#123 [mypy-zipp.*] ignore_missing_imports = True - -# jaraco/jaraco.test#7 -[mypy-jaraco.test.*] -ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index c42dc0e7..e825d637 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,6 @@ test = [ "packaging", "pyfakefs", "pytest-perf >= 0.9.2", - "jaraco.test >= 5.4", ] doc = [ From 2dcb761d940b0115b786ab3b6f336af7d94630f4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 09:05:22 -0400 Subject: [PATCH 48/68] Add uniform exclusions for test.support. --- mypy.ini | 3 +++ tests/compat/py312.py | 2 +- tests/fixtures.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy.ini b/mypy.ini index 1b0b1d8d..533fe73d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -21,3 +21,6 @@ ignore_missing_imports = True # jaraco/zipp#123 [mypy-zipp.*] ignore_missing_imports = True + +[mypy-test.support.*] +ignore_missing_imports = True diff --git a/tests/compat/py312.py b/tests/compat/py312.py index ef2f0495..904446b1 100644 --- a/tests/compat/py312.py +++ b/tests/compat/py312.py @@ -1,6 +1,6 @@ import contextlib -from test.support import import_helper # type: ignore[import-untyped] +from test.support import import_helper @contextlib.contextmanager diff --git a/tests/fixtures.py b/tests/fixtures.py index c2211739..0416e4a4 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -8,7 +8,7 @@ import textwrap from importlib import resources -from test.support import os_helper # type: ignore[import-untyped] +from test.support import os_helper from . import _path from ._path import FilesSpec From b89388a53bf857127e0a6860dfcfe2cd69a79ab8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 09:06:45 -0400 Subject: [PATCH 49/68] Import os_helper directly. --- tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index f4ae69a7..fff50eb9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -4,6 +4,7 @@ import unittest import pyfakefs.fake_filesystem_unittest as ffs +from test.support import os_helper import importlib_metadata from importlib_metadata import ( @@ -20,7 +21,6 @@ from . import fixtures from ._path import Symlink -from .fixtures import os_helper class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): From 6027933ae96c9e51dd0b7ce392cb30f6fcae1940 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 09:07:37 -0400 Subject: [PATCH 50/68] Add news fragment. --- newsfragments/+530.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/+530.feature.rst diff --git a/newsfragments/+530.feature.rst b/newsfragments/+530.feature.rst new file mode 100644 index 00000000..0c0fe6a5 --- /dev/null +++ b/newsfragments/+530.feature.rst @@ -0,0 +1 @@ +Removed Python 3.9 compatibility. From a5c2154835facb4a9d0a6f5b3aac1f3d1ff86170 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 09:09:29 -0400 Subject: [PATCH 51/68] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+530.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+530.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 1a92cd19..1f2e2141 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.8.0 +====== + +Features +-------- + +- Removed Python 3.9 compatibility. + + v8.7.1 ====== diff --git a/newsfragments/+530.feature.rst b/newsfragments/+530.feature.rst deleted file mode 100644 index 0c0fe6a5..00000000 --- a/newsfragments/+530.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Removed Python 3.9 compatibility. From 0ac27203f8044daf634c22f385838122a0707449 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 20:15:05 -0400 Subject: [PATCH 52/68] Add news fragment. --- NEWS.rst | 11 ----------- newsfragments/532.removal.rst | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) create mode 100644 newsfragments/532.removal.rst diff --git a/NEWS.rst b/NEWS.rst index 83944755..1a92cd19 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,14 +1,3 @@ -v8.8.0 -====== - -Features --------- - -- Added ``MetadataNotFound`` (subclass of ``FileNotFoundError``) and updated - ``Distribution.metadata``/``metadata()`` to raise it when the metadata files - are missing instead of returning ``None`` (python/cpython#143387). - - v8.7.1 ====== diff --git a/newsfragments/532.removal.rst b/newsfragments/532.removal.rst new file mode 100644 index 00000000..355050a7 --- /dev/null +++ b/newsfragments/532.removal.rst @@ -0,0 +1 @@ +Added ``MetadataNotFound`` (subclass of ``FileNotFoundError``) and updated ``Distribution.metadata``/``metadata()`` to raise it when the metadata files are missing instead of returning ``None`` (python/cpython#143387). From 2f4088e490a73ac7f39b86214d2da16d2eb1ff39 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 20:18:10 -0400 Subject: [PATCH 53/68] Remove news fragments about internal details. --- newsfragments/+.removal.rst | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 newsfragments/+.removal.rst diff --git a/newsfragments/+.removal.rst b/newsfragments/+.removal.rst deleted file mode 100644 index cbbb2158..00000000 --- a/newsfragments/+.removal.rst +++ /dev/null @@ -1,2 +0,0 @@ -- Removed the internal ``md_none`` typing helper since ``Distribution.metadata`` now always returns ``PackageMetadata`` and raises ``MetadataNotFound`` when absent (python/cpython#143387). -- Vendored ``ExceptionTrap`` from ``jaraco.context`` (as ``_context``) and now rely on its ``passes`` helper when checking for missing metadata, keeping behavior aligned without adding dependencies. From a9f883fef337c667a81a987bc0cbc0dbb43b2bfe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 02:39:14 -0400 Subject: [PATCH 54/68] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/532.removal.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/532.removal.rst diff --git a/NEWS.rst b/NEWS.rst index 1f2e2141..f83925b4 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v9.0.0 +====== + +Deprecations and Removals +------------------------- + +- Added ``MetadataNotFound`` (subclass of ``FileNotFoundError``) and updated ``Distribution.metadata``/``metadata()`` to raise it when the metadata files are missing instead of returning ``None`` (python/cpython#143387). (#532) + + v8.8.0 ====== diff --git a/newsfragments/532.removal.rst b/newsfragments/532.removal.rst deleted file mode 100644 index 355050a7..00000000 --- a/newsfragments/532.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``MetadataNotFound`` (subclass of ``FileNotFoundError``) and updated ``Distribution.metadata``/``metadata()`` to raise it when the metadata files are missing instead of returning ``None`` (python/cpython#143387). From 3e7f3ac6742a63ab729966c0ff8e205f92ac42f7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 13 Jan 2026 08:54:15 +0200 Subject: [PATCH 55/68] gh-143658: importlib.metadata: Use `str.translate` to improve performance of `importlib.metadata.Prepared.normalized` (#143660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Henry Schreiner Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Bartosz Sławecki --- ...-01-10-15-40-57.gh-issue-143658.Ox6pE5.rst | 3 ++ importlib_metadata/__init__.py | 16 ++++++++- tests/test_api.py | 34 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-01-10-15-40-57.gh-issue-143658.Ox6pE5.rst diff --git a/Misc/NEWS.d/next/Library/2026-01-10-15-40-57.gh-issue-143658.Ox6pE5.rst b/Misc/NEWS.d/next/Library/2026-01-10-15-40-57.gh-issue-143658.Ox6pE5.rst new file mode 100644 index 00000000..1d227095 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-10-15-40-57.gh-issue-143658.Ox6pE5.rst @@ -0,0 +1,3 @@ +:mod:`importlib.metadata`: Use :meth:`str.translate` to improve performance of +:meth:`!importlib.metadata.Prepared.normalize`. Patch by Hugo van Kemenade and +Henry Schreiner. diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index cdfc1f62..04575234 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -894,6 +894,14 @@ def search(self, prepared: Prepared): return itertools.chain(infos, eggs) +# Translation table for Prepared.normalize: lowercase and +# replace "-" (hyphen) and "." (dot) with "_" (underscore). +_normalize_table = str.maketrans( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ-.", + "abcdefghijklmnopqrstuvwxyz__", +) + + class Prepared: """ A prepared search query for metadata on a possibly-named package. @@ -929,7 +937,13 @@ def normalize(name): """ PEP 503 normalization plus dashes as underscores. """ - return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_') + # Emulates ``re.sub(r"[-_.]+", "-", name).lower()`` from PEP 503 + # About 3x faster, safe since packages only support alphanumeric characters + value = name.translate(_normalize_table) + # Condense repeats (faster than regex) + while "__" in value: + value = value.replace("__", "_") + return value @staticmethod def legacy_normalize(name): diff --git a/tests/test_api.py b/tests/test_api.py index c36f93e0..553fe740 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -6,6 +6,7 @@ from importlib_metadata import ( Distribution, PackageNotFoundError, + Prepared, distribution, entry_points, files, @@ -317,3 +318,36 @@ class InvalidateCache(unittest.TestCase): def test_invalidate_cache(self): # No externally observable behavior, but ensures test coverage... importlib.invalidate_caches() + + +class PreparedTests(unittest.TestCase): + def test_normalize(self): + tests = [ + # Simple + ("sample", "sample"), + # Mixed case + ("Sample", "sample"), + ("SAMPLE", "sample"), + ("SaMpLe", "sample"), + # Separator conversions + ("sample-pkg", "sample_pkg"), + ("sample.pkg", "sample_pkg"), + ("sample_pkg", "sample_pkg"), + # Multiple separators + ("sample---pkg", "sample_pkg"), + ("sample___pkg", "sample_pkg"), + ("sample...pkg", "sample_pkg"), + # Mixed separators + ("sample-._pkg", "sample_pkg"), + ("sample_.-pkg", "sample_pkg"), + # Complex + ("Sample__Pkg-name.foo", "sample_pkg_name_foo"), + ("Sample__Pkg.name__foo", "sample_pkg_name_foo"), + # Uppercase with separators + ("SAMPLE-PKG", "sample_pkg"), + ("Sample.Pkg", "sample_pkg"), + ("SAMPLE_PKG", "sample_pkg"), + ] + for name, expected in tests: + with self.subTest(name=name): + self.assertEqual(Prepared.normalize(name), expected) From 001db0db09ddc4fb9906cfae5e5c0d737f619313 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:38:58 +0200 Subject: [PATCH 56/68] gh-143658: Use `str.lower` and `replace` to further improve performance of `importlib.metadata.Prepared.normalized` (#144083) Co-authored-by: Henry Schreiner --- .../2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst | 4 ++++ importlib_metadata/__init__.py | 13 ++----------- 2 files changed, 6 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst diff --git a/Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst b/Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst new file mode 100644 index 00000000..8935b4c6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst @@ -0,0 +1,4 @@ +:mod:`importlib.metadata`: Use :meth:`str.lower` and :meth:`str.replace` to +further improve performance of +:meth:`!importlib.metadata.Prepared.normalize`. Patch by Hugo van Kemenade +and Henry Schreiner. diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 04575234..09b37255 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -894,14 +894,6 @@ def search(self, prepared: Prepared): return itertools.chain(infos, eggs) -# Translation table for Prepared.normalize: lowercase and -# replace "-" (hyphen) and "." (dot) with "_" (underscore). -_normalize_table = str.maketrans( - "ABCDEFGHIJKLMNOPQRSTUVWXYZ-.", - "abcdefghijklmnopqrstuvwxyz__", -) - - class Prepared: """ A prepared search query for metadata on a possibly-named package. @@ -937,9 +929,8 @@ def normalize(name): """ PEP 503 normalization plus dashes as underscores. """ - # Emulates ``re.sub(r"[-_.]+", "-", name).lower()`` from PEP 503 - # About 3x faster, safe since packages only support alphanumeric characters - value = name.translate(_normalize_table) + # Much faster than re.sub, and even faster than str.translate + value = name.lower().replace("-", "_").replace(".", "_") # Condense repeats (faster than regex) while "__" in value: value = value.replace("__", "_") From 852e44f218d75fcffaca50a56169fcc4763d863f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 03:10:24 -0400 Subject: [PATCH 57/68] Remove CPython news fragments. --- .../Library/2026-01-10-15-40-57.gh-issue-143658.Ox6pE5.rst | 3 --- .../Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst | 4 ---- 2 files changed, 7 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2026-01-10-15-40-57.gh-issue-143658.Ox6pE5.rst delete mode 100644 Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst diff --git a/Misc/NEWS.d/next/Library/2026-01-10-15-40-57.gh-issue-143658.Ox6pE5.rst b/Misc/NEWS.d/next/Library/2026-01-10-15-40-57.gh-issue-143658.Ox6pE5.rst deleted file mode 100644 index 1d227095..00000000 --- a/Misc/NEWS.d/next/Library/2026-01-10-15-40-57.gh-issue-143658.Ox6pE5.rst +++ /dev/null @@ -1,3 +0,0 @@ -:mod:`importlib.metadata`: Use :meth:`str.translate` to improve performance of -:meth:`!importlib.metadata.Prepared.normalize`. Patch by Hugo van Kemenade and -Henry Schreiner. diff --git a/Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst b/Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst deleted file mode 100644 index 8935b4c6..00000000 --- a/Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst +++ /dev/null @@ -1,4 +0,0 @@ -:mod:`importlib.metadata`: Use :meth:`str.lower` and :meth:`str.replace` to -further improve performance of -:meth:`!importlib.metadata.Prepared.normalize`. Patch by Hugo van Kemenade -and Henry Schreiner. From bbb4f6d4134597599dce397bdddc3e81de8f5c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Wed, 15 Oct 2025 18:49:14 +0200 Subject: [PATCH 58/68] gh-140141: Properly break exception chain in `importlib.metadata.Distribution.from_name` (#140142) --- .../Library/2025-10-15-17-23-51.gh-issue-140141.j2mUDB.rst | 5 +++++ importlib_metadata/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-15-17-23-51.gh-issue-140141.j2mUDB.rst diff --git a/Misc/NEWS.d/next/Library/2025-10-15-17-23-51.gh-issue-140141.j2mUDB.rst b/Misc/NEWS.d/next/Library/2025-10-15-17-23-51.gh-issue-140141.j2mUDB.rst new file mode 100644 index 00000000..2edadbc3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-15-17-23-51.gh-issue-140141.j2mUDB.rst @@ -0,0 +1,5 @@ +The :py:class:`importlib.metadata.PackageNotFoundError` traceback raised when +``importlib.metadata.Distribution.from_name`` cannot discover a +distribution no longer includes a transient :exc:`StopIteration` exception trace. + +Contributed by Bartosz Sławecki in :gh:`140142`. diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index cdfc1f62..d4d6a9d5 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -462,7 +462,7 @@ def from_name(cls, name: str) -> Distribution: try: return next(iter(cls._prefer_valid(cls.discover(name=name)))) except StopIteration: - raise PackageNotFoundError(name) + raise PackageNotFoundError(name) from None @classmethod def discover( From aa488ac1f89289f7772d1060f9818ac3e0ae04d9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 03:23:14 -0400 Subject: [PATCH 59/68] Remove CPython news fragments. --- .../Library/2025-10-15-17-23-51.gh-issue-140141.j2mUDB.rst | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2025-10-15-17-23-51.gh-issue-140141.j2mUDB.rst diff --git a/Misc/NEWS.d/next/Library/2025-10-15-17-23-51.gh-issue-140141.j2mUDB.rst b/Misc/NEWS.d/next/Library/2025-10-15-17-23-51.gh-issue-140141.j2mUDB.rst deleted file mode 100644 index 2edadbc3..00000000 --- a/Misc/NEWS.d/next/Library/2025-10-15-17-23-51.gh-issue-140141.j2mUDB.rst +++ /dev/null @@ -1,5 +0,0 @@ -The :py:class:`importlib.metadata.PackageNotFoundError` traceback raised when -``importlib.metadata.Distribution.from_name`` cannot discover a -distribution no longer includes a transient :exc:`StopIteration` exception trace. - -Contributed by Bartosz Sławecki in :gh:`140142`. From 1b0be12fc662f0ba4ee6c86d544585485ff40dac Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 03:35:17 -0400 Subject: [PATCH 60/68] Use parameterize fixture for parameterized tests. --- tests/test_api.py | 58 +++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 553fe740..3dbed628 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -321,33 +321,31 @@ def test_invalidate_cache(self): class PreparedTests(unittest.TestCase): - def test_normalize(self): - tests = [ - # Simple - ("sample", "sample"), - # Mixed case - ("Sample", "sample"), - ("SAMPLE", "sample"), - ("SaMpLe", "sample"), - # Separator conversions - ("sample-pkg", "sample_pkg"), - ("sample.pkg", "sample_pkg"), - ("sample_pkg", "sample_pkg"), - # Multiple separators - ("sample---pkg", "sample_pkg"), - ("sample___pkg", "sample_pkg"), - ("sample...pkg", "sample_pkg"), - # Mixed separators - ("sample-._pkg", "sample_pkg"), - ("sample_.-pkg", "sample_pkg"), - # Complex - ("Sample__Pkg-name.foo", "sample_pkg_name_foo"), - ("Sample__Pkg.name__foo", "sample_pkg_name_foo"), - # Uppercase with separators - ("SAMPLE-PKG", "sample_pkg"), - ("Sample.Pkg", "sample_pkg"), - ("SAMPLE_PKG", "sample_pkg"), - ] - for name, expected in tests: - with self.subTest(name=name): - self.assertEqual(Prepared.normalize(name), expected) + @fixtures.parameterize( + # Simple + dict(input='sample', expected='sample'), + # Mixed case + dict(input='Sample', expected='sample'), + dict(input='SAMPLE', expected='sample'), + dict(input='SaMpLe', expected='sample'), + # Separator conversions + dict(input='sample-pkg', expected='sample_pkg'), + dict(input='sample.pkg', expected='sample_pkg'), + dict(input='sample_pkg', expected='sample_pkg'), + # Multiple separators + dict(input='sample---pkg', expected='sample_pkg'), + dict(input='sample___pkg', expected='sample_pkg'), + dict(input='sample...pkg', expected='sample_pkg'), + # Mixed separators + dict(input='sample-._pkg', expected='sample_pkg'), + dict(input='sample_.-pkg', expected='sample_pkg'), + # Complex + dict(input='Sample__Pkg-name.foo', expected='sample_pkg_name_foo'), + dict(input='Sample__Pkg.name__foo', expected='sample_pkg_name_foo'), + # Uppercase with separators + dict(input='SAMPLE-PKG', expected='sample_pkg'), + dict(input='Sample.Pkg', expected='sample_pkg'), + dict(input='SAMPLE_PKG', expected='sample_pkg'), + ) + def test_normalize(self, input, expected): + self.assertEqual(Prepared.normalize(input), expected) From a77d0d1b2f79d7fd21728284d2955ffa6d5caceb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 03:40:52 -0400 Subject: [PATCH 61/68] Add performance test for Prepared.normalize. --- exercises.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/exercises.py b/exercises.py index adccf03c..b346cc05 100644 --- a/exercises.py +++ b/exercises.py @@ -45,3 +45,10 @@ def entrypoint_regexp_perf(): input = '0' + ' ' * 2**10 + '0' # end warmup re.match(importlib_metadata.EntryPoint.pattern, input) + + +def normalize_perf(): + # python/cpython#143658 + import importlib_metadata # end warmup + + importlib_metadata.Prepared.normalize('sample') From cbadafcad64cee12d292ed8ac1dc96bb0295966a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 04:31:01 -0400 Subject: [PATCH 62/68] Repeat the operation to get performance visibility. --- exercises.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/exercises.py b/exercises.py index b346cc05..cf6d9d18 100644 --- a/exercises.py +++ b/exercises.py @@ -51,4 +51,7 @@ def normalize_perf(): # python/cpython#143658 import importlib_metadata # end warmup - importlib_metadata.Prepared.normalize('sample') + # operation completes in < 1ms, so repeat it to get visibility + # https://github.com/jaraco/pytest-perf/issues/12 + for _ in range(1000): + importlib_metadata.Prepared.normalize('sample') From 27169dcd343e65727805c12bc95bd52c9153cd04 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 11:47:45 -0400 Subject: [PATCH 63/68] Move behavior description into the docstring. Remove references to intermediate implementations. Reference the rationale. --- importlib_metadata/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 09b37255..88d65d5d 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -636,7 +636,8 @@ def _read_files_egginfo_installed(self): return paths = ( - py311.relative_fix((subdir / name).resolve()) + py311 + .relative_fix((subdir / name).resolve()) .relative_to(self.locate_file('').resolve(), walk_up=True) .as_posix() for name in text.splitlines() @@ -928,10 +929,12 @@ def __init__(self, name: str | None): def normalize(name): """ PEP 503 normalization plus dashes as underscores. + + Specifically avoids ``re.sub`` as prescribed for performance + benefits (see python/cpython#143658). """ - # Much faster than re.sub, and even faster than str.translate value = name.lower().replace("-", "_").replace(".", "_") - # Condense repeats (faster than regex) + # Condense repeats while "__" in value: value = value.replace("__", "_") return value From 349957ee71c4a9ddbfc67fd9c907a3df80a2e64b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 12:08:25 -0400 Subject: [PATCH 64/68] Add news fragment. --- newsfragments/+e6755131.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/+e6755131.feature.rst diff --git a/newsfragments/+e6755131.feature.rst b/newsfragments/+e6755131.feature.rst new file mode 100644 index 00000000..22021efb --- /dev/null +++ b/newsfragments/+e6755131.feature.rst @@ -0,0 +1 @@ +Ported changes from CPython (python/cpython#110937, python/cpython#140141, python/cpython#143658) From 613e9801303fff742a40cde7e46aaecddb41cdbf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 12:08:45 -0400 Subject: [PATCH 65/68] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+e6755131.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+e6755131.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 1f2e2141..8ab689df 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.9.0 +====== + +Features +-------- + +- Ported changes from CPython (python/cpython#110937, python/cpython#140141, python/cpython#143658) + + v8.8.0 ====== diff --git a/newsfragments/+e6755131.feature.rst b/newsfragments/+e6755131.feature.rst deleted file mode 100644 index 22021efb..00000000 --- a/newsfragments/+e6755131.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Ported changes from CPython (python/cpython#110937, python/cpython#140141, python/cpython#143658) From 76f03df2f4df25de8aa9424cf31e600cf27f7d59 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 12:51:08 -0400 Subject: [PATCH 66/68] =?UTF-8?q?=F0=9F=9A=A1=20Toil=20the=20docs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 32528f86..aed40cd2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -82,6 +82,7 @@ nitpick_ignore += [ # Workaround for #316 ('py:class', 'importlib_metadata.EntryPoints'), + ('py:class', 'importlib_metadata.FileHash'), ('py:class', 'importlib_metadata.PackagePath'), ('py:class', 'importlib_metadata.SelectableGroups'), ('py:class', 'importlib_metadata._meta._T'), From 6f29e814f053475dbf65b3c85fb038463527289d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 15:59:36 -0400 Subject: [PATCH 67/68] Suppress 'fork in thread' deprecation warnings as introduced in Python 3.15. --- tests/compat/py314.py | 43 +++++++++++++++++++++++++++++++++++++++++++ tests/test_zip.py | 2 ++ 2 files changed, 45 insertions(+) create mode 100644 tests/compat/py314.py diff --git a/tests/compat/py314.py b/tests/compat/py314.py new file mode 100644 index 00000000..cf34f467 --- /dev/null +++ b/tests/compat/py314.py @@ -0,0 +1,43 @@ +import contextlib +import sys +import types +import warnings + +from test.support import warnings_helper as orig + + +@contextlib.contextmanager +def ignore_warnings(*, category, message=''): + """Decorator to suppress warnings. + + Can also be used as a context manager. This is not preferred, + because it makes diffs more noisy and tools like 'git blame' less useful. + But, it's useful for async functions. + """ + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', category=category, message=message) + yield + + +@contextlib.contextmanager +def ignore_fork_in_thread_deprecation_warnings(): + """Suppress deprecation warnings related to forking in multi-threaded code. + + See gh-135427 + + Can be used as decorator (preferred) or context manager. + """ + with ignore_warnings( + message=".*fork.*may lead to deadlocks in the child.*", + category=DeprecationWarning, + ): + yield + + +if sys.version_info >= (3, 15): + warnings_helper = orig +else: + warnings_helper = types.SimpleNamespace( + ignore_fork_in_thread_deprecation_warnings=ignore_fork_in_thread_deprecation_warnings, + **vars(orig), + ) diff --git a/tests/test_zip.py b/tests/test_zip.py index 165aa6dd..67407145 100644 --- a/tests/test_zip.py +++ b/tests/test_zip.py @@ -14,6 +14,7 @@ ) from . import fixtures +from .compat.py314 import warnings_helper class TestZip(fixtures.ZipFixtures, unittest.TestCase): @@ -50,6 +51,7 @@ def test_one_distribution(self): dists = list(distributions(path=sys.path[:1])) assert len(dists) == 1 + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @unittest.skipUnless( hasattr(os, 'register_at_fork') and 'fork' in multiprocessing.get_all_start_methods(), From 684a3157eae9988cdd8e95969efa9a79a70f69f6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 13 Apr 2026 04:21:33 -0400 Subject: [PATCH 68/68] Bump badge for 2026. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3000f5ab..44f48639 100644 --- a/README.rst +++ b/README.rst @@ -14,5 +14,5 @@ .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2025-informational +.. image:: https://img.shields.io/badge/skeleton-2026-informational :target: https://blog.jaraco.com/skeleton