mirror of
https://github.com/python/cpython.git
synced 2025-11-10 10:32:04 +00:00
gh-120492: Sync importlib_metadata 8.2.0 (#124033)
* Sync with importlib_metadata 8.2.0 Removes deprecated behaviors, including support for `PackageMetadata.__getitem__` returning None for missing keys and Distribution subclasses not implementing abstract methods. Prioritizes valid dists to invalid dists when retrieving by name (python/cpython/#120492). Adds SimplePath to `importlib.metadata.__all__`. * Add blurb
This commit is contained in:
parent
d86c2257a6
commit
ec4021c6d7
8 changed files with 154 additions and 64 deletions
|
|
@ -12,7 +12,6 @@
|
||||||
import zipfile
|
import zipfile
|
||||||
import operator
|
import operator
|
||||||
import textwrap
|
import textwrap
|
||||||
import warnings
|
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import posixpath
|
import posixpath
|
||||||
|
|
@ -21,7 +20,7 @@
|
||||||
from . import _meta
|
from . import _meta
|
||||||
from ._collections import FreezableDefaultDict, Pair
|
from ._collections import FreezableDefaultDict, Pair
|
||||||
from ._functools import method_cache, pass_none
|
from ._functools import method_cache, pass_none
|
||||||
from ._itertools import always_iterable, unique_everseen
|
from ._itertools import always_iterable, bucket, unique_everseen
|
||||||
from ._meta import PackageMetadata, SimplePath
|
from ._meta import PackageMetadata, SimplePath
|
||||||
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
|
@ -35,6 +34,7 @@
|
||||||
'DistributionFinder',
|
'DistributionFinder',
|
||||||
'PackageMetadata',
|
'PackageMetadata',
|
||||||
'PackageNotFoundError',
|
'PackageNotFoundError',
|
||||||
|
'SimplePath',
|
||||||
'distribution',
|
'distribution',
|
||||||
'distributions',
|
'distributions',
|
||||||
'entry_points',
|
'entry_points',
|
||||||
|
|
@ -329,27 +329,7 @@ def __repr__(self) -> str:
|
||||||
return f'<FileHash mode: {self.mode} value: {self.value}>'
|
return f'<FileHash mode: {self.mode} value: {self.value}>'
|
||||||
|
|
||||||
|
|
||||||
class DeprecatedNonAbstract:
|
class Distribution(metaclass=abc.ABCMeta):
|
||||||
# Required until Python 3.14
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
all_names = {
|
|
||||||
name for subclass in inspect.getmro(cls) for name in vars(subclass)
|
|
||||||
}
|
|
||||||
abstract = {
|
|
||||||
name
|
|
||||||
for name in all_names
|
|
||||||
if getattr(getattr(cls, name), '__isabstractmethod__', False)
|
|
||||||
}
|
|
||||||
if abstract:
|
|
||||||
warnings.warn(
|
|
||||||
f"Unimplemented abstract methods {abstract}",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
return super().__new__(cls)
|
|
||||||
|
|
||||||
|
|
||||||
class Distribution(DeprecatedNonAbstract):
|
|
||||||
"""
|
"""
|
||||||
An abstract Python distribution package.
|
An abstract Python distribution package.
|
||||||
|
|
||||||
|
|
@ -404,7 +384,7 @@ def from_name(cls, name: str) -> Distribution:
|
||||||
if not name:
|
if not name:
|
||||||
raise ValueError("A distribution name is required.")
|
raise ValueError("A distribution name is required.")
|
||||||
try:
|
try:
|
||||||
return next(iter(cls.discover(name=name)))
|
return next(iter(cls._prefer_valid(cls.discover(name=name))))
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise PackageNotFoundError(name)
|
raise PackageNotFoundError(name)
|
||||||
|
|
||||||
|
|
@ -428,6 +408,16 @@ def discover(
|
||||||
resolver(context) for resolver in cls._discover_resolvers()
|
resolver(context) for resolver in cls._discover_resolvers()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]:
|
||||||
|
"""
|
||||||
|
Prefer (move to the front) distributions that have metadata.
|
||||||
|
|
||||||
|
Ref python/importlib_resources#489.
|
||||||
|
"""
|
||||||
|
buckets = bucket(dists, lambda dist: bool(dist.metadata))
|
||||||
|
return itertools.chain(buckets[True], buckets[False])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def at(path: str | os.PathLike[str]) -> Distribution:
|
def at(path: str | os.PathLike[str]) -> Distribution:
|
||||||
"""Return a Distribution for the indicated metadata path.
|
"""Return a Distribution for the indicated metadata path.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import functools
|
|
||||||
import warnings
|
|
||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
import email.message
|
import email.message
|
||||||
|
|
@ -7,15 +5,6 @@
|
||||||
from ._text import FoldedCase
|
from ._text import FoldedCase
|
||||||
|
|
||||||
|
|
||||||
# Do not remove prior to 2024-01-01 or Python 3.14
|
|
||||||
_warn = functools.partial(
|
|
||||||
warnings.warn,
|
|
||||||
"Implicit None on return values is deprecated and will raise KeyErrors.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Message(email.message.Message):
|
class Message(email.message.Message):
|
||||||
multiple_use_keys = set(
|
multiple_use_keys = set(
|
||||||
map(
|
map(
|
||||||
|
|
@ -52,12 +41,17 @@ def __iter__(self):
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
"""
|
"""
|
||||||
Warn users that a ``KeyError`` can be expected when a
|
Override parent behavior to typical dict behavior.
|
||||||
missing key is supplied. Ref python/importlib_metadata#371.
|
|
||||||
|
``email.message.Message`` will emit None values for missing
|
||||||
|
keys. Typical mappings, including this ``Message``, will raise
|
||||||
|
a key error for missing keys.
|
||||||
|
|
||||||
|
Ref python/importlib_metadata#371.
|
||||||
"""
|
"""
|
||||||
res = super().__getitem__(item)
|
res = super().__getitem__(item)
|
||||||
if res is None:
|
if res is None:
|
||||||
_warn()
|
raise KeyError(item)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _repair_headers(self):
|
def _repair_headers(self):
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from collections import defaultdict, deque
|
||||||
from itertools import filterfalse
|
from itertools import filterfalse
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,3 +72,100 @@ def always_iterable(obj, base_type=(str, bytes)):
|
||||||
return iter(obj)
|
return iter(obj)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return iter((obj,))
|
return iter((obj,))
|
||||||
|
|
||||||
|
|
||||||
|
# Copied from more_itertools 10.3
|
||||||
|
class bucket:
|
||||||
|
"""Wrap *iterable* and return an object that buckets the iterable into
|
||||||
|
child iterables based on a *key* function.
|
||||||
|
|
||||||
|
>>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3']
|
||||||
|
>>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character
|
||||||
|
>>> sorted(list(s)) # Get the keys
|
||||||
|
['a', 'b', 'c']
|
||||||
|
>>> a_iterable = s['a']
|
||||||
|
>>> next(a_iterable)
|
||||||
|
'a1'
|
||||||
|
>>> next(a_iterable)
|
||||||
|
'a2'
|
||||||
|
>>> list(s['b'])
|
||||||
|
['b1', 'b2', 'b3']
|
||||||
|
|
||||||
|
The original iterable will be advanced and its items will be cached until
|
||||||
|
they are used by the child iterables. This may require significant storage.
|
||||||
|
|
||||||
|
By default, attempting to select a bucket to which no items belong will
|
||||||
|
exhaust the iterable and cache all values.
|
||||||
|
If you specify a *validator* function, selected buckets will instead be
|
||||||
|
checked against it.
|
||||||
|
|
||||||
|
>>> from itertools import count
|
||||||
|
>>> it = count(1, 2) # Infinite sequence of odd numbers
|
||||||
|
>>> key = lambda x: x % 10 # Bucket by last digit
|
||||||
|
>>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only
|
||||||
|
>>> s = bucket(it, key=key, validator=validator)
|
||||||
|
>>> 2 in s
|
||||||
|
False
|
||||||
|
>>> list(s[2])
|
||||||
|
[]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, iterable, key, validator=None):
|
||||||
|
self._it = iter(iterable)
|
||||||
|
self._key = key
|
||||||
|
self._cache = defaultdict(deque)
|
||||||
|
self._validator = validator or (lambda x: True)
|
||||||
|
|
||||||
|
def __contains__(self, value):
|
||||||
|
if not self._validator(value):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
item = next(self[value])
|
||||||
|
except StopIteration:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self._cache[value].appendleft(item)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _get_values(self, value):
|
||||||
|
"""
|
||||||
|
Helper to yield items from the parent iterator that match *value*.
|
||||||
|
Items that don't match are stored in the local cache as they
|
||||||
|
are encountered.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
# If we've cached some items that match the target value, emit
|
||||||
|
# the first one and evict it from the cache.
|
||||||
|
if self._cache[value]:
|
||||||
|
yield self._cache[value].popleft()
|
||||||
|
# Otherwise we need to advance the parent iterator to search for
|
||||||
|
# a matching item, caching the rest.
|
||||||
|
else:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
item = next(self._it)
|
||||||
|
except StopIteration:
|
||||||
|
return
|
||||||
|
item_value = self._key(item)
|
||||||
|
if item_value == value:
|
||||||
|
yield item
|
||||||
|
break
|
||||||
|
elif self._validator(item_value):
|
||||||
|
self._cache[item_value].append(item)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for item in self._it:
|
||||||
|
item_value = self._key(item)
|
||||||
|
if self._validator(item_value):
|
||||||
|
self._cache[item_value].append(item)
|
||||||
|
|
||||||
|
yield from self._cache.keys()
|
||||||
|
|
||||||
|
def __getitem__(self, value):
|
||||||
|
if not self._validator(value):
|
||||||
|
return iter(())
|
||||||
|
|
||||||
|
return self._get_values(value)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
|
||||||
import importlib
|
import importlib
|
||||||
import contextlib
|
|
||||||
|
|
||||||
from . import fixtures
|
from . import fixtures
|
||||||
from importlib.metadata import (
|
from importlib.metadata import (
|
||||||
|
|
@ -18,13 +16,6 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def suppress_known_deprecation():
|
|
||||||
with warnings.catch_warnings(record=True) as ctx:
|
|
||||||
warnings.simplefilter('default', category=DeprecationWarning)
|
|
||||||
yield ctx
|
|
||||||
|
|
||||||
|
|
||||||
class APITests(
|
class APITests(
|
||||||
fixtures.EggInfoPkg,
|
fixtures.EggInfoPkg,
|
||||||
fixtures.EggInfoPkgPipInstalledNoToplevel,
|
fixtures.EggInfoPkgPipInstalledNoToplevel,
|
||||||
|
|
@ -153,13 +144,13 @@ def test_metadata_for_this_package(self):
|
||||||
classifiers = md.get_all('Classifier')
|
classifiers = md.get_all('Classifier')
|
||||||
assert 'Topic :: Software Development :: Libraries' in classifiers
|
assert 'Topic :: Software Development :: Libraries' in classifiers
|
||||||
|
|
||||||
def test_missing_key_legacy(self):
|
def test_missing_key(self):
|
||||||
"""
|
"""
|
||||||
Requesting a missing key will still return None, but warn.
|
Requesting a missing key raises KeyError.
|
||||||
"""
|
"""
|
||||||
md = metadata('distinfo-pkg')
|
md = metadata('distinfo-pkg')
|
||||||
with suppress_known_deprecation():
|
with self.assertRaises(KeyError):
|
||||||
assert md['does-not-exist'] is None
|
md['does-not-exist']
|
||||||
|
|
||||||
def test_get_key(self):
|
def test_get_key(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import re
|
import re
|
||||||
import pickle
|
import pickle
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
|
||||||
import importlib
|
import importlib
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import contextlib
|
|
||||||
from test.support import os_helper
|
from test.support import os_helper
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -13,7 +11,6 @@
|
||||||
from .stubs import fake_filesystem_unittest as ffs
|
from .stubs import fake_filesystem_unittest as ffs
|
||||||
|
|
||||||
from . import fixtures
|
from . import fixtures
|
||||||
from ._context import suppress
|
|
||||||
from ._path import Symlink
|
from ._path import Symlink
|
||||||
from importlib.metadata import (
|
from importlib.metadata import (
|
||||||
Distribution,
|
Distribution,
|
||||||
|
|
@ -28,13 +25,6 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def suppress_known_deprecation():
|
|
||||||
with warnings.catch_warnings(record=True) as ctx:
|
|
||||||
warnings.simplefilter('default', category=DeprecationWarning)
|
|
||||||
yield ctx
|
|
||||||
|
|
||||||
|
|
||||||
class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
|
class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
|
||||||
version_pattern = r'\d+\.\d+(\.\d)?'
|
version_pattern = r'\d+\.\d+(\.\d)?'
|
||||||
|
|
||||||
|
|
@ -59,9 +49,6 @@ def test_package_not_found_mentions_metadata(self):
|
||||||
|
|
||||||
assert "metadata" in str(ctx.exception)
|
assert "metadata" in str(ctx.exception)
|
||||||
|
|
||||||
# expected to fail until ABC is enforced
|
|
||||||
@suppress(AssertionError)
|
|
||||||
@suppress_known_deprecation()
|
|
||||||
def test_abc_enforced(self):
|
def test_abc_enforced(self):
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
type('DistributionSubclass', (Distribution,), {})()
|
type('DistributionSubclass', (Distribution,), {})()
|
||||||
|
|
@ -146,6 +133,31 @@ def test_unique_distributions(self):
|
||||||
assert len(after) == len(before)
|
assert len(after) == len(before)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMetadataTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||||
|
@staticmethod
|
||||||
|
def make_pkg(name, files=dict(METADATA="VERSION: 1.0")):
|
||||||
|
"""
|
||||||
|
Create metadata for a dist-info package with name and files.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
f'{name}.dist-info': files,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_valid_dists_preferred(self):
|
||||||
|
"""
|
||||||
|
Dists with metadata should be preferred when discovered by name.
|
||||||
|
|
||||||
|
Ref python/importlib_metadata#489.
|
||||||
|
"""
|
||||||
|
# create three dists with the valid one in the middle (lexicographically)
|
||||||
|
# such that on most file systems, the valid one is never naturally first.
|
||||||
|
fixtures.build_files(self.make_pkg('foo-4.0', files={}), self.site_dir)
|
||||||
|
fixtures.build_files(self.make_pkg('foo-4.1'), self.site_dir)
|
||||||
|
fixtures.build_files(self.make_pkg('foo-4.2', files={}), self.site_dir)
|
||||||
|
dist = Distribution.from_name('foo')
|
||||||
|
assert dist.version == "1.0"
|
||||||
|
|
||||||
|
|
||||||
class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def pkg_with_non_ascii_description(site_dir):
|
def pkg_with_non_ascii_description(site_dir):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
``importlib.metadata`` now prioritizes valid dists to invalid dists when
|
||||||
|
retrieving by name.
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
``importlib.metadata`` now raises a ``KeyError`` instead of returning
|
||||||
|
``None`` when a key is missing from the metadata.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
``SimplePath`` is now presented in ``importlib.metadata.__all__``.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue