From d8a7576dedb16de480e1d8798d2a02771f8eb844 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 29 Dec 2025 11:40:24 -0500 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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 10/21] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/_functools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/_functools.py b/importlib_metadata/_functools.py index b1fd04a8..c159b46e 100644 --- a/importlib_metadata/_functools.py +++ b/importlib_metadata/_functools.py @@ -1,6 +1,7 @@ import functools import types -from typing import Callable, TypeVar +from collections.abc import Callable +from typing import TypeVar # from jaraco.functools 3.3 From 16dcf12e6a14d1b4087d0d7ec350dfefbf717264 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Mar 2026 08:55:04 -0400 Subject: [PATCH 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] 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 16/21] 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 17/21] 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 18/21] 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 19/21] 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 20/21] 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 21/21] 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).