Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: python/importlib_metadata
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v8.0.0
Choose a base ref
...
head repository: python/importlib_metadata
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v8.1.0
Choose a head ref
  • 9 commits
  • 6 files changed
  • 3 contributors

Commits on Jul 10, 2024

  1. Copy the full SHA
    33c4896 View commit details

Commits on Jul 19, 2024

  1. "preserve" does not require preview any more (jaraco/skeleton#133)

    * "preserve" does not require preview any more
    * Update URL in ruff.toml comment
    
    ---------
    
    Co-authored-by: Bartosz Sławecki <bartoszpiotrslawecki@gmail.com>
    DimitriPapadopoulos and Bartosz Sławecki authored Jul 19, 2024
    Copy the full SHA
    f087fb4 View commit details
  2. Copy the full SHA
    30f940e View commit details
  3. Merge https://github.com/jaraco/skeleton

    jaraco committed Jul 19, 2024
    Copy the full SHA
    c7c7b64 View commit details
  4. Copy the full SHA
    ab34814 View commit details
  5. Merge https://github.com/jaraco/skeleton

    jaraco committed Jul 19, 2024
    Copy the full SHA
    9693e99 View commit details

Commits on Jul 23, 2024

  1. Copy the full SHA
    48f6b14 View commit details
  2. Prioritize valid dists to invalid dists when retrieving by name.

    Closes #489
    jaraco committed Jul 23, 2024
    Copy the full SHA
    a65c29a View commit details
  3. Finalize

    jaraco committed Jul 23, 2024
    Copy the full SHA
    4bcda08 View commit details
Showing with 149 additions and 5 deletions.
  1. +9 −0 NEWS.rst
  2. +12 −2 importlib_metadata/__init__.py
  3. +98 −0 importlib_metadata/_itertools.py
  4. +1 −1 pyproject.toml
  5. +4 −2 ruff.toml
  6. +25 −0 tests/test_main.py
9 changes: 9 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
v8.1.0
======

Features
--------

- Prioritize valid dists to invalid dists when retrieving by name. (#489)


v8.0.0
======

14 changes: 12 additions & 2 deletions importlib_metadata/__init__.py
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@
install,
)
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 contextlib import suppress
@@ -388,7 +388,7 @@ def from_name(cls, name: str) -> Distribution:
if not name:
raise ValueError("A distribution name is required.")
try:
return next(iter(cls.discover(name=name)))
return next(iter(cls._prefer_valid(cls.discover(name=name))))
except StopIteration:
raise PackageNotFoundError(name)

@@ -412,6 +412,16 @@ def discover(
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
def at(path: str | os.PathLike[str]) -> Distribution:
"""Return a Distribution for the indicated metadata path.
98 changes: 98 additions & 0 deletions importlib_metadata/_itertools.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import defaultdict, deque
from itertools import filterfalse


@@ -71,3 +72,100 @@ def always_iterable(obj, base_type=(str, bytes)):
return iter(obj)
except TypeError:
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)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ test = [
"pytest-cov",
"pytest-mypy",
"pytest-enabler >= 2.2",
"pytest-ruff >= 0.2.1",
"pytest-ruff >= 0.2.1; sys_platform != 'cygwin'",

# local
'importlib_resources>=1.3; python_version < "3.9"',
6 changes: 4 additions & 2 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[lint]
extend-select = [
"C901",
"PERF401",
"W",
]
ignore = [
@@ -22,7 +23,8 @@ ignore = [
]

[format]
# Enable preview, required for quote-style = "preserve"
# Enable preview to get hugged parenthesis unwrapping and other nice surprises
# See https://github.com/jaraco/skeleton/pull/133#issuecomment-2239538373
preview = true
# https://docs.astral.sh/ruff/settings/#format-quote-style
# https://docs.astral.sh/ruff/settings/#format_quote-style
quote-style = "preserve"
25 changes: 25 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -130,6 +130,31 @@ def test_unique_distributions(self):
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):
@staticmethod
def pkg_with_non_ascii_description(site_dir):