From 4261109a9f6a21d93081cf376b465d16d8e73ecc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 11:46:31 -0400 Subject: [PATCH 01/12] Add a couple of functions from jaraco.functools. --- importlib_metadata/_functools.py | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/importlib_metadata/_functools.py b/importlib_metadata/_functools.py index 5dda6a21..dfe1f746 100644 --- a/importlib_metadata/_functools.py +++ b/importlib_metadata/_functools.py @@ -102,3 +102,57 @@ def wrapper(param, *args, **kwargs): return func(param, *args, **kwargs) return wrapper + + +# From jaraco.functools 4.0.2 +def compose(*funcs): + """ + Compose any number of unary functions into a single unary function. + + Comparable to + `function composition `_ + in mathematics: + + ``h = g ∘ f`` implies ``h(x) = g(f(x))``. + + In Python, ``h = compose(g, f)``. + + >>> import textwrap + >>> expected = str.strip(textwrap.dedent(compose.__doc__)) + >>> strip_and_dedent = compose(str.strip, textwrap.dedent) + >>> strip_and_dedent(compose.__doc__) == expected + True + + Compose also allows the innermost function to take arbitrary arguments. + + >>> round_three = lambda x: round(x, ndigits=3) + >>> f = compose(round_three, int.__truediv__) + >>> [f(3*x, x+1) for x in range(1,10)] + [1.5, 2.0, 2.25, 2.4, 2.5, 2.571, 2.625, 2.667, 2.7] + """ + + def compose_two(f1, f2): + return lambda *args, **kwargs: f1(f2(*args, **kwargs)) + + return functools.reduce(compose_two, funcs) + + +def apply(transform): + """ + Decorate a function with a transform function that is + invoked on results returned from the decorated function. + + >>> @apply(reversed) + ... def get_numbers(start): + ... "doc for get_numbers" + ... return range(start, start+3) + >>> list(get_numbers(4)) + [6, 5, 4] + >>> get_numbers.__doc__ + 'doc for get_numbers' + """ + + def wrap(func): + return functools.wraps(func)(compose(transform, func)) + + return wrap From e36808d31bcf3848e30702eb4a487eb4b8f8b4ae Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 11:47:30 -0400 Subject: [PATCH 02/12] Create 'localize_dist' function to convert stdlib to local versions of Distributions. --- importlib_metadata/__init__.py | 5 ++++- importlib_metadata/_compat.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 46a14e64..416d5a25 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -33,8 +33,9 @@ from ._compat import ( NullFinder, install, + localize_dist, ) -from ._functools import method_cache, pass_none +from ._functools import apply, method_cache, pass_none from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath from .compat import py39, py311 @@ -409,6 +410,7 @@ def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: """ @classmethod + @apply(localize_dist) def from_name(cls, name: str) -> Distribution: """Return the Distribution for the given package name. @@ -427,6 +429,7 @@ def from_name(cls, name: str) -> Distribution: raise PackageNotFoundError(name) @classmethod + @apply(functools.partial(map, localize_dist)) def discover( cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs ) -> Iterable[Distribution]: diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index 01356d69..ebd163db 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -1,5 +1,13 @@ +from __future__ import annotations + +import importlib.metadata import platform import sys +import warnings +from typing import cast + +import importlib_metadata + __all__ = ['install', 'NullFinder'] @@ -54,3 +62,17 @@ def pypy_partial(val): """ is_pypy = platform.python_implementation() == 'PyPy' return val + is_pypy + + +def localize_dist( + dist: importlib_metadata.Distribution | importlib.metadata.Distribution, +) -> importlib_metadata.Distribution: + """ + Ensure dist is an :class:`importlib_metadata.Distribution`. + """ + if isinstance(dist, importlib_metadata.Distribution): + return dist + if isinstance(dist, importlib.metadata.PathDistribution): + return importlib_metadata.PathDistribution(dist._path) + warnings.warn(f"Unrecognized distribution subclass {dist.__class__}") + return cast(importlib_metadata.Distribution, dist) From 6ff671d97e7f807cfc082c2753c93dcd01c0b2e3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 11:43:37 -0400 Subject: [PATCH 03/12] Create 'localize_metadata' function to normalize the outputs for .metadata. --- importlib_metadata/__init__.py | 2 ++ importlib_metadata/_compat.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 416d5a25..6962eee2 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -34,6 +34,7 @@ NullFinder, install, localize_dist, + localize_metadata, ) from ._functools import apply, method_cache, pass_none from ._itertools import always_iterable, bucket, unique_everseen @@ -477,6 +478,7 @@ def _discover_resolvers(): return filter(None, declared) @property + @apply(localize_metadata) def metadata(self) -> _meta.PackageMetadata: """Return the parsed metadata for this Distribution. diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index ebd163db..84937ff5 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -1,12 +1,13 @@ from __future__ import annotations +import email.message import importlib.metadata import platform import sys import warnings from typing import cast -import importlib_metadata +import importlib_metadata._adapters __all__ = ['install', 'NullFinder'] @@ -76,3 +77,17 @@ def localize_dist( return importlib_metadata.PathDistribution(dist._path) warnings.warn(f"Unrecognized distribution subclass {dist.__class__}") return cast(importlib_metadata.Distribution, dist) + + +if sys.version_info >= (3, 10): + StdlibMessage = importlib.metadata._adapters.Message +else: + StdlibMessage = email.message.Message + + +def localize_metadata( + input: importlib_metadata._adapters.Message | StdlibMessage, +) -> importlib_metadata._adapters.Message: + if isinstance(input, importlib_metadata._adapters.Message): + return input + return importlib_metadata._adapters.Message(input) From 5cd7d14974ccd3c565274f46fb2a939184ea990d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 12:04:29 -0400 Subject: [PATCH 04/12] Create 'localize_package_path' function to convert stdlib to local versions of PackagePath. --- importlib_metadata/__init__.py | 4 +++- importlib_metadata/_compat.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6962eee2..0933d433 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -35,8 +35,9 @@ install, localize_dist, localize_metadata, + localize_package_path, ) -from ._functools import apply, method_cache, pass_none +from ._functools import apply, compose, method_cache, pass_none from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath from .compat import py39, py311 @@ -529,6 +530,7 @@ def entry_points(self) -> EntryPoints: return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) @property + @apply(pass_none(compose(list, functools.partial(map, localize_package_path)))) def files(self) -> Optional[List[PackagePath]]: """Files in this distribution. diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index 84937ff5..1d34cce6 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -91,3 +91,13 @@ def localize_metadata( if isinstance(input, importlib_metadata._adapters.Message): return input return importlib_metadata._adapters.Message(input) + + +def localize_package_path( + input: importlib_metadata.PackagePath | importlib.metadata.PackagePath, +) -> importlib_metadata.PackagePath: + if isinstance(input, importlib_metadata.PackagePath): + return input + replacement = importlib_metadata.PackagePath(input) + vars(replacement).update(vars(input)) + return replacement From 374bd980308e65c5e17b881d79fd52b4cc3219e7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 12:08:55 -0400 Subject: [PATCH 05/12] Add news fragment. --- newsfragments/486.removal.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/486.removal.rst diff --git a/newsfragments/486.removal.rst b/newsfragments/486.removal.rst new file mode 100644 index 00000000..1adc1dda --- /dev/null +++ b/newsfragments/486.removal.rst @@ -0,0 +1 @@ +When providers supply objects from ``importlib.metadata``, they are now adapted to the classes from ``importlib_metadata``. \ No newline at end of file From 90002689b46ab7606cc6e7b40986fec3cdc82b42 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 12:40:37 -0400 Subject: [PATCH 06/12] In localize_dist, cast the result to paper over an error. --- importlib_metadata/_compat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index 1d34cce6..128d35e9 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -74,7 +74,9 @@ def localize_dist( if isinstance(dist, importlib_metadata.Distribution): return dist if isinstance(dist, importlib.metadata.PathDistribution): - return importlib_metadata.PathDistribution(dist._path) + return importlib_metadata.PathDistribution( + cast(importlib_metadata._meta.SimplePath, dist._path) + ) warnings.warn(f"Unrecognized distribution subclass {dist.__class__}") return cast(importlib_metadata.Distribution, dist) From 1a30e01f5bdc64f1b2b097c670faeeeac24a2ff6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 12:42:20 -0400 Subject: [PATCH 07/12] Stop trying to differentiate between importlib.metadata._adapters.Message and email.message.Message. --- importlib_metadata/_compat.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index 128d35e9..e242e3ce 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -81,14 +81,8 @@ def localize_dist( return cast(importlib_metadata.Distribution, dist) -if sys.version_info >= (3, 10): - StdlibMessage = importlib.metadata._adapters.Message -else: - StdlibMessage = email.message.Message - - def localize_metadata( - input: importlib_metadata._adapters.Message | StdlibMessage, + input: importlib_metadata._adapters.Message | email.message.Message, ) -> importlib_metadata._adapters.Message: if isinstance(input, importlib_metadata._adapters.Message): return input From 5a67414802e7e863fa6e52a6cc96d8d23bdc41b6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 14:03:38 -0400 Subject: [PATCH 08/12] Suppress warnings when 'importlib_metadata' has been imported twice. --- importlib_metadata/_compat.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index e242e3ce..04311fb9 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -77,7 +77,10 @@ def localize_dist( return importlib_metadata.PathDistribution( cast(importlib_metadata._meta.SimplePath, dist._path) ) - warnings.warn(f"Unrecognized distribution subclass {dist.__class__}") + # workaround for when pytest has replaced importlib_metadata + # https://github.com/python/importlib_metadata/pull/505#issuecomment-2344329001 + if dist.__class__.__module__ != 'importlib_metadata': + warnings.warn(f"Unrecognized distribution subclass {dist.__class__}") return cast(importlib_metadata.Distribution, dist) From 5a67a0d8f53e2bb478d46f57a1c9b151b59a6964 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 14:14:36 -0400 Subject: [PATCH 09/12] Suppress warning when CustomDistribution, derived from importlib.metadata, is present. --- tests/compat/test_py39_compat.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/compat/test_py39_compat.py b/tests/compat/test_py39_compat.py index db9fb1b7..dd5dcfc0 100644 --- a/tests/compat/test_py39_compat.py +++ b/tests/compat/test_py39_compat.py @@ -1,6 +1,8 @@ +import contextlib import pathlib import sys import unittest +import warnings from importlib_metadata import ( distribution, @@ -63,6 +65,9 @@ def test_compatibility_with_old_stdlib_path_distribution(self): Ref python/importlib_metadata#396. """ self.fixtures.enter_context(fixtures.install_finder(self._meta_path_finder())) + self.fixtures.enter_context( + suppress_unrecognized_distribution_subclass_warning() + ) assert list(distributions()) assert distribution("distinfo_pkg") @@ -72,3 +77,15 @@ def test_compatibility_with_old_stdlib_path_distribution(self): assert list(metadata("distinfo_pkg")) assert list(metadata("distinfo_pkg_custom")) assert list(entry_points(group="entries")) + + +@contextlib.contextmanager +def suppress_unrecognized_distribution_subclass_warning(): + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + category=UserWarning, + message="Unrecognized distribution subclass", + append=True, + ) + yield From 3dad7541b5b99194ada5dc5411efa8857379779b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 14:20:00 -0400 Subject: [PATCH 10/12] Move _compat to its own package. --- importlib_metadata/{_compat.py => _compat/__init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename importlib_metadata/{_compat.py => _compat/__init__.py} (100%) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat/__init__.py similarity index 100% rename from importlib_metadata/_compat.py rename to importlib_metadata/_compat/__init__.py From 0ad266afdfa1411b3b34e621e6df1088ce3e1f5c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 14:22:56 -0400 Subject: [PATCH 11/12] Extract localize functions into their own module. --- importlib_metadata/__init__.py | 12 +++---- importlib_metadata/_compat/__init__.py | 46 -------------------------- importlib_metadata/_compat/localize.py | 45 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 53 deletions(-) create mode 100644 importlib_metadata/_compat/localize.py diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 0933d433..9a673358 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -33,9 +33,7 @@ from ._compat import ( NullFinder, install, - localize_dist, - localize_metadata, - localize_package_path, + localize, ) from ._functools import apply, compose, method_cache, pass_none from ._itertools import always_iterable, bucket, unique_everseen @@ -412,7 +410,7 @@ def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: """ @classmethod - @apply(localize_dist) + @apply(localize.dist) def from_name(cls, name: str) -> Distribution: """Return the Distribution for the given package name. @@ -431,7 +429,7 @@ def from_name(cls, name: str) -> Distribution: raise PackageNotFoundError(name) @classmethod - @apply(functools.partial(map, localize_dist)) + @apply(functools.partial(map, localize.dist)) def discover( cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs ) -> Iterable[Distribution]: @@ -479,7 +477,7 @@ def _discover_resolvers(): return filter(None, declared) @property - @apply(localize_metadata) + @apply(localize.metadata) def metadata(self) -> _meta.PackageMetadata: """Return the parsed metadata for this Distribution. @@ -530,7 +528,7 @@ def entry_points(self) -> EntryPoints: return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) @property - @apply(pass_none(compose(list, functools.partial(map, localize_package_path)))) + @apply(pass_none(compose(list, functools.partial(map, localize.package_path)))) def files(self) -> Optional[List[PackagePath]]: """Files in this distribution. diff --git a/importlib_metadata/_compat/__init__.py b/importlib_metadata/_compat/__init__.py index 04311fb9..01356d69 100644 --- a/importlib_metadata/_compat/__init__.py +++ b/importlib_metadata/_compat/__init__.py @@ -1,14 +1,5 @@ -from __future__ import annotations - -import email.message -import importlib.metadata import platform import sys -import warnings -from typing import cast - -import importlib_metadata._adapters - __all__ = ['install', 'NullFinder'] @@ -63,40 +54,3 @@ def pypy_partial(val): """ is_pypy = platform.python_implementation() == 'PyPy' return val + is_pypy - - -def localize_dist( - dist: importlib_metadata.Distribution | importlib.metadata.Distribution, -) -> importlib_metadata.Distribution: - """ - Ensure dist is an :class:`importlib_metadata.Distribution`. - """ - if isinstance(dist, importlib_metadata.Distribution): - return dist - if isinstance(dist, importlib.metadata.PathDistribution): - return importlib_metadata.PathDistribution( - cast(importlib_metadata._meta.SimplePath, dist._path) - ) - # workaround for when pytest has replaced importlib_metadata - # https://github.com/python/importlib_metadata/pull/505#issuecomment-2344329001 - if dist.__class__.__module__ != 'importlib_metadata': - warnings.warn(f"Unrecognized distribution subclass {dist.__class__}") - return cast(importlib_metadata.Distribution, dist) - - -def localize_metadata( - input: importlib_metadata._adapters.Message | email.message.Message, -) -> importlib_metadata._adapters.Message: - if isinstance(input, importlib_metadata._adapters.Message): - return input - return importlib_metadata._adapters.Message(input) - - -def localize_package_path( - input: importlib_metadata.PackagePath | importlib.metadata.PackagePath, -) -> importlib_metadata.PackagePath: - if isinstance(input, importlib_metadata.PackagePath): - return input - replacement = importlib_metadata.PackagePath(input) - vars(replacement).update(vars(input)) - return replacement diff --git a/importlib_metadata/_compat/localize.py b/importlib_metadata/_compat/localize.py new file mode 100644 index 00000000..9062ff01 --- /dev/null +++ b/importlib_metadata/_compat/localize.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import email.message +import importlib.metadata +import warnings +from typing import cast + +import importlib_metadata._adapters + + +def dist( + dist: importlib_metadata.Distribution | importlib.metadata.Distribution, +) -> importlib_metadata.Distribution: + """ + Ensure dist is an :class:`importlib_metadata.Distribution`. + """ + if isinstance(dist, importlib_metadata.Distribution): + return dist + if isinstance(dist, importlib.metadata.PathDistribution): + return importlib_metadata.PathDistribution( + cast(importlib_metadata._meta.SimplePath, dist._path) + ) + # workaround for when pytest has replaced importlib_metadata + # https://github.com/python/importlib_metadata/pull/505#issuecomment-2344329001 + if dist.__class__.__module__ != 'importlib_metadata': + warnings.warn(f"Unrecognized distribution subclass {dist.__class__}") + return cast(importlib_metadata.Distribution, dist) + + +def metadata( + input: importlib_metadata._adapters.Message | email.message.Message, +) -> importlib_metadata._adapters.Message: + if isinstance(input, importlib_metadata._adapters.Message): + return input + return importlib_metadata._adapters.Message(input) + + +def package_path( + input: importlib_metadata.PackagePath | importlib.metadata.PackagePath, +) -> importlib_metadata.PackagePath: + if isinstance(input, importlib_metadata.PackagePath): + return input + replacement = importlib_metadata.PackagePath(input) + vars(replacement).update(vars(input)) + return replacement From 4a350a53b8e83d12db595dc3fb3f511bea4a4993 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 14:24:05 -0400 Subject: [PATCH 12/12] Rename 'localize.metadata' to 'localize.message'. --- importlib_metadata/__init__.py | 2 +- importlib_metadata/_compat/localize.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 9a673358..c35fe108 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -477,7 +477,7 @@ def _discover_resolvers(): return filter(None, declared) @property - @apply(localize.metadata) + @apply(localize.message) def metadata(self) -> _meta.PackageMetadata: """Return the parsed metadata for this Distribution. diff --git a/importlib_metadata/_compat/localize.py b/importlib_metadata/_compat/localize.py index 9062ff01..d90da1a3 100644 --- a/importlib_metadata/_compat/localize.py +++ b/importlib_metadata/_compat/localize.py @@ -27,7 +27,7 @@ def dist( return cast(importlib_metadata.Distribution, dist) -def metadata( +def message( input: importlib_metadata._adapters.Message | email.message.Message, ) -> importlib_metadata._adapters.Message: if isinstance(input, importlib_metadata._adapters.Message):