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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] =?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/33] 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/33] 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/33] 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/33] 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/33] 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/33] =?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/33] =?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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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.