diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 46a14e64..c35fe108 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -33,8 +33,9 @@ from ._compat import ( NullFinder, install, + localize, ) -from ._functools import 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 @@ -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]: @@ -474,6 +477,7 @@ def _discover_resolvers(): return filter(None, declared) @property + @apply(localize.message) def metadata(self) -> _meta.PackageMetadata: """Return the parsed metadata for this Distribution. @@ -524,6 +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)))) def files(self) -> Optional[List[PackagePath]]: """Files in this distribution. 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 diff --git a/importlib_metadata/_compat/localize.py b/importlib_metadata/_compat/localize.py new file mode 100644 index 00000000..d90da1a3 --- /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 message( + 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 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 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 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