From 5ddd1228a1c96b803409159d2acd2eccb064ed48 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 25 Jul 2024 16:56:48 -0400 Subject: [PATCH 001/176] Expand the documentation of Distribution.locate_file to explain why 'locate_file' does what it does and what the consequences are of not implementing it. Ref pypa/pip#11684 --- importlib_metadata/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2c71d33c..2eefb1d6 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -373,6 +373,17 @@ def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: """ Given a path to a file in this distribution, return a SimplePath to it. + + This method is used by callers of ``Distribution.files()`` to + locate files within the distribution. If it's possible for a + Distribution to represent files in the distribution as + ``SimplePath`` objects, it should implement this method + to resolve such objects. + + Some Distribution providers may elect not to resolve SimplePath + objects within the distribution by raising a + NotImplementedError, but consumers of such a Distribution would + be unable to invoke ``Distribution.files()``. """ @classmethod From 875003a735fdd6334782250d15baa8142896a9a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 1 Aug 2024 15:48:57 -0400 Subject: [PATCH 002/176] Disallow passing of 'dist' to EntryPoints.select. Closes python/cpython#107220. --- importlib_metadata/__init__.py | 17 +++++++++++++++++ newsfragments/+29a322e3.feature.rst | 1 + 2 files changed, 18 insertions(+) create mode 100644 newsfragments/+29a322e3.feature.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2eefb1d6..9a96d061 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -227,9 +227,26 @@ def matches(self, **params): >>> ep.matches(attr='bong') True """ + self._disallow_dist(params) attrs = (getattr(self, param) for param in params) return all(map(operator.eq, params.values(), attrs)) + @staticmethod + def _disallow_dist(params): + """ + Querying by dist is not allowed (dist objects are not comparable). + >>> EntryPoint(name='fan', value='fav', group='fag').matches(dist='foo') + Traceback (most recent call last): + ... + ValueError: "dist" is not suitable for matching... + """ + if "dist" in params: + raise ValueError( + '"dist" is not suitable for matching. ' + "Instead, use Distribution.entry_points.select() on a " + "located distribution." + ) + def _key(self): return self.name, self.value, self.group diff --git a/newsfragments/+29a322e3.feature.rst b/newsfragments/+29a322e3.feature.rst new file mode 100644 index 00000000..0e80b746 --- /dev/null +++ b/newsfragments/+29a322e3.feature.rst @@ -0,0 +1 @@ +Disallow passing of 'dist' to EntryPoints.select. \ No newline at end of file From 8e66fbc444727f75a54c055bfcc5504c49f6418c Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Mon, 5 Aug 2024 19:40:35 +0100 Subject: [PATCH 003/176] Defer import inspect --- importlib_metadata/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2eefb1d6..a8dead9b 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -8,7 +8,6 @@ import zipp import email import types -import inspect import pathlib import operator import textwrap @@ -1071,6 +1070,9 @@ def _topmost(name: PackagePath) -> Optional[str]: return top if rest else None +inspect = None + + def _get_toplevel_name(name: PackagePath) -> str: """ Infer a possibly importable module name from a name presumed on @@ -1089,11 +1091,14 @@ def _get_toplevel_name(name: PackagePath) -> str: >>> _get_toplevel_name(PackagePath('foo.dist-info')) 'foo.dist-info' """ - return _topmost(name) or ( - # python/typeshed#10328 - inspect.getmodulename(name) # type: ignore - or str(name) - ) + n = _topmost(name) + if n: + return n + + global inspect + if inspect is None: + import inspect + return inspect.getmodulename(name) or str(name) def _top_level_inferred(dist): From 06acfd262258d809242c74179477af324389e1c7 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:14:35 +0200 Subject: [PATCH 004/176] Update to the latest ruff version (jaraco/skeleton#137) --- .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 5a4a7e91..ff54405e 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.1.8 + rev: v0.5.6 hooks: - id: ruff - id: ruff-format From dd30b7600f33ce06a479a73002b950f4a3947759 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 8 Aug 2024 17:19:17 -0400 Subject: [PATCH 005/176] Add Protocols, remove @overload, from `.coveragerc` `exclude_also` (jaraco/skeleton#135) Co-authored-by: Jason R. Coombs --- .coveragerc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 35b98b1d..2e3f4dd7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,8 @@ disable_warnings = [report] show_missing = True exclude_also = - # jaraco/skeleton#97 - @overload + # Exclude common false positives per + # https://coverage.readthedocs.io/en/latest/excluding.html#advanced-exclusion + # Ref jaraco/skeleton#97 and jaraco/skeleton#135 + class .*\bProtocol\): if TYPE_CHECKING: From 3841656c61bad87f922fcba50445b503209b69c2 Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 12 Aug 2024 12:13:19 -0400 Subject: [PATCH 006/176] Loosen restrictions on mypy (jaraco/skeleton#136) Based on changes downstream in setuptools. --- mypy.ini | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/mypy.ini b/mypy.ini index b6f97276..83b0d15c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,14 @@ [mypy] -ignore_missing_imports = True -# required to support namespace packages -# https://github.com/python/mypy/issues/14057 +# Is the project well-typed? +strict = False + +# Early opt-in even when strict = False +warn_unused_ignores = True +warn_redundant_casts = True +enable_error_code = ignore-without-code + +# Support namespace packages per https://github.com/python/mypy/issues/14057 explicit_package_bases = True + +# Disable overload-overlap due to many false-positives +disable_error_code = overload-overlap From 1a27fd5b8815e65571e6c028d6bef2c1daf61688 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Aug 2024 12:16:15 -0400 Subject: [PATCH 007/176] Split the test dependencies into four classes (test, cover, type, check). (jaraco/skeleton#139) --- pyproject.toml | 25 ++++++++++++++++++++----- tox.ini | 4 ++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1307e1fa..31057d85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,14 +28,10 @@ Source = "https://github.com/PROJECT_PATH" test = [ # upstream "pytest >= 6, != 8.1.*", - "pytest-checkdocs >= 2.4", - "pytest-cov", - "pytest-mypy", - "pytest-enabler >= 2.2", - "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", # local ] + doc = [ # upstream "sphinx >= 3.5", @@ -47,4 +43,23 @@ doc = [ # local ] +check = [ + "pytest-checkdocs >= 2.4", + "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", +] + +cover = [ + "pytest-cov", +] + +enabler = [ + "pytest-enabler >= 2.2", +] + +type = [ + "pytest-mypy", +] + + + [tool.setuptools_scm] diff --git a/tox.ini b/tox.ini index cc4db36e..01f0975f 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,10 @@ commands = usedevelop = True extras = test + check + cover + enabler + type [testenv:diffcov] description = run tests and check that diff from main is covered From 6d9b766099dbac1c97a220badde7e14304e03291 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 19 Aug 2024 13:47:26 -0400 Subject: [PATCH 008/176] Remove MetadataPathFinder regardless of its position. Closes #500 --- conftest.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/conftest.py b/conftest.py index 779ac24b..762e66f3 100644 --- a/conftest.py +++ b/conftest.py @@ -13,13 +13,18 @@ def pytest_configure(): def remove_importlib_metadata(): """ - Because pytest imports importlib_metadata, the coverage - reports are broken (#322). So work around the issue by - undoing the changes made by pytest's import of - importlib_metadata (if any). + Ensure importlib_metadata is not imported yet. + + Because pytest or other modules might import + importlib_metadata, the coverage reports are broken (#322). + Work around the issue by undoing the changes made by a + previous import of importlib_metadata (if any). """ - if sys.meta_path[-1].__class__.__name__ == 'MetadataPathFinder': - del sys.meta_path[-1] + sys.meta_path[:] = [ + item + for item in sys.meta_path + if item.__class__.__name__ != 'MetadataPathFinder' + ] for mod in list(sys.modules): if mod.startswith('importlib_metadata'): del sys.modules[mod] From 3c8e1ec4e34c11dcff086be7fbd0d1981bf32480 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 19 Aug 2024 15:35:22 -0400 Subject: [PATCH 009/176] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+29a322e3.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+29a322e3.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 2e22a335..018825be 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.3.0 +====== + +Features +-------- + +- Disallow passing of 'dist' to EntryPoints.select. + + v8.2.0 ====== diff --git a/newsfragments/+29a322e3.feature.rst b/newsfragments/+29a322e3.feature.rst deleted file mode 100644 index 0e80b746..00000000 --- a/newsfragments/+29a322e3.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Disallow passing of 'dist' to EntryPoints.select. \ No newline at end of file From debb5165a88b1a4433150b265e155c21b497d154 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Tue, 20 Aug 2024 12:23:17 +0100 Subject: [PATCH 010/176] Don't use global var - wallrusify - add a note about deffered import --- importlib_metadata/__init__.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index a8dead9b..f76ef2cc 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1070,9 +1070,6 @@ def _topmost(name: PackagePath) -> Optional[str]: return top if rest else None -inspect = None - - def _get_toplevel_name(name: PackagePath) -> str: """ Infer a possibly importable module name from a name presumed on @@ -1091,13 +1088,12 @@ def _get_toplevel_name(name: PackagePath) -> str: >>> _get_toplevel_name(PackagePath('foo.dist-info')) 'foo.dist-info' """ - n = _topmost(name) - if n: + if n := _topmost(name): return n - global inspect - if inspect is None: - import inspect + # We're deffering import of inspect to speed up overall import time + import inspect + return inspect.getmodulename(name) or str(name) From e99c10510d48e840b0550bd05d1167633dcfaea7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:00:48 -0400 Subject: [PATCH 011/176] Restore single-expression logic. --- importlib_metadata/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index f76ef2cc..b1a15350 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1088,13 +1088,14 @@ def _get_toplevel_name(name: PackagePath) -> str: >>> _get_toplevel_name(PackagePath('foo.dist-info')) 'foo.dist-info' """ - if n := _topmost(name): - return n - # We're deffering import of inspect to speed up overall import time import inspect - return inspect.getmodulename(name) or str(name) + return _topmost(name) or ( + # python/typeshed#10328 + inspect.getmodulename(name) # type: ignore + or str(name) + ) def _top_level_inferred(dist): From a7aaf72702b3a49ea3e33c9cf7f223839067c883 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:01:47 -0400 Subject: [PATCH 012/176] Use third-person imperative voice and link to issue in comment. --- 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 b1a15350..c2b76dd2 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1088,7 +1088,7 @@ def _get_toplevel_name(name: PackagePath) -> str: >>> _get_toplevel_name(PackagePath('foo.dist-info')) 'foo.dist-info' """ - # We're deffering import of inspect to speed up overall import time + # Defer import of inspect for performance (python/cpython#118761) import inspect return _topmost(name) or ( From ebcdcfdd18d427498f11b74e245b3f8a7ef5df9c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:04:10 -0400 Subject: [PATCH 013/176] Remove workaround for python/typeshed#10328. --- importlib_metadata/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 72670f44..24587e68 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1108,11 +1108,7 @@ def _get_toplevel_name(name: PackagePath) -> str: # Defer import of inspect for performance (python/cpython#118761) import inspect - return _topmost(name) or ( - # python/typeshed#10328 - inspect.getmodulename(name) # type: ignore - or str(name) - ) + return _topmost(name) or (inspect.getmodulename(name) or str(name)) def _top_level_inferred(dist): From 71b467843258873048eb944545ba1235866523e6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:05:25 -0400 Subject: [PATCH 014/176] Add news fragment. --- newsfragments/499.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/499.feature.rst diff --git a/newsfragments/499.feature.rst b/newsfragments/499.feature.rst new file mode 100644 index 00000000..1aa347ed --- /dev/null +++ b/newsfragments/499.feature.rst @@ -0,0 +1 @@ +Deferred import of inspect for import performance. \ No newline at end of file From 1616cb3a82c33c3603ff984b6ff417e68068aa6e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:06:05 -0400 Subject: [PATCH 015/176] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/499.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/499.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 018825be..f4a37fdf 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.4.0 +====== + +Features +-------- + +- Deferred import of inspect for import performance. (#499) + + v8.3.0 ====== diff --git a/newsfragments/499.feature.rst b/newsfragments/499.feature.rst deleted file mode 100644 index 1aa347ed..00000000 --- a/newsfragments/499.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Deferred import of inspect for import performance. \ No newline at end of file From 46c6127405cba0cf19b96ce6e533c28890eb1ea3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:39:18 -0400 Subject: [PATCH 016/176] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conftest.py | 1 - exercises.py | 4 +++- importlib_metadata/__init__.py | 34 ++++++++++++++++---------------- importlib_metadata/_adapters.py | 2 +- importlib_metadata/_compat.py | 3 +-- importlib_metadata/_functools.py | 2 +- importlib_metadata/_meta.py | 14 ++++++++++--- tests/_path.py | 3 +-- tests/compat/py39.py | 1 - tests/compat/test_py39_compat.py | 5 +++-- tests/fixtures.py | 14 ++++++------- tests/test_api.py | 5 +++-- tests/test_integration.py | 4 +++- tests/test_main.py | 13 ++++++------ tests/test_zip.py | 3 ++- 15 files changed, 59 insertions(+), 49 deletions(-) diff --git a/conftest.py b/conftest.py index 762e66f3..6d3402d6 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,5 @@ import sys - collect_ignore = [ # this module fails mypy tests because 'setup.py' matches './setup.py' 'tests/data/sources/example/setup.py', diff --git a/exercises.py b/exercises.py index c88fa983..adccf03c 100644 --- a/exercises.py +++ b/exercises.py @@ -29,6 +29,7 @@ def cached_distribution_perf(): def uncached_distribution_perf(): "uncached distribution" import importlib + import importlib_metadata # end warmup @@ -37,9 +38,10 @@ def uncached_distribution_perf(): def entrypoint_regexp_perf(): - import importlib_metadata import re + import importlib_metadata + input = '0' + ' ' * 2**10 + '0' # end warmup re.match(importlib_metadata.EntryPoint.pattern, input) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 24587e68..84d16508 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1,23 +1,28 @@ from __future__ import annotations -import os -import re import abc -import sys -import json -import zipp +import collections import email -import types -import pathlib -import operator -import textwrap import functools import itertools +import json +import operator +import os +import pathlib import posixpath -import collections +import re +import sys +import textwrap +import types +from contextlib import suppress +from importlib import import_module +from importlib.abc import MetaPathFinder +from itertools import starmap +from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast + +import zipp from . import _meta -from .compat import py39, py311 from ._collections import FreezableDefaultDict, Pair from ._compat import ( NullFinder, @@ -26,12 +31,7 @@ from ._functools import method_cache, pass_none from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath - -from contextlib import suppress -from importlib import import_module -from importlib.abc import MetaPathFinder -from itertools import starmap -from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast +from .compat import py39, py311 __all__ = [ 'Distribution', diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index 6223263e..3b516a2d 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -1,6 +1,6 @@ +import email.message import re import textwrap -import email.message from ._text import FoldedCase diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index df312b1c..01356d69 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -1,6 +1,5 @@ -import sys import platform - +import sys __all__ = ['install', 'NullFinder'] diff --git a/importlib_metadata/_functools.py b/importlib_metadata/_functools.py index 71f66bd0..5dda6a21 100644 --- a/importlib_metadata/_functools.py +++ b/importlib_metadata/_functools.py @@ -1,5 +1,5 @@ -import types import functools +import types # from jaraco.functools 3.3 diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 1927d0f6..0942bbd9 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -1,9 +1,17 @@ from __future__ import annotations import os -from typing import Protocol -from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload - +from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + Protocol, + TypeVar, + Union, + overload, +) _T = TypeVar("_T") diff --git a/tests/_path.py b/tests/_path.py index b3cfb9cd..7e4f7ee0 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -2,8 +2,7 @@ import functools import pathlib -from typing import Dict, Protocol, Union -from typing import runtime_checkable +from typing import Dict, Protocol, Union, runtime_checkable class Symlink(str): diff --git a/tests/compat/py39.py b/tests/compat/py39.py index 9476eb35..4e45d7cc 100644 --- a/tests/compat/py39.py +++ b/tests/compat/py39.py @@ -1,6 +1,5 @@ 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' ) diff --git a/tests/compat/test_py39_compat.py b/tests/compat/test_py39_compat.py index 549e518a..db9fb1b7 100644 --- a/tests/compat/test_py39_compat.py +++ b/tests/compat/test_py39_compat.py @@ -1,8 +1,7 @@ -import sys import pathlib +import sys import unittest -from .. import fixtures from importlib_metadata import ( distribution, distributions, @@ -11,6 +10,8 @@ version, ) +from .. import fixtures + class OldStdlibFinderTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): def setUp(self): diff --git a/tests/fixtures.py b/tests/fixtures.py index 187f1705..410e6f17 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,18 +1,16 @@ -import sys +import contextlib import copy +import functools import json -import shutil import pathlib +import shutil +import sys import textwrap -import functools -import contextlib - -from .compat.py312 import import_helper -from .compat.py39 import os_helper from . import _path from ._path import FilesSpec - +from .compat.py39 import os_helper +from .compat.py312 import import_helper try: from importlib import resources # type: ignore diff --git a/tests/test_api.py b/tests/test_api.py index 7ce0cd64..c36f93e0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,9 +1,8 @@ +import importlib import re import textwrap import unittest -import importlib -from . import fixtures from importlib_metadata import ( Distribution, PackageNotFoundError, @@ -15,6 +14,8 @@ version, ) +from . import fixtures + class APITests( fixtures.EggInfoPkg, diff --git a/tests/test_integration.py b/tests/test_integration.py index f7af67f3..9bb3e793 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -8,15 +8,17 @@ """ import unittest + import packaging.requirements import packaging.version -from . import fixtures from importlib_metadata import ( _compat, version, ) +from . import fixtures + class IntegrationTests(fixtures.DistInfoPkg, unittest.TestCase): def test_package_spec_installed(self): diff --git a/tests/test_main.py b/tests/test_main.py index dc248492..7c9851fc 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,14 +1,11 @@ -import re +import importlib import pickle +import re import unittest -import importlib -import importlib_metadata -from .compat.py39 import os_helper import pyfakefs.fake_filesystem_unittest as ffs -from . import fixtures -from ._path import Symlink +import importlib_metadata from importlib_metadata import ( Distribution, EntryPoint, @@ -21,6 +18,10 @@ version, ) +from . import fixtures +from ._path import Symlink +from .compat.py39 import os_helper + class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): version_pattern = r'\d+\.\d+(\.\d)?' diff --git a/tests/test_zip.py b/tests/test_zip.py index 01aba6df..d4f8e2f0 100644 --- a/tests/test_zip.py +++ b/tests/test_zip.py @@ -1,7 +1,6 @@ import sys import unittest -from . import fixtures from importlib_metadata import ( PackageNotFoundError, distribution, @@ -11,6 +10,8 @@ version, ) +from . import fixtures + class TestZip(fixtures.ZipFixtures, unittest.TestCase): def setUp(self): From d968f6270d55f27a10491344a22e9e0fd77b5583 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:40:57 -0400 Subject: [PATCH 017/176] Remove superfluous parentheses. --- 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 84d16508..f6477999 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1108,7 +1108,7 @@ def _get_toplevel_name(name: PackagePath) -> str: # Defer import of inspect for performance (python/cpython#118761) import inspect - return _topmost(name) or (inspect.getmodulename(name) or str(name)) + return _topmost(name) or inspect.getmodulename(name) or str(name) def _top_level_inferred(dist): From 56b61b3dd90df2dba2da445a8386029b54fdebf3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:42:29 -0400 Subject: [PATCH 018/176] Rely on zipp overlay for zipfile.Path. --- importlib_metadata/__init__.py | 4 ++-- newsfragments/+d7832466.feature.rst | 1 + pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 newsfragments/+d7832466.feature.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index f6477999..b75befa3 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -20,7 +20,7 @@ from itertools import starmap from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast -import zipp +from zipp.compat.overlay import zipfile from . import _meta from ._collections import FreezableDefaultDict, Pair @@ -768,7 +768,7 @@ def children(self): return [] def zip_children(self): - zip_path = zipp.Path(self.root) + zip_path = zipfile.Path(self.root) names = zip_path.root.namelist() self.joinpath = zip_path.joinpath diff --git a/newsfragments/+d7832466.feature.rst b/newsfragments/+d7832466.feature.rst new file mode 100644 index 00000000..3f3f3162 --- /dev/null +++ b/newsfragments/+d7832466.feature.rst @@ -0,0 +1 @@ +Rely on zipp overlay for zipfile.Path. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 24ce25e3..8c45cc9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ ] requires-python = ">=3.8" dependencies = [ - "zipp>=0.5", + "zipp>=3.20", 'typing-extensions>=3.6.4; python_version < "3.8"', ] dynamic = ["version"] From f1350e413775a9e79e20779cc9705e28a1c55900 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 07:05:32 -0400 Subject: [PATCH 019/176] Add upstream and local sections for 'type' extra, since many projects will have 'types-*' dependencies. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 31057d85..3866a323 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,10 @@ enabler = [ ] type = [ + # upstream "pytest-mypy", + + # local ] From 6c0b7b37d94650ab5c59464615f56e3720cd3d56 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 18:30:07 -0400 Subject: [PATCH 020/176] =?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 Ref jaraco/pytest-perf#16 --- mypy.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy.ini b/mypy.ini index 83b0d15c..8e00827c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,3 +12,7 @@ explicit_package_bases = True # Disable overload-overlap due to many false-positives disable_error_code = overload-overlap + +# jaraco/pytest-perf#16 +[mypy-pytest_perf.*] +ignore_missing_imports = True From 4551c19215511b192fb3e5253ed9b05d7aa6c821 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 19:20:02 -0400 Subject: [PATCH 021/176] =?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 Ref jaraco/zipp#123 --- mypy.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy.ini b/mypy.ini index 8e00827c..9f476547 100644 --- a/mypy.ini +++ b/mypy.ini @@ -16,3 +16,7 @@ disable_error_code = overload-overlap # jaraco/pytest-perf#16 [mypy-pytest_perf.*] ignore_missing_imports = True + +# jaraco/zipp#123 +[mypy-zipp.*] +ignore_missing_imports = True From d4aa5054a9818aeaa73174fd69ca21c0bb6a3fad Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 19:26:42 -0400 Subject: [PATCH 022/176] =?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 | 4 ++-- tests/fixtures.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index b75befa3..3f1d942e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -331,7 +331,7 @@ class PackagePath(pathlib.PurePosixPath): size: int dist: Distribution - def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override] + def read_text(self, encoding: str = 'utf-8') -> str: return self.locate().read_text(encoding=encoding) def read_binary(self) -> bytes: @@ -750,7 +750,7 @@ class FastPath: True """ - @functools.lru_cache() # type: ignore + @functools.lru_cache() # type: ignore[misc] def __new__(cls, root): return super().__new__(cls) diff --git a/tests/fixtures.py b/tests/fixtures.py index 410e6f17..6708a063 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -13,12 +13,12 @@ from .compat.py312 import import_helper try: - from importlib import resources # type: ignore + from importlib import resources getattr(resources, 'files') getattr(resources, 'as_file') except (ImportError, AttributeError): - import importlib_resources as resources # type: ignore + import importlib_resources as resources # type: ignore[import-not-found, no-redef] @contextlib.contextmanager From 8484eccd39bda1ead5d8fec8cd967004f640a81f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 19:27:40 -0400 Subject: [PATCH 023/176] =?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 Re-sync jaraco.path 3.7.1. --- tests/_path.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/_path.py b/tests/_path.py index 7e4f7ee0..81ab76ac 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -1,4 +1,4 @@ -# from jaraco.path 3.7 +# from jaraco.path 3.7.1 import functools import pathlib @@ -11,7 +11,7 @@ class Symlink(str): """ -FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore +FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] @runtime_checkable @@ -28,12 +28,12 @@ def symlink_to(self, target): ... # pragma: no cover def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: - return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] def build( spec: FilesSpec, - prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore + prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] ): """ Build a set of files/directories, as described by the spec. @@ -67,7 +67,7 @@ def build( @functools.singledispatch def create(content: Union[str, bytes, FilesSpec], path): path.mkdir(exist_ok=True) - build(content, prefix=path) # type: ignore + build(content, prefix=path) # type: ignore[arg-type] @create.register From 0666d9da54886a68488d93a9907943e95fa2d1d4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 19:30:53 -0400 Subject: [PATCH 024/176] =?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 Ref jaraco/jaraco.test#7 --- mypy.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy.ini b/mypy.ini index 9f476547..786d03f4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -20,3 +20,7 @@ ignore_missing_imports = True # jaraco/zipp#123 [mypy-zipp.*] ignore_missing_imports = True + +# jaraco/jaraco.test#7 +[mypy-jaraco.test.*] +ignore_missing_imports = True From 6f8cc1ef4bf403e8c01e90230b6b2c6fc532179c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 19:37:14 -0400 Subject: [PATCH 025/176] =?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 --- tests/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 6708a063..7c9740d9 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -18,7 +18,7 @@ getattr(resources, 'files') getattr(resources, 'as_file') except (ImportError, AttributeError): - import importlib_resources as resources # type: ignore[import-not-found, no-redef] + import importlib_resources as resources # type: ignore[import-not-found, no-redef, unused-ignore] @contextlib.contextmanager From a55f01c752984538435f4baea1efc3ac58b5f006 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Aug 2024 09:42:05 -0400 Subject: [PATCH 026/176] Add reference to development methodology. Ref python/cpython#123144 --- importlib_metadata/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 3f1d942e..6dfaad0c 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1,3 +1,12 @@ +""" +APIs exposing metadata from third-party Python packages. + +This codebase is shared between importlib.metadata in the stdlib +and importlib_metadata in PyPI. See +https://github.com/python/importlib_metadata/wiki/Development-Methodology +for more detail. +""" + from __future__ import annotations import abc From 57b8aa81d77416805dcaaa22d5d45fef3e8b331c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 25 Aug 2024 09:29:10 +0100 Subject: [PATCH 027/176] Add `--fix` flag to ruff pre-commit hook for automatic suggestion of fixes (jaraco/skeleton#140) * Add `--fix` flag to ruff pre-commit hook for automatic suggestion of fixes. This is documented in https://github.com/astral-sh/ruff-pre-commit?tab=readme-ov-file#using-ruff-with-pre-commit and should be safe to apply, because it requires the developer to "manually approve" the suggested changes via `git add`. * Add --unsafe-fixes to ruff pre-commit hoot --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff54405e..8ec58e22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,4 +3,5 @@ repos: rev: v0.5.6 hooks: - id: ruff + args: [--fix, --unsafe-fixes] - id: ruff-format From d3e83beaec3bdf4a628f2f0ae0a52d21c84e346f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Aug 2024 06:33:23 -0400 Subject: [PATCH 028/176] Disable mypy for now. Ref jaraco/skeleton#143 --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3866a323..1d81b1cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,5 +64,8 @@ type = [ ] - [tool.setuptools_scm] + + +[tool.pytest-enabler.mypy] +# Disabled due to jaraco/skeleton#143 From 3fcabf10b810c8585b858fb81fc3cd8c5efe898d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Aug 2024 13:26:38 -0400 Subject: [PATCH 029/176] Move overload-overlap disablement to its own line for easier diffs and simpler relevant comments. Ref #142 --- mypy.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy.ini b/mypy.ini index 83b0d15c..2806c330 100644 --- a/mypy.ini +++ b/mypy.ini @@ -10,5 +10,6 @@ enable_error_code = ignore-without-code # Support namespace packages per https://github.com/python/mypy/issues/14057 explicit_package_bases = True -# Disable overload-overlap due to many false-positives -disable_error_code = overload-overlap +disable_error_code = + # Disable due to many false positives + overload-overlap From 04c69839d11588f9f6b1e7bfb868d53f48ee52bd Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 26 Aug 2024 16:53:06 -0400 Subject: [PATCH 030/176] Pass mypy and link issues --- importlib_metadata/__init__.py | 4 ++-- pyproject.toml | 4 ---- tests/_path.py | 7 ++++--- tests/fixtures.py | 9 +++------ 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6dfaad0c..9d8fb980 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -66,7 +66,7 @@ def __str__(self) -> str: return f"No package metadata was found for {self.name}" @property - def name(self) -> str: # type: ignore[override] + def name(self) -> str: # type: ignore[override] # make readonly (name,) = self.args return name @@ -284,7 +284,7 @@ class EntryPoints(tuple): __slots__ = () - def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] + def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] # Work with str instead of int """ Get the EntryPoint in self matching name. """ diff --git a/pyproject.toml b/pyproject.toml index c00be7ed..2e5e40c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,3 @@ type = [ [tool.setuptools_scm] - - -[tool.pytest-enabler.mypy] -# Disabled due to jaraco/skeleton#143 diff --git a/tests/_path.py b/tests/_path.py index 81ab76ac..30a6c15d 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -28,12 +28,12 @@ def symlink_to(self, target): ... # pragma: no cover def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: - return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] # jaraco/jaraco.path#4 def build( spec: FilesSpec, - prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] + prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] # jaraco/jaraco.path#4 ): """ Build a set of files/directories, as described by the spec. @@ -67,7 +67,8 @@ def build( @functools.singledispatch def create(content: Union[str, bytes, FilesSpec], path): path.mkdir(exist_ok=True) - build(content, prefix=path) # type: ignore[arg-type] + # Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union + build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727 @create.register diff --git a/tests/fixtures.py b/tests/fixtures.py index 7c9740d9..8e692f86 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -12,13 +12,10 @@ from .compat.py39 import os_helper from .compat.py312 import import_helper -try: +if sys.version_info >= (3, 9): from importlib import resources - - getattr(resources, 'files') - getattr(resources, 'as_file') -except (ImportError, AttributeError): - import importlib_resources as resources # type: ignore[import-not-found, no-redef, unused-ignore] +else: + import importlib_resources as resources @contextlib.contextmanager From 76eebe0eb4632d7658f4fdaac119211313684f8b Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Tue, 27 Aug 2024 15:31:57 +0100 Subject: [PATCH 031/176] Defer import of zipp --- importlib_metadata/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6dfaad0c..e9af4972 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -29,8 +29,6 @@ from itertools import starmap from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast -from zipp.compat.overlay import zipfile - from . import _meta from ._collections import FreezableDefaultDict, Pair from ._compat import ( @@ -777,6 +775,8 @@ def children(self): return [] def zip_children(self): + from zipp.compat.overlay import zipfile + zip_path = zipfile.Path(self.root) names = zip_path.root.namelist() self.joinpath = zip_path.joinpath From e0b4f09ed38845178d1bc5a2d8cb3d13ba69740f Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Tue, 27 Aug 2024 15:49:58 +0100 Subject: [PATCH 032/176] Defer json import --- importlib_metadata/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index e9af4972..d1b0d92d 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -14,7 +14,6 @@ import email import functools import itertools -import json import operator import os import pathlib @@ -673,6 +672,8 @@ def origin(self): return self._load_json('direct_url.json') def _load_json(self, filename): + import json + return pass_none(json.loads)( self.read_text(filename), object_hook=lambda data: types.SimpleNamespace(**data), From 8436e8142685cc5378ecaea3311a6172ec8e34c0 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Tue, 27 Aug 2024 16:04:41 +0100 Subject: [PATCH 033/176] Defer platform import --- importlib_metadata/_compat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index 01356d69..e78ff59d 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -1,4 +1,3 @@ -import platform import sys __all__ = ['install', 'NullFinder'] @@ -52,5 +51,7 @@ def pypy_partial(val): Workaround for #327. """ + import platform + is_pypy = platform.python_implementation() == 'PyPy' return val + is_pypy From 0c326f3f77b2420163f73d97f8fbd090fa49147d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Aug 2024 13:13:06 -0400 Subject: [PATCH 034/176] Add a degenerate nitpick_ignore for downstream consumers. Add a 'local' comment to delineate where the skeleton ends and the downstream begins. --- docs/conf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 32150488..3d956a8c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', @@ -30,6 +33,7 @@ # Be strict about any broken references nitpicky = True +nitpick_ignore: list[tuple[str, str]] = [] # Include Python intersphinx mapping to prevent failures # jaraco/skeleton#51 @@ -40,3 +44,5 @@ # Preserve authored syntax for defaults autodoc_preserve_defaults = True + +# local From cae15af2d0c34fca473bf5425c23da378d1b0419 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 29 Aug 2024 15:25:16 -0400 Subject: [PATCH 035/176] Update tests/_path.py with jaraco.path 3.7.2 --- tests/_path.py | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/tests/_path.py b/tests/_path.py index 81ab76ac..c66cf5f8 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -1,8 +1,13 @@ -# from jaraco.path 3.7.1 +# from jaraco.path 3.7.2 + +from __future__ import annotations import functools import pathlib -from typing import Dict, Protocol, Union, runtime_checkable +from typing import TYPE_CHECKING, Mapping, Protocol, Union, runtime_checkable + +if TYPE_CHECKING: + from typing_extensions import Self class Symlink(str): @@ -11,29 +16,25 @@ class Symlink(str): """ -FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] +FilesSpec = Mapping[str, Union[str, bytes, Symlink, 'FilesSpec']] @runtime_checkable class TreeMaker(Protocol): - def __truediv__(self, *args, **kwargs): ... # pragma: no cover - - def mkdir(self, **kwargs): ... # pragma: no cover - - def write_text(self, content, **kwargs): ... # pragma: no cover - - def write_bytes(self, content): ... # pragma: no cover - - def symlink_to(self, target): ... # pragma: no cover + def __truediv__(self, other, /) -> Self: ... + def mkdir(self, *, exist_ok) -> object: ... + def write_text(self, content, /, *, encoding) -> object: ... + def write_bytes(self, content, /) -> object: ... + def symlink_to(self, target, /) -> object: ... -def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: - return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] +def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker: + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) def build( spec: FilesSpec, - prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] + prefix: str | TreeMaker = pathlib.Path(), ): """ Build a set of files/directories, as described by the spec. @@ -65,23 +66,24 @@ def build( @functools.singledispatch -def create(content: Union[str, bytes, FilesSpec], path): +def create(content: str | bytes | FilesSpec, path: TreeMaker) -> None: path.mkdir(exist_ok=True) - build(content, prefix=path) # type: ignore[arg-type] + # Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union + build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727 @create.register -def _(content: bytes, path): +def _(content: bytes, path: TreeMaker) -> None: path.write_bytes(content) @create.register -def _(content: str, path): +def _(content: str, path: TreeMaker) -> None: path.write_text(content, encoding='utf-8') @create.register -def _(content: Symlink, path): +def _(content: Symlink, path: TreeMaker) -> None: path.symlink_to(content) From 2beb8b0c9d0f7046370e7c58c4e6baaf35154a16 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Aug 2024 16:26:28 -0400 Subject: [PATCH 036/176] Add support for linking usernames. Closes jaraco/skeleton#144 --- docs/conf.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 3d956a8c..d5745d62 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,4 +45,13 @@ # Preserve authored syntax for defaults autodoc_preserve_defaults = True +# Add support for linking usernames, PyPI projects, Wikipedia pages +github_url = 'https://github.com/' +extlinks = { + 'user': (f'{github_url}%s', '@%s'), + 'pypi': ('https://pypi.org/project/%s', '%s'), + 'wiki': ('https://wikipedia.org/wiki/%s', '%s'), +} +extensions += ['sphinx.ext.extlinks'] + # local From 790fa6e6feb9a93d39135494819b12e9df8a7bba Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Aug 2024 16:53:52 -0400 Subject: [PATCH 037/176] Include the trailing slash in disable_error_code(overload-overlap), also required for clean diffs. Ref jaraco/skeleton#142 --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 2806c330..efcb8cbc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,4 +12,4 @@ explicit_package_bases = True disable_error_code = # Disable due to many false positives - overload-overlap + overload-overlap, From 1a6e38c0bfccd18a01deaca1491bcde3e778404c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 31 Aug 2024 05:50:38 -0400 Subject: [PATCH 038/176] Remove workaround for sphinx-contrib/sphinx-lint#83 --- tox.ini | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 01f0975f..14243051 100644 --- a/tox.ini +++ b/tox.ini @@ -31,9 +31,7 @@ extras = changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html - python -m sphinxlint \ - # workaround for sphinx-contrib/sphinx-lint#83 - --jobs 1 + python -m sphinxlint [testenv:finalize] description = assemble changelog and tag a release From a675458e1a7d6ae81d0d441338a74dc98ffc5a61 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 7 Sep 2024 10:16:01 -0400 Subject: [PATCH 039/176] Allow the workflow to be triggered manually. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ac0ff69e..441b93ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,7 @@ on: # required if branches-ignore is supplied (jaraco/skeleton#103) - '**' pull_request: + workflow_dispatch: permissions: contents: read From d11b67fed9f21503ca369e33c917a8038994ce0b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 09:58:23 -0400 Subject: [PATCH 040/176] Add comment to protect the deferred import. --- importlib_metadata/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index e9af4972..9b912f8e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -775,6 +775,7 @@ def children(self): return [] def zip_children(self): + # deferred for performance (python/importlib_metadata#502) from zipp.compat.overlay import zipfile zip_path = zipfile.Path(self.root) From e3ce33b45e572824b482049570cac13da543999b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 09:59:03 -0400 Subject: [PATCH 041/176] Add news fragment. --- newsfragments/502.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/502.feature.rst diff --git a/newsfragments/502.feature.rst b/newsfragments/502.feature.rst new file mode 100644 index 00000000..20659bb8 --- /dev/null +++ b/newsfragments/502.feature.rst @@ -0,0 +1 @@ +Deferred import of zipfile.Path \ No newline at end of file From 18eb2da0ee267394c1735bec5b1d9f2b0fa77dd9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 10:13:51 -0400 Subject: [PATCH 042/176] Revert "Defer platform import" This reverts commit 8436e8142685cc5378ecaea3311a6172ec8e34c0. --- importlib_metadata/_compat.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index e78ff59d..01356d69 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -1,3 +1,4 @@ +import platform import sys __all__ = ['install', 'NullFinder'] @@ -51,7 +52,5 @@ def pypy_partial(val): Workaround for #327. """ - import platform - is_pypy = platform.python_implementation() == 'PyPy' return val + is_pypy From 3f78dc17786e0e0290db450e843ac494af0158e9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 10:15:30 -0400 Subject: [PATCH 043/176] Add comment to protect the deferred import. --- importlib_metadata/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index d1b0d92d..58bbd95e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -672,6 +672,7 @@ def origin(self): return self._load_json('direct_url.json') def _load_json(self, filename): + # Deferred for performance (python/importlib_metadata#503) import json return pass_none(json.loads)( From 2a3f50d8bbd41fc831676e7dc89d84c605c85760 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 10:15:46 -0400 Subject: [PATCH 044/176] Add news fragment. --- newsfragments/503.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/503.feature.rst diff --git a/newsfragments/503.feature.rst b/newsfragments/503.feature.rst new file mode 100644 index 00000000..7e6bdc7a --- /dev/null +++ b/newsfragments/503.feature.rst @@ -0,0 +1 @@ +Deferred import of json \ No newline at end of file From afa39e8e08b48fbedd3b8ac94cf58de39ff09c35 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 10:30:18 -0400 Subject: [PATCH 045/176] Back out changes to tests._path --- tests/_path.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/_path.py b/tests/_path.py index 30a6c15d..81ab76ac 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -28,12 +28,12 @@ def symlink_to(self, target): ... # pragma: no cover def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: - return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] # jaraco/jaraco.path#4 + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] def build( spec: FilesSpec, - prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] # jaraco/jaraco.path#4 + prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] ): """ Build a set of files/directories, as described by the spec. @@ -67,8 +67,7 @@ def build( @functools.singledispatch def create(content: Union[str, bytes, FilesSpec], path): path.mkdir(exist_ok=True) - # Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union - build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727 + build(content, prefix=path) # type: ignore[arg-type] @create.register From b34810b1e0665580a91ea19b6317a1890ecd42c1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 10:50:50 -0400 Subject: [PATCH 046/176] Finalize --- NEWS.rst | 11 +++++++++++ newsfragments/+d7832466.feature.rst | 1 - newsfragments/502.feature.rst | 1 - newsfragments/503.feature.rst | 1 - 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 newsfragments/+d7832466.feature.rst delete mode 100644 newsfragments/502.feature.rst delete mode 100644 newsfragments/503.feature.rst diff --git a/NEWS.rst b/NEWS.rst index f4a37fdf..4e75f3b0 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,14 @@ +v8.5.0 +====== + +Features +-------- + +- Deferred import of zipfile.Path (#502) +- Deferred import of json (#503) +- Rely on zipp overlay for zipfile.Path. + + v8.4.0 ====== diff --git a/newsfragments/+d7832466.feature.rst b/newsfragments/+d7832466.feature.rst deleted file mode 100644 index 3f3f3162..00000000 --- a/newsfragments/+d7832466.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Rely on zipp overlay for zipfile.Path. \ No newline at end of file diff --git a/newsfragments/502.feature.rst b/newsfragments/502.feature.rst deleted file mode 100644 index 20659bb8..00000000 --- a/newsfragments/502.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Deferred import of zipfile.Path \ No newline at end of file diff --git a/newsfragments/503.feature.rst b/newsfragments/503.feature.rst deleted file mode 100644 index 7e6bdc7a..00000000 --- a/newsfragments/503.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Deferred import of json \ No newline at end of file From 20295ece8189459ac6d69273026fa4bc3f9a996b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 14:26:07 -0400 Subject: [PATCH 047/176] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index bb19889b..c04545cd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,5 @@ from __future__ import annotations - extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', From db14d96503b8d9c3af49fa14e2347838de85b1ef Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 13:59:16 -0400 Subject: [PATCH 048/176] Ensure redent is idempotent (doesn't add 8 spaces to already dedented values). --- importlib_metadata/_adapters.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index 3b516a2d..4e660938 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -57,9 +57,10 @@ def __getitem__(self, item): def _repair_headers(self): def redent(value): "Correct for RFC822 indentation" - if not value or '\n' not in value: + indent = ' ' * 8 + if not value or '\n' + indent not in value: return value - return textwrap.dedent(' ' * 8 + value) + return textwrap.dedent(indent + value) headers = [(key, redent(value)) for key, value in vars(self)['_headers']] if self._payload: From 81b766c06cc83679c4a04c2bfa6d2c8cc559bf33 Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 11 Sep 2024 18:14:38 -0400 Subject: [PATCH 049/176] Fix an incompatibility (and source of merge conflicts) with projects using Ruff/isort. Remove extra line after imports in conf.py (jaraco/skeleton#147) --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d5745d62..240329c3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,5 @@ from __future__ import annotations - extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', From 3fe8c5ba792fd58a5a24eef4e8a845f3b5dd6c2c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 18:14:58 -0400 Subject: [PATCH 050/176] Add Python 3.13 and 3.14 into the matrix. (jaraco/skeleton#146) --- .github/workflows/main.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 441b93ef..251b9c1d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,7 +36,7 @@ jobs: matrix: python: - "3.8" - - "3.12" + - "3.13" platform: - ubuntu-latest - macos-latest @@ -48,10 +48,14 @@ jobs: platform: ubuntu-latest - python: "3.11" platform: ubuntu-latest + - python: "3.12" + platform: ubuntu-latest + - python: "3.14" + platform: ubuntu-latest - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.13' }} + continue-on-error: ${{ matrix.python == '3.14' }} steps: - uses: actions/checkout@v4 - name: Setup Python From 90073b1aa7a49cc5fdbdc0e6e871f39e461b9422 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 12 Sep 2024 05:01:16 -0400 Subject: [PATCH 051/176] Separate bpo from Python issue numbers. --- docs/conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index c04545cd..32528f86 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,9 +27,13 @@ url='https://peps.python.org/pep-{pep_number:0>4}/', ), dict( - pattern=r'(python/cpython#|Python #|py-)(?P\d+)', + pattern=r'(python/cpython#|Python #)(?P\d+)', url='https://github.com/python/cpython/issues/{python}', ), + dict( + pattern=r'bpo-(?P\d+)', + url='http://bugs.python.org/issue{bpo}', + ), ], ) } From 62b6678a32087ed3bfc8ff19761764340295834e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 26 Oct 2024 00:12:59 +0100 Subject: [PATCH 052/176] Bump pre-commit hook for ruff to avoid clashes with pytest-ruff (jaraco/skeleton#150) --- .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 8ec58e22..04870d16 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.5.6 + rev: v0.7.1 hooks: - id: ruff args: [--fix, --unsafe-fixes] From db4dfc495552aca8d6f05ed58441fa65fdc2ed9c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2024 09:11:52 -0700 Subject: [PATCH 053/176] Add Python 3.13 and 3.14 into the matrix. (jaraco/skeleton#151) From e61a9df7cdc9c8d1b56c30b7b3f94a7cdac14414 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2024 12:19:31 -0400 Subject: [PATCH 054/176] Include pyproject.toml in ruff.toml. Closes jaraco/skeleton#119. Workaround for astral-sh/ruff#10299. --- ruff.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruff.toml b/ruff.toml index 922aa1f1..8b22940a 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,3 +1,6 @@ +# include pyproject.toml for requires-python (workaround astral-sh/ruff#10299) +include = "pyproject.toml" + [lint] extend-select = [ "C901", From 750a1891ec4a1c0602050e3463e9593a8c13aa14 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2024 12:22:50 -0400 Subject: [PATCH 055/176] Require Python 3.9 or later now that Python 3.8 is EOL. --- .github/workflows/main.yml | 4 +--- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 251b9c1d..9c01fc4d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,15 +35,13 @@ jobs: # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - - "3.8" + - "3.9" - "3.13" platform: - ubuntu-latest - macos-latest - windows-latest include: - - python: "3.9" - platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - python: "3.11" diff --git a/pyproject.toml b/pyproject.toml index 1d81b1cc..328b98cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ ] dynamic = ["version"] From 5c34e69568f23a524af4fa9dad3f5e80f22ec3e6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Nov 2024 22:35:31 -0400 Subject: [PATCH 056/176] Use extend for proper workaround. Closes jaraco/skeleton#152 Workaround for astral-sh/ruff#10299 --- ruff.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ruff.toml b/ruff.toml index 8b22940a..9379d6e1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ -# include pyproject.toml for requires-python (workaround astral-sh/ruff#10299) -include = "pyproject.toml" +# extend pyproject.toml for requires-python (workaround astral-sh/ruff#10299) +extend = "pyproject.toml" [lint] extend-select = [ From 39a607d25def76ef760334a494554847da8c8f0f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 10:23:13 -0500 Subject: [PATCH 057/176] Bump badge for 2025. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index efabeee4..4d3cabee 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-2024-informational +.. image:: https://img.shields.io/badge/skeleton-2025-informational :target: https://blog.jaraco.com/skeleton From 07aa607e54672bc1077007771cbca0e16475aa24 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Jan 2025 16:55:08 -0500 Subject: [PATCH 058/176] Add support for rendering metadata where some fields have newlines (python/cpython#119650). --- importlib_metadata/_adapters.py | 46 +++++++++++++++++++++++++++++ newsfragments/+f9f493e6.feature.rst | 1 + 2 files changed, 47 insertions(+) create mode 100644 newsfragments/+f9f493e6.feature.rst diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index 4e660938..f6763aa7 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -1,11 +1,54 @@ import email.message +import email.policy import re import textwrap from ._text import FoldedCase +class RawPolicy(email.policy.EmailPolicy): + def fold(self, name, value): + folded = self.linesep.join( + textwrap.indent(value, prefix=' ' * 8).lstrip().splitlines() + ) + return f'{name}: {folded}{self.linesep}' + + class Message(email.message.Message): + r""" + Specialized Message subclass to handle metadata naturally. + + Reads values that may have newlines in them and converts the + payload to the Description. + + >>> msg_text = textwrap.dedent(''' + ... Name: Foo + ... Version: 3.0 + ... License: blah + ... de-blah + ... + ... First line of description. + ... Second line of description. + ... ''').lstrip().replace('', '') + >>> msg = Message(email.message_from_string(msg_text)) + >>> msg['Description'] + 'First line of description.\nSecond line of description.\n' + + Message should render even if values contain newlines. + + >>> print(msg) + Name: Foo + Version: 3.0 + License: blah + de-blah + Description: First line of description. + Second line of description. + + First line of description. + Second line of description. + + """ + multiple_use_keys = set( map( FoldedCase, @@ -67,6 +110,9 @@ def redent(value): headers.append(('Description', self.get_payload())) return headers + def as_string(self): + return super().as_string(policy=RawPolicy()) + @property def json(self): """ diff --git a/newsfragments/+f9f493e6.feature.rst b/newsfragments/+f9f493e6.feature.rst new file mode 100644 index 00000000..14b812b9 --- /dev/null +++ b/newsfragments/+f9f493e6.feature.rst @@ -0,0 +1 @@ +Add support for rendering metadata where some fields have newlines (python/cpython#119650). From dab1dd81507fef5bff6a32a63ab8fc8633e2c24c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Jan 2025 17:00:01 -0500 Subject: [PATCH 059/176] When transforming the payload to a Description key, clear the payload. --- importlib_metadata/_adapters.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index f6763aa7..ce4a2fe8 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -44,8 +44,6 @@ class Message(email.message.Message): Description: First line of description. Second line of description. - First line of description. - Second line of description. """ @@ -108,6 +106,7 @@ def redent(value): headers = [(key, redent(value)) for key, value in vars(self)['_headers']] if self._payload: headers.append(('Description', self.get_payload())) + self.set_payload('') return headers def as_string(self): From dad738095d6d28f24d254f3cc9f82e2394fb2a09 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Jan 2025 17:00:47 -0500 Subject: [PATCH 060/176] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+f9f493e6.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+f9f493e6.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 4e75f3b0..1998ebcb 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.6.0 +====== + +Features +-------- + +- Add support for rendering metadata where some fields have newlines (python/cpython#119650). + + v8.5.0 ====== diff --git a/newsfragments/+f9f493e6.feature.rst b/newsfragments/+f9f493e6.feature.rst deleted file mode 100644 index 14b812b9..00000000 --- a/newsfragments/+f9f493e6.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add support for rendering metadata where some fields have newlines (python/cpython#119650). From 506beb7cbd23fef4f93a63a5c3931e302d828896 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Jan 2025 17:15:33 -0500 Subject: [PATCH 061/176] Fixed indentation logic to also honor blank lines. --- importlib_metadata/_adapters.py | 10 ++++++++-- newsfragments/+bd6aec5a.bugfix.rst | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 newsfragments/+bd6aec5a.bugfix.rst diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index ce4a2fe8..f5b30dd9 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -9,7 +9,9 @@ class RawPolicy(email.policy.EmailPolicy): def fold(self, name, value): folded = self.linesep.join( - textwrap.indent(value, prefix=' ' * 8).lstrip().splitlines() + textwrap.indent(value, prefix=' ' * 8, predicate=lambda line: True) + .lstrip() + .splitlines() ) return f'{name}: {folded}{self.linesep}' @@ -29,10 +31,12 @@ class Message(email.message.Message): ... ... First line of description. ... Second line of description. + ... + ... Fourth line! ... ''').lstrip().replace('', '') >>> msg = Message(email.message_from_string(msg_text)) >>> msg['Description'] - 'First line of description.\nSecond line of description.\n' + 'First line of description.\nSecond line of description.\n\nFourth line!\n' Message should render even if values contain newlines. @@ -43,6 +47,8 @@ class Message(email.message.Message): de-blah Description: First line of description. Second line of description. + + Fourth line! """ diff --git a/newsfragments/+bd6aec5a.bugfix.rst b/newsfragments/+bd6aec5a.bugfix.rst new file mode 100644 index 00000000..ec1dd1e3 --- /dev/null +++ b/newsfragments/+bd6aec5a.bugfix.rst @@ -0,0 +1 @@ +Fixed indentation logic to also honor blank lines. From 45e8bde73f8ce816fdc47618a31ba3916a332485 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Jan 2025 17:15:43 -0500 Subject: [PATCH 062/176] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+bd6aec5a.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+bd6aec5a.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 1998ebcb..e5a4b397 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.6.1 +====== + +Bugfixes +-------- + +- Fixed indentation logic to also honor blank lines. + + v8.6.0 ====== diff --git a/newsfragments/+bd6aec5a.bugfix.rst b/newsfragments/+bd6aec5a.bugfix.rst deleted file mode 100644 index ec1dd1e3..00000000 --- a/newsfragments/+bd6aec5a.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed indentation logic to also honor blank lines. From aee344d781920bba42ddbee4b4b44af29d7bab6e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 Feb 2025 10:44:24 -0500 Subject: [PATCH 063/176] Removing dependabot config. Closes jaraco/skeleton#156 --- .github/dependabot.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 89ff3396..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "daily" - allow: - - dependency-type: "all" From 75ce9aba3ed9f4002fa01db0287dfdb1600fb635 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Feb 2025 18:57:40 -0500 Subject: [PATCH 064/176] Add support for building lxml on pre-release Pythons. Closes jaraco/skeleton#161 --- .github/workflows/main.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c01fc4d..5841cc37 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,6 +56,13 @@ jobs: continue-on-error: ${{ matrix.python == '3.14' }} 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' + run: | + sudo apt update + sudo apt install -y libxml2-dev libxslt-dev - name: Setup Python uses: actions/setup-python@v4 with: From 1c9467fdec1cc1456772cd71c7e740f048ce86fc Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 24 Feb 2025 22:00:11 +0000 Subject: [PATCH 065/176] Fix new mandatory configuration field for RTD (jaraco/skeleton#159) This field is now required and prevents the build from running if absent. Details in https://about.readthedocs.com/blog/2024/12/deprecate-config-files-without-sphinx-or-mkdocs-config/ --- .readthedocs.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index dc8516ac..72437063 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,6 +5,9 @@ python: extra_requirements: - doc +sphinx: + configuration: docs/conf.py + # required boilerplate readthedocs/readthedocs.org#10401 build: os: ubuntu-lts-latest From 1a2f93053d789f041d88c97c5da4eea9e949bdfe Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 25 Feb 2025 13:21:13 -0500 Subject: [PATCH 066/176] Select Ruff rules for modern type annotations (jaraco/skeleton#160) * Select Ruff rules for modern type annotations Ensure modern type annotation syntax and best practices Not including those covered by type-checkers or exclusive to Python 3.11+ Not including rules currently in preview either. These are the same set of rules I have in pywin32 as of https://github.com/mhammond/pywin32/pull/2458 setuptools has all the same rules enabled (except it also includes the `UP` group directly) * Add PYI011 ignore and #local section * Update ruff.toml Co-authored-by: Jason R. Coombs * Add # upstream --------- Co-authored-by: Jason R. Coombs --- ruff.toml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ruff.toml b/ruff.toml index 9379d6e1..1d65c7c2 100644 --- a/ruff.toml +++ b/ruff.toml @@ -3,11 +3,32 @@ extend = "pyproject.toml" [lint] extend-select = [ + # upstream + "C901", "PERF401", "W", + + # 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 + "PYI", # flake8-pyi + "UP006", # non-pep585-annotation + "UP007", # non-pep604-annotation + "UP010", # unnecessary-future-import + "UP035", # deprecated-import + "UP037", # quoted-annotation + "UP043", # unnecessary-default-type-args + + # local ] ignore = [ + # upstream + + # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, + # irrelevant to this project. + "PYI011", # typed-argument-default-in-stub # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", @@ -23,6 +44,8 @@ ignore = [ "COM819", "ISC001", "ISC002", + + # local ] [format] From aa891069099398fe2eb294ac4b781460d8c0a39b Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 26 Feb 2025 17:56:42 -0500 Subject: [PATCH 067/176] Consistent import sorting (isort) (jaraco/skeleton#157) --- ruff.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ruff.toml b/ruff.toml index 1d65c7c2..b52a6d7c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -5,9 +5,10 @@ extend = "pyproject.toml" extend-select = [ # upstream - "C901", - "PERF401", - "W", + "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 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 068/176] 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 069/176] 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 070/176] 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 071/176] 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 072/176] 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 073/176] 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 074/176] 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 d9fc620fd5d00b439397dc15f1acfdd6f583b770 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:56:23 -0400 Subject: [PATCH 075/176] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/__init__.py | 36 ++++++++++++++++++---------------- importlib_metadata/_meta.py | 20 ++++++++----------- tests/_path.py | 3 ++- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 46a14e64..87c9eb51 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -22,11 +22,13 @@ import sys import textwrap import types +from collections.abc import Iterable, Mapping from contextlib import suppress from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast +from re import Match +from typing import Any, List, Optional, Set, cast from . import _meta from ._collections import FreezableDefaultDict, Pair @@ -175,7 +177,7 @@ class EntryPoint: value: str group: str - dist: Optional[Distribution] = None + dist: Distribution | None = None def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) @@ -203,7 +205,7 @@ def attr(self) -> str: return match.group('attr') @property - def extras(self) -> List[str]: + def extras(self) -> list[str]: match = self.pattern.match(self.value) assert match is not None return re.findall(r'\w+', match.group('extras') or '') @@ -305,14 +307,14 @@ def select(self, **params) -> EntryPoints: return EntryPoints(ep for ep in self if py39.ep_matches(ep, **params)) @property - def names(self) -> Set[str]: + def names(self) -> set[str]: """ Return the set of all names of all entry points. """ return {ep.name for ep in self} @property - def groups(self) -> Set[str]: + def groups(self) -> set[str]: """ Return the set of all groups of all entry points. """ @@ -333,7 +335,7 @@ def _from_text(text): class PackagePath(pathlib.PurePosixPath): """A reference to a path in a package""" - hash: Optional[FileHash] + hash: FileHash | None size: int dist: Distribution @@ -368,7 +370,7 @@ class Distribution(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def read_text(self, filename) -> Optional[str]: + def read_text(self, filename) -> str | None: """Attempt to load metadata file given by the name. Python distribution metadata is organized by blobs of text @@ -428,7 +430,7 @@ def from_name(cls, name: str) -> Distribution: @classmethod def discover( - cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs + cls, *, context: DistributionFinder.Context | None = None, **kwargs ) -> Iterable[Distribution]: """Return an iterable of Distribution objects for all packages. @@ -524,7 +526,7 @@ def entry_points(self) -> EntryPoints: return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) @property - def files(self) -> Optional[List[PackagePath]]: + def files(self) -> list[PackagePath] | None: """Files in this distribution. :return: List of PackagePath for this distribution or None @@ -616,7 +618,7 @@ def _read_files_egginfo_sources(self): return text and map('"{}"'.format, text.splitlines()) @property - def requires(self) -> Optional[List[str]]: + def requires(self) -> list[str] | None: """Generated requirements specified for this Distribution""" reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() return reqs and list(reqs) @@ -722,7 +724,7 @@ def __init__(self, **kwargs): vars(self).update(kwargs) @property - def path(self) -> List[str]: + def path(self) -> list[str]: """ The sequence of directory path that a distribution finder should search. @@ -874,7 +876,7 @@ class Prepared: normalized = None legacy_normalized = None - def __init__(self, name: Optional[str]): + def __init__(self, name: str | None): self.name = name if name is None: return @@ -944,7 +946,7 @@ def __init__(self, path: SimplePath) -> None: """ self._path = path - def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]: + def read_text(self, filename: str | os.PathLike[str]) -> str | None: with suppress( FileNotFoundError, IsADirectoryError, @@ -1051,7 +1053,7 @@ def entry_points(**params) -> EntryPoints: return EntryPoints(eps).select(**params) -def files(distribution_name: str) -> Optional[List[PackagePath]]: +def files(distribution_name: str) -> list[PackagePath] | None: """Return a list of files for the named package. :param distribution_name: The name of the distribution package to query. @@ -1060,7 +1062,7 @@ def files(distribution_name: str) -> Optional[List[PackagePath]]: return distribution(distribution_name).files -def requires(distribution_name: str) -> Optional[List[str]]: +def requires(distribution_name: str) -> list[str] | None: """ Return a list of requirements for the named package. @@ -1070,7 +1072,7 @@ def requires(distribution_name: str) -> Optional[List[str]]: return distribution(distribution_name).requires -def packages_distributions() -> Mapping[str, List[str]]: +def packages_distributions() -> Mapping[str, list[str]]: """ Return a mapping of top-level packages to their distributions. @@ -1091,7 +1093,7 @@ def _top_level_declared(dist): return (dist.read_text('top_level.txt') or '').split() -def _topmost(name: PackagePath) -> Optional[str]: +def _topmost(name: PackagePath) -> str | None: """ Return the top-most parent as long as there is a parent. """ diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 0942bbd9..0c20eff3 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -1,15 +1,11 @@ from __future__ import annotations import os +from collections.abc import Iterator from typing import ( Any, - Dict, - Iterator, - List, - Optional, Protocol, TypeVar, - Union, overload, ) @@ -28,25 +24,25 @@ def __iter__(self) -> Iterator[str]: ... # pragma: no cover @overload def get( self, name: str, failobj: None = None - ) -> Optional[str]: ... # pragma: no cover + ) -> str | None: ... # pragma: no cover @overload - def get(self, name: str, failobj: _T) -> Union[str, _T]: ... # pragma: no cover + def get(self, name: str, failobj: _T) -> str | _T: ... # pragma: no cover # overload per python/importlib_metadata#435 @overload def get_all( self, name: str, failobj: None = None - ) -> Optional[List[Any]]: ... # pragma: no cover + ) -> list[Any] | None: ... # pragma: no cover @overload - def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]: + def get_all(self, name: str, failobj: _T) -> list[Any] | _T: """ Return all values associated with a possibly multi-valued key. """ @property - def json(self) -> Dict[str, Union[str, List[str]]]: + def json(self) -> dict[str, str | list[str]]: """ A JSON-compatible form of the metadata. """ @@ -58,11 +54,11 @@ class SimplePath(Protocol): """ def joinpath( - self, other: Union[str, os.PathLike[str]] + self, other: str | os.PathLike[str] ) -> SimplePath: ... # pragma: no cover def __truediv__( - self, other: Union[str, os.PathLike[str]] + self, other: str | os.PathLike[str] ) -> SimplePath: ... # pragma: no cover @property diff --git a/tests/_path.py b/tests/_path.py index c66cf5f8..e63d889f 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -4,7 +4,8 @@ import functools import pathlib -from typing import TYPE_CHECKING, Mapping, Protocol, Union, runtime_checkable +from collections.abc import Mapping +from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable if TYPE_CHECKING: from typing_extensions import Self From 75670d283f379bbe7072cf5ec8fe1f6c7703f9ea Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:58:01 -0400 Subject: [PATCH 076/176] Remove unused imports. --- 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 87c9eb51..275c7106 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -28,7 +28,7 @@ from importlib.abc import MetaPathFinder from itertools import starmap from re import Match -from typing import Any, List, Optional, Set, cast +from typing import Any, cast from . import _meta from ._collections import FreezableDefaultDict, Pair From 2bfbaf3bed463fc85646d5d57c04d257876844b5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:01:40 -0400 Subject: [PATCH 077/176] Prefer typing.NamedTuple --- importlib_metadata/_collections.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/_collections.py b/importlib_metadata/_collections.py index cf0954e1..fc5045d3 100644 --- a/importlib_metadata/_collections.py +++ b/importlib_metadata/_collections.py @@ -1,4 +1,5 @@ import collections +import typing # from jaraco.collections 3.3 @@ -24,7 +25,10 @@ def freeze(self): self._frozen = lambda key: self.default_factory() -class Pair(collections.namedtuple('Pair', 'name value')): +class Pair(typing.NamedTuple): + name: str + value: str + @classmethod def parse(cls, text): return cls(*map(str.strip, text.split("=", 1))) From c10bdf30dafb55ec471a289e751089255e7f281d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:02:50 -0400 Subject: [PATCH 078/176] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/compat/py39.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/compat/py39.py b/importlib_metadata/compat/py39.py index 1f15bd97..2592436d 100644 --- a/importlib_metadata/compat/py39.py +++ b/importlib_metadata/compat/py39.py @@ -2,7 +2,9 @@ Compatibility layer with Python 3.8/3.9 """ -from typing import TYPE_CHECKING, Any, Optional +from __future__ import annotations + +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. @@ -11,7 +13,7 @@ Distribution = EntryPoint = Any -def normalized_name(dist: Distribution) -> Optional[str]: +def normalized_name(dist: Distribution) -> str | None: """ Honor name normalization for distributions that don't provide ``_normalized_name``. """ From 55c6070ad7f337a423962698d3e02c62a8e1b10e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:38:56 -0400 Subject: [PATCH 079/176] Refactored parsing and handling of EntryPoint.value. --- importlib_metadata/__init__.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 275c7106..849ce068 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -27,7 +27,6 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from re import Match from typing import Any, cast from . import _meta @@ -135,6 +134,12 @@ def valid(line: str): return line and not line.startswith('#') +class _EntryPointMatch(types.SimpleNamespace): + module: str + attr: str + extras: str + + class EntryPoint: """An entry point as defined by Python packaging conventions. @@ -187,28 +192,27 @@ def load(self) -> Any: is indicated by the value, return that module. Otherwise, return the named object. """ - match = cast(Match, self.pattern.match(self.value)) - module = import_module(match.group('module')) - attrs = filter(None, (match.group('attr') or '').split('.')) + module = import_module(self.module) + attrs = filter(None, (self.attr or '').split('.')) return functools.reduce(getattr, attrs, module) @property def module(self) -> str: - match = self.pattern.match(self.value) - assert match is not None - return match.group('module') + return self._match.module @property def attr(self) -> str: - match = self.pattern.match(self.value) - assert match is not None - return match.group('attr') + return self._match.attr @property def extras(self) -> list[str]: + return re.findall(r'\w+', self._match.extras or '') + + @property + def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) assert match is not None - return re.findall(r'\w+', match.group('extras') or '') + return _EntryPointMatch(**match.groupdict()) def _for(self, dist): vars(self).update(dist=dist) From eae6a754d004e8ea72d5d07b7dc3733a6be71f1b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:41:26 -0400 Subject: [PATCH 080/176] Raise a ValueError if no match. Closes #488 --- importlib_metadata/__init__.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 849ce068..d527e403 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -155,6 +155,22 @@ class EntryPoint: 'attr' >>> ep.extras ['extra1', 'extra2'] + + If the value package or module are not valid identifiers, a + ValueError is raised on access. + + >>> EntryPoint(name=None, group=None, value='invalid-name').module + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + >>> EntryPoint(name=None, group=None, value='invalid-name').attr + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + >>> EntryPoint(name=None, group=None, value='invalid-name').extras + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... """ pattern = re.compile( @@ -211,7 +227,13 @@ def extras(self) -> list[str]: @property def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) - assert match is not None + if not match: + raise ValueError( + 'Invalid object reference. ' + 'See https://packaging.python.org' + '/en/latest/specifications/entry-points/#data-model', + self.value, + ) return _EntryPointMatch(**match.groupdict()) def _for(self, dist): From f179e28888b2c6caf12baaf5449ff1cd82513dfe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:45:56 -0400 Subject: [PATCH 081/176] Also raise ValueError on construction if the value is invalid. --- importlib_metadata/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index d527e403..ff3c2a44 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -171,6 +171,14 @@ class EntryPoint: Traceback (most recent call last): ... ValueError: ('Invalid object reference...invalid-name... + + The same thing happens on construction. + + >>> EntryPoint(name=None, group=None, value='invalid-name') + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + """ pattern = re.compile( @@ -202,6 +210,7 @@ class EntryPoint: def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) + self.module def load(self) -> Any: """Load the entry point from its definition. If only a module From 9f8af013635833cf3ac348413c9ac63b37caa3dd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:50:19 -0400 Subject: [PATCH 082/176] Prefer a cached property, as the property is likely to be retrieved at least 3 times (on construction and for module:attr access). --- 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 ff3c2a44..157b2c6f 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -233,7 +233,7 @@ def attr(self) -> str: def extras(self) -> list[str]: return re.findall(r'\w+', self._match.extras or '') - @property + @functools.cached_property def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) if not match: From 57f31d77e18fef11dfadfd44775f253971c36920 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 27 Jun 2024 12:53:04 -0400 Subject: [PATCH 083/176] Allow metadata to return None when there is no metadata present. --- importlib_metadata/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 157b2c6f..4717f3d7 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -511,7 +511,7 @@ def _discover_resolvers(): return filter(None, declared) @property - def metadata(self) -> _meta.PackageMetadata: + def metadata(self) -> _meta.PackageMetadata | None: """Return the parsed metadata for this Distribution. The returned object will have keys that name the various bits of @@ -521,10 +521,8 @@ def metadata(self) -> _meta.PackageMetadata: Custom providers may provide the METADATA file or override this property. """ - # deferred for performance (python/cpython#109829) - from . import _adapters - opt_text = ( + text = ( self.read_text('METADATA') or self.read_text('PKG-INFO') # This last clause is here to support old egg-info files. Its @@ -532,7 +530,14 @@ def metadata(self) -> _meta.PackageMetadata: # (which points to the egg-info file) attribute unchanged. or self.read_text('') ) - text = cast(str, opt_text) + return self._assemble_message(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)) @property From 22bb567692d8e7bd216f864a9d8dee1272ee8674 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:32:46 -0400 Subject: [PATCH 084/176] Fix type errors where metadata could be None. --- importlib_metadata/__init__.py | 8 ++++---- importlib_metadata/compat/py39.py | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 4717f3d7..ded27e13 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -543,7 +543,7 @@ def _assemble_message(text: str) -> _meta.PackageMetadata: @property def name(self) -> str: """Return the 'Name' metadata for the distribution package.""" - return self.metadata['Name'] + return cast(PackageMetadata, self.metadata)['Name'] @property def _normalized_name(self): @@ -553,7 +553,7 @@ def _normalized_name(self): @property def version(self) -> str: """Return the 'Version' metadata for the distribution package.""" - return self.metadata['Version'] + return cast(PackageMetadata, self.metadata)['Version'] @property def entry_points(self) -> EntryPoints: @@ -1050,7 +1050,7 @@ def distributions(**kwargs) -> Iterable[Distribution]: return Distribution.discover(**kwargs) -def metadata(distribution_name: str) -> _meta.PackageMetadata: +def metadata(distribution_name: str) -> _meta.PackageMetadata | None: """Get the metadata for the named package. :param distribution_name: The name of the distribution package to query. @@ -1125,7 +1125,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(dist.metadata['Name']) + pkg_to_dist[pkg].append(cast(PackageMetadata, dist.metadata)['Name']) return dict(pkg_to_dist) diff --git a/importlib_metadata/compat/py39.py b/importlib_metadata/compat/py39.py index 2592436d..1fbcbf7b 100644 --- a/importlib_metadata/compat/py39.py +++ b/importlib_metadata/compat/py39.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. @@ -12,6 +12,8 @@ else: Distribution = EntryPoint = Any +from .._meta import PackageMetadata + def normalized_name(dist: Distribution) -> str | None: """ @@ -22,7 +24,9 @@ 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 dist.metadata['Name']) + return Prepared.normalize( + getattr(dist, "name", None) or cast(PackageMetadata, dist.metadata)['Name'] + ) def ep_matches(ep: EntryPoint, **params) -> bool: From 0830c39b8a23e48024365120c0e97a6f7c36c5ec Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:34:40 -0400 Subject: [PATCH 085/176] Add news fragment. --- newsfragments/493.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/493.feature.rst diff --git a/newsfragments/493.feature.rst b/newsfragments/493.feature.rst new file mode 100644 index 00000000..e75e0e3e --- /dev/null +++ b/newsfragments/493.feature.rst @@ -0,0 +1 @@ +``.metadata()`` (and ``Distribution.metadata``) can now return ``None`` if the metadata directory exists but not metadata file is present. From 5a657051f7386de6f0560c200d78e941be2c8058 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:47:08 -0400 Subject: [PATCH 086/176] Refactor the casting into a wrapper for brevity and to document its purpose. --- importlib_metadata/__init__.py | 9 +++++---- importlib_metadata/_typing.py | 15 +++++++++++++++ importlib_metadata/compat/py39.py | 6 +++--- 3 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 importlib_metadata/_typing.py diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index ded27e13..cdfc1f62 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -27,7 +27,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import Any, cast +from typing import Any from . import _meta from ._collections import FreezableDefaultDict, Pair @@ -38,6 +38,7 @@ from ._functools import method_cache, pass_none from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath +from ._typing import md_none from .compat import py39, py311 __all__ = [ @@ -543,7 +544,7 @@ def _assemble_message(text: str) -> _meta.PackageMetadata: @property def name(self) -> str: """Return the 'Name' metadata for the distribution package.""" - return cast(PackageMetadata, self.metadata)['Name'] + return md_none(self.metadata)['Name'] @property def _normalized_name(self): @@ -553,7 +554,7 @@ def _normalized_name(self): @property def version(self) -> str: """Return the 'Version' metadata for the distribution package.""" - return cast(PackageMetadata, self.metadata)['Version'] + return md_none(self.metadata)['Version'] @property def entry_points(self) -> EntryPoints: @@ -1125,7 +1126,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(cast(PackageMetadata, dist.metadata)['Name']) + pkg_to_dist[pkg].append(md_none(dist.metadata)['Name']) return dict(pkg_to_dist) diff --git a/importlib_metadata/_typing.py b/importlib_metadata/_typing.py new file mode 100644 index 00000000..32b1d2b9 --- /dev/null +++ b/importlib_metadata/_typing.py @@ -0,0 +1,15 @@ +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 1fbcbf7b..3eb9c01e 100644 --- a/importlib_metadata/compat/py39.py +++ b/importlib_metadata/compat/py39.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. @@ -12,7 +12,7 @@ else: Distribution = EntryPoint = Any -from .._meta import PackageMetadata +from .._typing import md_none def normalized_name(dist: Distribution) -> str | None: @@ -25,7 +25,7 @@ def normalized_name(dist: Distribution) -> str | None: from .. import Prepared # -> delay to prevent circular imports. return Prepared.normalize( - getattr(dist, "name", None) or cast(PackageMetadata, dist.metadata)['Name'] + getattr(dist, "name", None) or md_none(dist.metadata)['Name'] ) From e4351c226765f53a40316fa6aab50488aee8a90f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:51:40 -0400 Subject: [PATCH 087/176] Add a new test capturing the new expectation. --- tests/test_main.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 7c9851fc..5ed08c89 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -155,6 +155,16 @@ def test_valid_dists_preferred(self): dist = Distribution.from_name('foo') assert dist.version == "1.0" + def test_missing_metadata(self): + """ + Dists with a missing metadata file should return None. + + Ref python/importlib_metadata#493. + """ + 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 + class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): @staticmethod From 708dff4f1ab89bdd126e3e8c56098d04282c5809 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 11:22:50 -0400 Subject: [PATCH 088/176] Finalize --- NEWS.rst | 15 +++++++++++++++ newsfragments/493.feature.rst | 1 - newsfragments/518.bugfix.rst | 1 - 3 files changed, 15 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/493.feature.rst delete mode 100644 newsfragments/518.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index e5a4b397..4d0c4bdc 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,18 @@ +v8.7.0 +====== + +Features +-------- + +- ``.metadata()`` (and ``Distribution.metadata``) can now return ``None`` if the metadata directory exists but not metadata file is present. (#493) + + +Bugfixes +-------- + +- Raise consistent ValueError for invalid EntryPoint.value (#518) + + v8.6.1 ====== diff --git a/newsfragments/493.feature.rst b/newsfragments/493.feature.rst deleted file mode 100644 index e75e0e3e..00000000 --- a/newsfragments/493.feature.rst +++ /dev/null @@ -1 +0,0 @@ -``.metadata()`` (and ``Distribution.metadata``) can now return ``None`` if the metadata directory exists but not metadata file is present. diff --git a/newsfragments/518.bugfix.rst b/newsfragments/518.bugfix.rst deleted file mode 100644 index 416071f7..00000000 --- a/newsfragments/518.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Raise consistent ValueError for invalid EntryPoint.value From 9a81db3c77bc106017dcd4b0853a5a94f43ae33c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 3 May 2025 03:57:47 -0400 Subject: [PATCH 089/176] 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 090/176] 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 091/176] 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 092/176] 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 093/176] 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 094/176] 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 095/176] 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 096/176] 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 097/176] 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 098/176] 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 099/176] 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 100/176] 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 101/176] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(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 102/176] 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 103/176] 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 104/176] 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 105/176] 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 106/176] 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 107/176] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(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 108/176] =?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 109/176] 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 110/176] 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 111/176] 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 112/176] 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 113/176] 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 114/176] 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 115/176] 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 116/176] 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 117/176] 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 118/176] 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 119/176] 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 120/176] 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 121/176] 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 122/176] 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 123/176] 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 124/176] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(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 125/176] 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 126/176] 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 127/176] 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 128/176] 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 129/176] 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 130/176] 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 131/176] 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 132/176] 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 133/176] 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 134/176] 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 135/176] 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 136/176] 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 137/176] 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 138/176] 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 c7ac18edb9a48be8a5c594a3692a4170f426b1e6 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Tue, 24 Feb 2026 00:51:03 +0100 Subject: [PATCH 139/176] gh-110937: Document full public importlib.metadata.Distribution API (#143480) --- .../Documentation/2026-01-06-16-04-08.gh-issue-110937.SyO5lk.rst | 1 + importlib_metadata/__init__.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Documentation/2026-01-06-16-04-08.gh-issue-110937.SyO5lk.rst diff --git a/Misc/NEWS.d/next/Documentation/2026-01-06-16-04-08.gh-issue-110937.SyO5lk.rst b/Misc/NEWS.d/next/Documentation/2026-01-06-16-04-08.gh-issue-110937.SyO5lk.rst new file mode 100644 index 00000000..d29bde5c --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2026-01-06-16-04-08.gh-issue-110937.SyO5lk.rst @@ -0,0 +1 @@ +Document rest of full public :class:`importlib.metadata.Distribution` API. Also add the (already documented) :class:`~importlib.metadata.PackagePath` to ``__all__``. diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index cdfc1f62..c218c075 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -46,6 +46,7 @@ 'DistributionFinder', 'PackageMetadata', 'PackageNotFoundError', + 'PackagePath', 'SimplePath', 'distribution', 'distributions', From ec62848b37a3f0a5ad4eeac320541d359bbef8be Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Mar 2026 03:21:44 -0400 Subject: [PATCH 140/176] Remove CPython news fragments. --- .../Documentation/2026-01-06-16-04-08.gh-issue-110937.SyO5lk.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Misc/NEWS.d/next/Documentation/2026-01-06-16-04-08.gh-issue-110937.SyO5lk.rst diff --git a/Misc/NEWS.d/next/Documentation/2026-01-06-16-04-08.gh-issue-110937.SyO5lk.rst b/Misc/NEWS.d/next/Documentation/2026-01-06-16-04-08.gh-issue-110937.SyO5lk.rst deleted file mode 100644 index d29bde5c..00000000 --- a/Misc/NEWS.d/next/Documentation/2026-01-06-16-04-08.gh-issue-110937.SyO5lk.rst +++ /dev/null @@ -1 +0,0 @@ -Document rest of full public :class:`importlib.metadata.Distribution` API. Also add the (already documented) :class:`~importlib.metadata.PackagePath` to ``__all__``. 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 141/176] 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 142/176] 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 143/176] 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 144/176] 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 145/176] 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 146/176] 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 147/176] 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 148/176] 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 149/176] =?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 150/176] 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 151/176] 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 From 225b2afaa7819c07d3d1cd306070d0f200b8d086 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 27 Apr 2026 11:58:22 -0400 Subject: [PATCH 152/176] Add agent guidance pointing to skeleton documentation. Closes jaraco/skeleton#197. --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..49ec34a2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +This project follows the [skeleton](https://blog.jaraco.com/skeleton); please read for context on development. From 91aed9a991ac5ca8ebcd631c8f32ab81ede0d652 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:14:36 -0400 Subject: [PATCH 153/176] Add free-threaded Python builds to CI matrix (jaraco/skeleton#198) Agent-Logs-Url: https://github.com/jaraco/skeleton/sessions/806b3a23-52a0-400c-85ef-08d6842f7c84 Co-authored-by: jaraco <308610+jaraco@users.noreply.github.com> --------- 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 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d40c74ac..d1837eca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,6 +36,7 @@ jobs: python: - "3.10" - "3.13" + - "3.13t" platform: - ubuntu-latest - macos-latest @@ -47,6 +48,8 @@ jobs: platform: ubuntu-latest - python: "3.14" platform: ubuntu-latest + - python: "3.14t" + platform: ubuntu-latest - python: "3.15" platform: ubuntu-latest - python: pypy3.10 From fb8483bd52023e910a9d37d7508315ffa8c48db6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 27 Apr 2026 12:18:58 -0400 Subject: [PATCH 154/176] Use Python 3.14 as the latest stable. --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d1837eca..799666e3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,8 +35,8 @@ jobs: matrix: python: - "3.10" - - "3.13" - - "3.13t" + - "3.14" + - "3.14t" platform: - ubuntu-latest - macos-latest @@ -46,9 +46,9 @@ jobs: platform: ubuntu-latest - python: "3.12" platform: ubuntu-latest - - python: "3.14" + - python: "3.13" platform: ubuntu-latest - - python: "3.14t" + - python: "3.13t" platform: ubuntu-latest - python: "3.15" platform: ubuntu-latest From b8f103031f45efad23afc7816be7c79238264a60 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 5 May 2026 19:57:49 -0400 Subject: [PATCH 155/176] More aggressively direct Agents not to add generated artifacts. --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 49ec34a2..8fa416f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1 +1,3 @@ This project follows the [skeleton](https://blog.jaraco.com/skeleton); please read for context on development. + +Before committing, ensure .git/info/exclude is populated from jaraco/dotfiles .gitignore_global so that generated artifacts are never staged. From a4f728c8fbca13091e5b839ffc51b8fa1b2fd74a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 12 Jul 2026 12:51:43 -0400 Subject: [PATCH 156/176] Bump CI matrix from PyPy 3.10 to PyPy 3.11 PyPy 3.10 no longer has wheels for cryptography and other Rust-built extensions, so projects pulling them transitively fail the pypy job. Closes jaraco/skeleton#205 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 799666e3..10c7e9e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,7 +52,7 @@ jobs: platform: ubuntu-latest - python: "3.15" platform: ubuntu-latest - - python: pypy3.10 + - python: pypy3.11 platform: ubuntu-latest runs-on: ${{ matrix.platform }} continue-on-error: ${{ matrix.python == '3.15' }} From 317be02b9b092a19b6087efcbac667aa636a189d Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:52:12 +0200 Subject: [PATCH 157/176] TOML proper indentation (jaraco/skeleton#202) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a25e78ec..1dc1ff1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ enabler = [ type = [ # upstream - # Exclude PyPy from type checks (python/mypy#20454 jaraco/skeleton#187) + # Exclude PyPy from type checks (python/mypy#20454 jaraco/skeleton#187) "pytest-mypy >= 1.0.1; platform_python_implementation != 'PyPy'", # local From b5c8ef5a8d788bd3132ba6d71246e0b6ea19a111 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 12 Jul 2026 12:59:45 -0400 Subject: [PATCH 158/176] Remove free-threaded 3.13 (3.13t) from CI matrix CFFI does not support the free-threaded build of CPython 3.13, breaking any project that depends transitively on cffi/cryptography. Free-threaded 3.14+ is supported, so 3.14t remains. Closes jaraco/skeleton#206 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 10c7e9e4..bb9e8922 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,8 +48,6 @@ jobs: platform: ubuntu-latest - python: "3.13" platform: ubuntu-latest - - python: "3.13t" - platform: ubuntu-latest - python: "3.15" platform: ubuntu-latest - python: pypy3.11 From d9a367c8fa38f41dc657c560deaee75663cedb12 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 12 Jul 2026 13:04:17 -0400 Subject: [PATCH 159/176] Expect modern ruff and simplify configs (jaraco/skeleton#185) --------- Co-authored-by: Jason R. Coombs --- ruff.toml | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/ruff.toml b/ruff.toml index 63c0825f..5dba83a8 100644 --- a/ruff.toml +++ b/ruff.toml @@ -5,38 +5,27 @@ extend-select = [ "C901", # complex-structure "I", # isort "PERF401", # manual-list-comprehension + "UP", # pyupgrade (Code modernization for strings, annotations, conditions, ...) # 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 "PYI", # flake8-pyi - "UP006", # non-pep585-annotation - "UP007", # non-pep604-annotation - "UP010", # unnecessary-future-import - "UP035", # deprecated-import - "UP037", # quoted-annotation - "UP043", # unnecessary-default-type-args # local ] ignore = [ # upstream + "UP015", # redundant-open-modes, explicit is preferred # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, # irrelevant to this project. "PYI011", # typed-argument-default-in-stub # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", - "E111", - "E114", - "E117", "D206", "D300", - "Q000", - "Q001", - "Q002", - "Q003", "COM812", "COM819", From 384acd94b0c9601cc2b747042d0c5760f5d48606 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:10:29 -0400 Subject: [PATCH 160/176] Use reusable workflow to eliminate setup duplication between test and collateral (jaraco/skeleton#199) * Initial plan * Add reusable workflow to share test/collateral setup steps Agent-Logs-Url: https://github.com/jaraco/skeleton/sessions/ae6f447e-da11-4a46-a8d7-23cdbbffe148 Co-authored-by: jaraco <308610+jaraco@users.noreply.github.com> * Remove duplicated env block from main.yml Agent-Logs-Url: https://github.com/jaraco/skeleton/sessions/35bde516-10a9-4b0d-8c1f-6daa390f7011 Co-authored-by: jaraco <308610+jaraco@users.noreply.github.com> --------- 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 | 54 +++++----------------------- .github/workflows/test-suite.yml | 60 ++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/test-suite.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bb9e8922..e0cebf80 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,19 +15,6 @@ on: permissions: contents: read -env: - # Environment variable to support color support (jaraco/skeleton#66) - FORCE_COLOR: 1 - - # Suppress noisy pip warnings - PIP_DISABLE_PIP_VERSION_CHECK: 'true' - PIP_NO_WARN_SCRIPT_LOCATION: 'true' - - # Ensure tests can sense settings about the environment - TOX_OVERRIDE: >- - testenv.pass_env+=GITHUB_*,FORCE_COLOR - - jobs: test: strategy: @@ -52,26 +39,12 @@ jobs: platform: ubuntu-latest - python: pypy3.11 platform: ubuntu-latest - runs-on: ${{ matrix.platform }} + uses: ./.github/workflows/test-suite.yml + with: + python: ${{ matrix.python }} + platform: ${{ matrix.platform }} + # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idcontinue-on-error 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.15' && matrix.platform == 'ubuntu-latest' - run: | - sudo apt update - sudo apt install -y libxml2-dev libxslt-dev - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python }} - allow-prereleases: true - - name: Install tox - run: python -m pip install tox - - name: Run - run: tox collateral: strategy: @@ -80,19 +53,10 @@ jobs: job: - diffcov - docs - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - 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 }} - run: tox -e ${{ matrix.job }} + uses: ./.github/workflows/test-suite.yml + with: + platform: ubuntu-latest + tox-env: ${{ matrix.job }} check: # This job does nothing and is only used for the branch protection if: always() diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml new file mode 100644 index 00000000..7c8a9359 --- /dev/null +++ b/.github/workflows/test-suite.yml @@ -0,0 +1,60 @@ +name: Test Suite + +on: + workflow_call: + inputs: + python: + description: Python version + type: string + default: "3.x" + platform: + description: Runner platform + type: string + default: ubuntu-latest + tox-env: + description: >- + Tox environment to run; if empty, runs default tox + type: string + default: "" + +env: + # Environment variable to support color support (jaraco/skeleton#66) + FORCE_COLOR: 1 + + # Suppress noisy pip warnings + PIP_DISABLE_PIP_VERSION_CHECK: 'true' + PIP_NO_WARN_SCRIPT_LOCATION: 'true' + + # Ensure tests can sense settings about the environment + TOX_OVERRIDE: >- + testenv.pass_env+=GITHUB_*,FORCE_COLOR + +jobs: + run: + runs-on: ${{ inputs.platform }} + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install build dependencies + # Install dependencies for building packages on pre-release Pythons + # jaraco/skeleton#161 + if: inputs.python == '3.15' && inputs.platform == 'ubuntu-latest' + run: | + sudo apt update + sudo apt install -y libxml2-dev libxslt-dev + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python }} + allow-prereleases: true + - name: Install tox + run: python -m pip install tox + - name: Run + if: ${{ !inputs.tox-env }} + run: tox + - name: Run ${{ inputs.tox-env }} + if: ${{ inputs.tox-env }} + run: tox -e ${{ inputs.tox-env }} From f7aefdf156a54d7f8cee3e3a90574f17f5ca3935 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 12 Jul 2026 15:25:18 -0400 Subject: [PATCH 161/176] Instruct agents to fetch and read the skeleton document as a required first step Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8fa416f7..700235be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1 @@ -This project follows the [skeleton](https://blog.jaraco.com/skeleton); please read for context on development. - -Before committing, ensure .git/info/exclude is populated from jaraco/dotfiles .gitignore_global so that generated artifacts are never staged. +**Required first step:** Before reading further or making any changes, fetch and read the [skeleton](https://blog.jaraco.com/skeleton/) document in its entirety. It is the authoritative source of truth for this project's structure, conventions, testing, and release process, and is not duplicated here. Do not act until you have ingested it. From 1f8c1f07e4d2937181a4116b1c1593685b8ff962 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 14 Jul 2026 19:22:15 -0400 Subject: [PATCH 162/176] Move continue-on-error into the reusable workflow's run job. (jaraco/skeleton#209) A job that calls a reusable workflow may not set 'continue-on-error'; GitHub rejects the whole file at startup ("Unexpected value 'continue-on-error'") before creating any jobs. The reusable-workflow refactor in jaraco/skeleton#199 moved the 'test' job to a reusable-workflow call while keeping the job-level 'continue-on-error', taking CI dark on skeleton and every downstream project that has since merged. Set continue-on-error on the reusable workflow's run job instead, keyed on the pre-release Python version already special-cased there. This preserves the allow-3.15-to-fail behavior in a single file, without threading an extra input through the caller. Co-authored-by: Claude Opus 4.8 --- .github/workflows/main.yml | 2 -- .github/workflows/test-suite.yml | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0cebf80..072f3d3b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -43,8 +43,6 @@ jobs: with: python: ${{ matrix.python }} platform: ${{ matrix.platform }} - # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idcontinue-on-error - continue-on-error: ${{ matrix.python == '3.15' }} collateral: strategy: diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 7c8a9359..5c05f9e2 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -32,6 +32,8 @@ env: jobs: run: runs-on: ${{ inputs.platform }} + # Tolerate failures on pre-release Pythons (jaraco/skeleton#199) + continue-on-error: ${{ inputs.python == '3.15' }} permissions: contents: read steps: From 25fc118556d0ad7c3a09dba501b9d6f495fcda24 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 14 Jul 2026 18:31:51 -0400 Subject: [PATCH 163/176] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2268619b..4d5592f6 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -339,7 +339,7 @@ def __repr__(self): Repr with classname and tuple constructor to signal that we deviate from regular tuple behavior. """ - return '%s(%r)' % (self.__class__.__name__, tuple(self)) + return f'{self.__class__.__name__}({tuple(self)!r})' def select(self, **params) -> EntryPoints: """ @@ -836,7 +836,7 @@ class FastPath: """ @_clear_after_fork # type: ignore[misc] - @functools.lru_cache() + @functools.lru_cache def __new__(cls, root): return super().__new__(cls) From 0f88c48c3572f1d35e6b9a3ef70726a139b31690 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 14 Jul 2026 18:36:16 -0400 Subject: [PATCH 164/176] Rely on pytest-perf 0.16 for nanosecond precision in performance tests. Closes #539 --- exercises.py | 5 +---- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/exercises.py b/exercises.py index cf6d9d18..b346cc05 100644 --- a/exercises.py +++ b/exercises.py @@ -51,7 +51,4 @@ def normalize_perf(): # python/cpython#143658 import importlib_metadata # end warmup - # 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') + importlib_metadata.Prepared.normalize('sample') diff --git a/pyproject.toml b/pyproject.toml index 42b2fe03..1b1b2f1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ test = [ # local "packaging", "pyfakefs", - "pytest-perf >= 0.9.2", + "pytest-perf >= 0.16", ] doc = [ From 9757b400ee412ddb1d685f139ed3300f607c059a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jul 2026 21:36:07 -0400 Subject: [PATCH 165/176] Add a benchmark for import time of importlib_metadata. Relies on pytest-perf 0.17 for import time testing. Co-Authored-By: Claude Opus 4.8 --- exercises.py | 5 +++++ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/exercises.py b/exercises.py index b346cc05..4cb1f275 100644 --- a/exercises.py +++ b/exercises.py @@ -52,3 +52,8 @@ def normalize_perf(): import importlib_metadata # end warmup importlib_metadata.Prepared.normalize('sample') + + +def import_time(): + "import" + import importlib_metadata # noqa: F401 diff --git a/pyproject.toml b/pyproject.toml index 1b1b2f1f..9ffce1bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ test = [ # local "packaging", "pyfakefs", - "pytest-perf >= 0.16", + "pytest-perf >= 0.17", ] doc = [ From c0f3789460540aec76a54bea30f2bbcb0028dd00 Mon Sep 17 00:00:00 2001 From: Bernat Gabor Date: Thu, 16 Jul 2026 09:29:36 -0700 Subject: [PATCH 166/176] Make SimplePath path parameters positional-only pathlib.Path does not satisfy SimplePath under ty or pyrefly. The protocol names the joinpath and __truediv__ parameter `other`, while typeshed names Path's `key` and `*other`. Neither side is positional-only, so the typing spec requires the names to match, and they do not. Marking the parameters positional-only reflects how both members are called and lets Path satisfy the protocol. mypy passes before and after. Closes #542 --- importlib_metadata/_meta.py | 4 ++-- newsfragments/542.bugfix.rst | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 newsfragments/542.bugfix.rst diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 0c20eff3..219b2a86 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -54,11 +54,11 @@ class SimplePath(Protocol): """ def joinpath( - self, other: str | os.PathLike[str] + self, other: str | os.PathLike[str], / ) -> SimplePath: ... # pragma: no cover def __truediv__( - self, other: str | os.PathLike[str] + self, other: str | os.PathLike[str], / ) -> SimplePath: ... # pragma: no cover @property diff --git a/newsfragments/542.bugfix.rst b/newsfragments/542.bugfix.rst new file mode 100644 index 00000000..316e28ba --- /dev/null +++ b/newsfragments/542.bugfix.rst @@ -0,0 +1 @@ +Marked the ``other`` parameter of ``SimplePath.joinpath`` and ``SimplePath.__truediv__`` positional-only, so ``pathlib.Path`` satisfies the protocol under type checkers that compare parameter names. From f2c32067e086e4838b5e7b97f6f7f4363c576bed Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Aug 2026 14:40:39 -0400 Subject: [PATCH 167/176] Disable C408 (unnecessary-collection-call) Retain the dict(a=1) call style as an elegant alternative to string-keyed literals rather than have Ruff 0.16's new default rewrite it. Ref jaraco/skeleton#210. Co-Authored-By: Claude Opus 4.8 --- ruff.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ruff.toml b/ruff.toml index 5dba83a8..b3852a7b 100644 --- a/ruff.toml +++ b/ruff.toml @@ -29,6 +29,10 @@ ignore = [ "COM812", "COM819", + # unnecessary-collection-call: retain the dict(a=1) call style rather than + # forcing string-keyed literals. See jaraco/skeleton#210. + "C408", + # local ] From f02e90a821fee92ff5ee2bf0d681b70cf24ebbb0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Aug 2026 17:01:01 -0400 Subject: [PATCH 168/176] Configure C408 to allow dict(a=1) rather than disabling it Switch from ignoring C408 (unnecessary-collection-call) entirely to enabling flake8-comprehensions allow-dict-calls-with-keyword-arguments, which retains the preferred dict(a=1) keyword-call style while C408 still flags dict() and dict([...]). Suggested by @Avasam; matches the configuration in pypa/setuptools#4785. Ref jaraco/skeleton#210. Co-Authored-By: Claude Opus 4.8 --- ruff.toml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ruff.toml b/ruff.toml index b3852a7b..a46d99e6 100644 --- a/ruff.toml +++ b/ruff.toml @@ -29,13 +29,14 @@ ignore = [ "COM812", "COM819", - # unnecessary-collection-call: retain the dict(a=1) call style rather than - # forcing string-keyed literals. See jaraco/skeleton#210. - "C408", - # local ] +[lint.flake8-comprehensions] +# Retain the elegant dict(a=1) keyword-call style; C408 still flags dict() +# and dict([...]). See jaraco/skeleton#210. +allow-dict-calls-with-keyword-arguments = true + [format] # Enable preview to get hugged parenthesis unwrapping and other nice surprises # See https://github.com/jaraco/skeleton/pull/133#issuecomment-2239538373 From 8aa3460fd351622af70174c72e0bc5f796a470c0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Aug 2026 15:21:29 -0400 Subject: [PATCH 169/176] Ignore N999 (invalid-module-name) (jaraco/skeleton#212) ruff 0.16 flags N999 for projects whose source directory is the distribution name and is redirected to a different import package (e.g. namespace layouts, as used throughout coherent-oss), even though that dotted name is a valid module. Reported upstream as astral-sh/ruff#27801; ignore it here rather than carry a per-file noqa in every such project. Co-authored-by: Claude Opus 4.8 --- ruff.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ruff.toml b/ruff.toml index a46d99e6..01353f90 100644 --- a/ruff.toml +++ b/ruff.toml @@ -29,6 +29,13 @@ ignore = [ "COM812", "COM819", + # N999 (invalid-module-name) yields false positives when a project's + # source directory is the distribution name and is redirected to a + # different import package (e.g. namespace layouts); the dotted directory + # name is itself a valid module. See + # https://github.com/astral-sh/ruff/issues/27801. + "N999", + # local ] From 7b7120264bb4158b9c5845c2ac0d5787e5ff9e58 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Aug 2026 10:25:42 -0400 Subject: [PATCH 170/176] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref jaraco/skeleton#210 --- importlib_metadata/__init__.py | 2 +- importlib_metadata/_compat.py | 2 +- importlib_metadata/_text.py | 2 +- tests/test_main.py | 5 ++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 4d5592f6..a7c0118f 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -44,10 +44,10 @@ __all__ = [ 'Distribution', 'DistributionFinder', + 'MetadataNotFound', 'PackageMetadata', 'PackageNotFoundError', 'PackagePath', - 'MetadataNotFound', 'SimplePath', 'distribution', 'distributions', diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index 01356d69..fe9a721e 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -1,7 +1,7 @@ import platform import sys -__all__ = ['install', 'NullFinder'] +__all__ = ['NullFinder', 'install'] def install(cls): diff --git a/importlib_metadata/_text.py b/importlib_metadata/_text.py index c88cfbb2..a7b87447 100644 --- a/importlib_metadata/_text.py +++ b/importlib_metadata/_text.py @@ -95,5 +95,5 @@ def index(self, sub): return self.lower().index(sub.lower()) def split(self, splitter=' ', maxsplit=0): - pattern = re.compile(re.escape(splitter), re.I) + pattern = re.compile(re.escape(splitter), re.IGNORECASE) return pattern.split(self, maxsplit) diff --git a/tests/test_main.py b/tests/test_main.py index f306f3d9..559bd499 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -259,9 +259,8 @@ def test_egg_info(self): def test_egg(self): egg = self.site_dir.joinpath('foo-3.6.egg') egg.mkdir() - with self.add_sys_path(egg): - with self.assertRaises(PackageNotFoundError): - version('foo') + with self.add_sys_path(egg), self.assertRaises(PackageNotFoundError): + version('foo') class MissingSysPath(fixtures.OnSysPath, unittest.TestCase): From ecfcc3836a004c0140b2f277859c5026e2166bd6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Aug 2026 10:27:18 -0400 Subject: [PATCH 171/176] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref jaraco/skeleton#210 --- importlib_metadata/__init__.py | 4 ++-- tests/test_api.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index a7c0118f..22d48d68 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -711,7 +711,7 @@ def make_condition(name): def quoted_marker(section): section = section or '' - extra, sep, markers = section.partition(':') + extra, _sep, markers = section.partition(':') if extra and markers: markers = f'({markers})' conditions = list(filter(None, [markers, make_condition(extra)])) @@ -1071,7 +1071,7 @@ def _name_from_stem(stem): filename, ext = os.path.splitext(stem) if ext not in ('.dist-info', '.egg-info'): return - name, sep, rest = filename.partition('-') + name, _sep, _rest = filename.partition('-') return name diff --git a/tests/test_api.py b/tests/test_api.py index 3dbed628..b2e43e27 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -77,9 +77,9 @@ def test_read_text(self): ] for pkg_name, expect_content in tests: with self.subTest(pkg_name): - top_level = [ + top_level = next( path for path in files(pkg_name) if path.name == 'top_level.txt' - ][0] + ) self.assertEqual(top_level.read_text(), expect_content) def test_entry_points(self): @@ -186,7 +186,7 @@ def _test_files(files): file.read_text() def test_file_hash_repr(self): - util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0] + util = next(p for p in files('distinfo-pkg') if p.name == 'mod.py') self.assertRegex(repr(util.hash), '') def test_files_dist_info(self): From 7b599977ebff18214f21d067ecb025ff2117e599 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Aug 2026 10:31:12 -0400 Subject: [PATCH 172/176] Disable nuisance B006; the proposed fix is inelegant. Better would be to have an immutable mapping object here. Ref jaraco/skeleton#210 --- 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 559bd499..5a6694d9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -134,7 +134,7 @@ def test_unique_distributions(self): class InvalidMetadataTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): @staticmethod - def make_pkg(name, files=dict(METADATA="VERSION: 1.0")): + def make_pkg(name, files=dict(METADATA="VERSION: 1.0")): # noqa: B006 """ Create metadata for a dist-info package with name and files. """ From 458fdc3bc3500c827ead2369fb27857c18dee093 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Aug 2026 10:47:51 -0400 Subject: [PATCH 173/176] Fix B006 by using an immutable type from the future. Ref jaraco/skeleton#210. --- tests/compat/py314.py | 18 ++++++++++++++++++ tests/test_main.py | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/compat/py314.py b/tests/compat/py314.py index cf34f467..a0dd2120 100644 --- a/tests/compat/py314.py +++ b/tests/compat/py314.py @@ -5,6 +5,24 @@ from test.support import warnings_helper as orig +if sys.version_info >= (3, 15): + from builtins import frozendict +else: + + def frozendict(*args, **kwargs) -> types.MappingProxyType: + """ + Approximate frozendict, added to builtins in Python 3.15. + + A mapping proxy is read-only, so as long as the mapping it wraps + has no other referents, it's effectively immutable. + + >>> frozendict(a=1)['a'] + 1 + >>> frozendict({'a': 1}) == {'a': 1} + True + """ + return types.MappingProxyType(dict(*args, **kwargs)) + @contextlib.contextmanager def ignore_warnings(*, category, message=''): diff --git a/tests/test_main.py b/tests/test_main.py index 5a6694d9..7abef6c7 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -22,6 +22,7 @@ from . import fixtures from ._path import Symlink +from .compat.py314 import frozendict class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): @@ -134,7 +135,7 @@ def test_unique_distributions(self): class InvalidMetadataTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): @staticmethod - def make_pkg(name, files=dict(METADATA="VERSION: 1.0")): # noqa: B006 + def make_pkg(name, files=frozendict(METADATA="VERSION: 1.0")): """ Create metadata for a dist-info package with name and files. """ From 2c43cdf7c2faa369083118742a036303b85edf66 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Aug 2026 10:57:31 -0400 Subject: [PATCH 174/176] Ignore B008 (function-call-in-default-argument). (jaraco/skeleton#215) B006 already catches shared mutable defaults; B008 flags every call regardless of mutability, and escaping it requires enumerating fully-qualified names in each downstream project. Closes jaraco/skeleton#214 Co-authored-by: Claude Opus 5 --- ruff.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruff.toml b/ruff.toml index 01353f90..24393e4f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -36,6 +36,9 @@ ignore = [ # https://github.com/astral-sh/ruff/issues/27801. "N999", + # B008 is over-reaching; see jaraco/skeleton#214 + "B008", # function-call-in-default-argument + # local ] From 2f377252aa18a3f286c18b0780e33d42a380a064 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Aug 2026 11:23:14 -0400 Subject: [PATCH 175/176] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref jaraco/skeleton#210 --- importlib_metadata/__init__.py | 3 ++- importlib_metadata/_adapters.py | 2 +- tests/compat/py314.py | 42 +++++++++++++++++++++++++++++---- tests/fixtures.py | 41 ++++++++++++++++---------------- tests/test_api.py | 5 ++-- tests/test_main.py | 4 ++-- 6 files changed, 66 insertions(+), 31 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 22d48d68..3796fbce 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -217,7 +217,8 @@ class EntryPoint: def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) - self.module + # resolve the value now, raising ValueError if it's invalid + _ = self.module def load(self) -> Any: """Load the entry point from its definition. If only a module diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index dede395d..0bc23000 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -54,7 +54,7 @@ class Message(email.message.Message): """ - multiple_use_keys = set( + multiple_use_keys = frozenset( map( FoldedCase, [ diff --git a/tests/compat/py314.py b/tests/compat/py314.py index a0dd2120..5468fa03 100644 --- a/tests/compat/py314.py +++ b/tests/compat/py314.py @@ -2,6 +2,7 @@ import sys import types import warnings +from collections.abc import Mapping from test.support import warnings_helper as orig @@ -9,19 +10,52 @@ from builtins import frozendict else: - def frozendict(*args, **kwargs) -> types.MappingProxyType: + class frozendict(Mapping): """ Approximate frozendict, added to builtins in Python 3.15. - A mapping proxy is read-only, so as long as the mapping it wraps - has no other referents, it's effectively immutable. + Accepts anything dict() accepts and presents it read-only. >>> frozendict(a=1)['a'] 1 >>> frozendict({'a': 1}) == {'a': 1} True + >>> frozendict(a=1) + frozendict({'a': 1}) + + Hashable when the values are. + + >>> hash(frozendict(a=1)) == hash(frozendict(a=1)) + True + + Unlike a mapping proxy, supports copying, so callers may take a + mutable snapshot to alter. + + >>> import copy + >>> spec = frozendict(pkg={'META': 'lower'}) + >>> altered = copy.deepcopy(spec) + >>> altered['pkg']['META'] = 'UPPER' + >>> spec['pkg']['META'] + 'lower' """ - return types.MappingProxyType(dict(*args, **kwargs)) + + def __init__(self, *args, **kwargs): + self._data = dict(*args, **kwargs) + + def __getitem__(self, key): + return self._data[key] + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + + def __hash__(self): + return hash(frozenset(self._data.items())) + + def __repr__(self): + return f'{type(self).__name__}({self._data!r})' @contextlib.contextmanager diff --git a/tests/fixtures.py b/tests/fixtures.py index 0416e4a4..685baf18 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -13,6 +13,7 @@ from . import _path from ._path import FilesSpec from .compat.py312 import import_helper +from .compat.py314 import frozendict @contextlib.contextmanager @@ -70,7 +71,7 @@ def setUp(self): class DistInfoPkg(OnSysPath, SiteBuilder): - files: FilesSpec = { + files: FilesSpec = frozendict({ "distinfo_pkg-1.0.0.dist-info": { "METADATA": """ Name: distinfo-pkg @@ -94,7 +95,7 @@ class DistInfoPkg(OnSysPath, SiteBuilder): def main(): print("hello world") """, - } + }) def make_uppercase(self): """ @@ -113,7 +114,7 @@ class DistInfoPkgEditable(DistInfoPkg): """ some_hash = '524127ce937f7cb65665130c695abd18ca386f60bb29687efb976faa1596fdcc' - files: FilesSpec = { + files: FilesSpec = frozendict({ 'distinfo_pkg-1.0.0.dist-info': { 'direct_url.json': json.dumps({ "archive_info": { @@ -123,22 +124,22 @@ class DistInfoPkgEditable(DistInfoPkg): "url": "file:///path/to/distinfo_pkg-1.0.0.editable-py3-none-any.whl", }) }, - } + }) class DistInfoPkgWithDot(OnSysPath, SiteBuilder): - files: FilesSpec = { + files: FilesSpec = frozendict({ "pkg_dot-1.0.0.dist-info": { "METADATA": """ Name: pkg.dot Version: 1.0.0 """, }, - } + }) class DistInfoPkgWithDotLegacy(OnSysPath, SiteBuilder): - files: FilesSpec = { + files: FilesSpec = frozendict({ "pkg.dot-1.0.0.dist-info": { "METADATA": """ Name: pkg.dot @@ -151,7 +152,7 @@ class DistInfoPkgWithDotLegacy(OnSysPath, SiteBuilder): Version: 1.0.0 """, }, - } + }) class DistInfoPkgOffPath(SiteBuilder): @@ -159,7 +160,7 @@ class DistInfoPkgOffPath(SiteBuilder): class EggInfoPkg(OnSysPath, SiteBuilder): - files: FilesSpec = { + files: FilesSpec = frozendict({ "egginfo_pkg.egg-info": { "PKG-INFO": """ Name: egginfo-pkg @@ -191,11 +192,11 @@ class EggInfoPkg(OnSysPath, SiteBuilder): def main(): print("hello world") """, - } + }) class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteBuilder): - files: FilesSpec = { + files: FilesSpec = frozendict({ "egg_with_module_pkg.egg-info": { "PKG-INFO": "Name: egg_with_module-pkg", # SOURCES.txt is made from the source archive, and contains files @@ -222,11 +223,11 @@ class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteBuilder): def main(): print("hello world") """, - } + }) class EggInfoPkgPipInstalledExternalDataFiles(OnSysPath, SiteBuilder): - files: FilesSpec = { + files: FilesSpec = frozendict({ "egg_with_module_pkg.egg-info": { "PKG-INFO": "Name: egg_with_module-pkg", # SOURCES.txt is made from the source archive, and contains files @@ -256,11 +257,11 @@ class EggInfoPkgPipInstalledExternalDataFiles(OnSysPath, SiteBuilder): def main(): print("hello world") """, - } + }) class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteBuilder): - files: FilesSpec = { + files: FilesSpec = frozendict({ "egg_with_no_modules_pkg.egg-info": { "PKG-INFO": "Name: egg_with_no_modules-pkg", # SOURCES.txt is made from the source archive, and contains files @@ -282,11 +283,11 @@ class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteBuilder): # top_level.txt correctly reflects that no modules are installed "top_level.txt": b"\n", }, - } + }) class EggInfoPkgSourcesFallback(OnSysPath, SiteBuilder): - files: FilesSpec = { + files: FilesSpec = frozendict({ "sources_fallback_pkg.egg-info": { "PKG-INFO": "Name: sources_fallback-pkg", # SOURCES.txt is made from the source archive, and contains files @@ -304,11 +305,11 @@ class EggInfoPkgSourcesFallback(OnSysPath, SiteBuilder): def main(): print("hello world") """, - } + }) class EggInfoFile(OnSysPath, SiteBuilder): - files: FilesSpec = { + files: FilesSpec = frozendict({ "egginfo_file.egg-info": """ Metadata-Version: 1.0 Name: egginfo_file @@ -321,7 +322,7 @@ class EggInfoFile(OnSysPath, SiteBuilder): Description: UNKNOWN Platform: UNKNOWN """, - } + }) # dedent all text strings before writing diff --git a/tests/test_api.py b/tests/test_api.py index b2e43e27..1468e0a9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -54,9 +54,8 @@ def test_name_normalization(self): def test_prefix_not_matched(self): prefixes = 'p', 'pkg', 'pkg.' for prefix in prefixes: - with self.subTest(prefix): - with self.assertRaises(PackageNotFoundError): - distribution(prefix) + with self.subTest(prefix), self.assertRaises(PackageNotFoundError): + distribution(prefix) def test_for_top_level(self): tests = [ diff --git a/tests/test_main.py b/tests/test_main.py index 7abef6c7..63f9c06a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -58,7 +58,7 @@ def test_abc_enforced(self): dict(name=''), ) def test_invalid_inputs_to_from_name(self, name): - with self.assertRaises(Exception): + with self.assertRaises(ValueError): Distribution.from_name(name) @@ -165,7 +165,7 @@ def test_missing_metadata(self): """ fixtures.build_files(self.make_pkg('foo-4.3', files={}), self.site_dir) with self.assertRaises(MetadataNotFound): - Distribution.from_name('foo').metadata + _ = Distribution.from_name('foo').metadata with self.assertRaises(MetadataNotFound): metadata('foo') From c5acd62aee92667de3d245a786cce14718cfe786 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Aug 2026 11:25:49 -0400 Subject: [PATCH 176/176] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/542.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/542.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index ec413ff2..e657ca8c 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v9.0.1 +====== + +Bugfixes +-------- + +- Marked the ``other`` parameter of ``SimplePath.joinpath`` and ``SimplePath.__truediv__`` positional-only, so ``pathlib.Path`` satisfies the protocol under type checkers that compare parameter names. (#542) + + v9.0.0 ====== diff --git a/newsfragments/542.bugfix.rst b/newsfragments/542.bugfix.rst deleted file mode 100644 index 316e28ba..00000000 --- a/newsfragments/542.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Marked the ``other`` parameter of ``SimplePath.joinpath`` and ``SimplePath.__truediv__`` positional-only, so ``pathlib.Path`` satisfies the protocol under type checkers that compare parameter names.