From 33c4896dbaeda2fd7a5fef701431dea05bb83bab Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 Jul 2024 15:34:09 -0400 Subject: [PATCH 1/7] Exclude pytest-ruff (and thus ruff), which cannot build on cygwin. Ref pypa/setuptools#3921 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ad67d3b1..1307e1fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ test = [ "pytest-cov", "pytest-mypy", "pytest-enabler >= 2.2", - "pytest-ruff >= 0.2.1", + "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", # local ] From f087fb4ca05eb08c46abdd2cd67b18a3f33e3c79 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 19 Jul 2024 16:32:46 +0200 Subject: [PATCH 2/7] "preserve" does not require preview any more (jaraco/skeleton#133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * "preserve" does not require preview any more * Update URL in ruff.toml comment --------- Co-authored-by: Bartosz Sławecki --- ruff.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ruff.toml b/ruff.toml index 70612985..7da4bee7 100644 --- a/ruff.toml +++ b/ruff.toml @@ -22,7 +22,5 @@ ignore = [ ] [format] -# Enable preview, required for quote-style = "preserve" -preview = true -# https://docs.astral.sh/ruff/settings/#format-quote-style +# https://docs.astral.sh/ruff/settings/#format_quote-style quote-style = "preserve" From 30f940e74b599400347d1162b7096f184cc46d31 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 19 Jul 2024 16:34:53 +0200 Subject: [PATCH 3/7] Enforce ruff/Perflint rule PERF401 (jaraco/skeleton#132) --- ruff.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ruff.toml b/ruff.toml index 7da4bee7..f1d03f83 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,6 +1,7 @@ [lint] extend-select = [ "C901", + "PERF401", "W", ] ignore = [ From ab34814ca3ffe511ad63bb9589da06fd76758db8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 19 Jul 2024 12:33:01 -0400 Subject: [PATCH 4/7] Re-enable preview, this time not for one specific feature, but for all features in preview. Ref jaraco/skeleton#133 --- ruff.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruff.toml b/ruff.toml index f1d03f83..922aa1f1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -23,5 +23,8 @@ ignore = [ ] [format] +# 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 quote-style = "preserve" From 48f6b143419941be07dc53330a4702ff8c9398c4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 23 Jul 2024 08:56:52 -0400 Subject: [PATCH 5/7] Add test capturing failed expectation. Ref #489. --- tests/test_main.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index f1c12855..a18c2d17 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -130,6 +130,32 @@ 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, + } + + @__import__('pytest').mark.xfail(reason="#489") + 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): From a65c29adc027b3615154cab73aaedd58a6aa23da Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 23 Jul 2024 08:36:16 -0400 Subject: [PATCH 6/7] Prioritize valid dists to invalid dists when retrieving by name. Closes python/importlib_metadata#489 --- importlib_metadata/__init__.py | 14 ++++- importlib_metadata/_itertools.py | 98 ++++++++++++++++++++++++++++++++ newsfragments/489.feature.rst | 1 + tests/test_main.py | 1 - 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 newsfragments/489.feature.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index ed481355..b9fc04f1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -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. diff --git a/importlib_metadata/_itertools.py b/importlib_metadata/_itertools.py index d4ca9b91..79d37198 100644 --- a/importlib_metadata/_itertools.py +++ b/importlib_metadata/_itertools.py @@ -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) diff --git a/newsfragments/489.feature.rst b/newsfragments/489.feature.rst new file mode 100644 index 00000000..f0e1b091 --- /dev/null +++ b/newsfragments/489.feature.rst @@ -0,0 +1 @@ +Prioritize valid dists to invalid dists when retrieving by name. \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index a18c2d17..dc248492 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -140,7 +140,6 @@ def make_pkg(name, files=dict(METADATA="VERSION: 1.0")): f'{name}.dist-info': files, } - @__import__('pytest').mark.xfail(reason="#489") def test_valid_dists_preferred(self): """ Dists with metadata should be preferred when discovered by name. From 4bcda08ca2b1bf91437619d4f99810ecbf83b2b2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 23 Jul 2024 09:03:10 -0400 Subject: [PATCH 7/7] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/489.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/489.feature.rst diff --git a/NEWS.rst b/NEWS.rst index a49f5ec9..0f1b63a4 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.1.0 +====== + +Features +-------- + +- Prioritize valid dists to invalid dists when retrieving by name. (#489) + + v8.0.0 ====== diff --git a/newsfragments/489.feature.rst b/newsfragments/489.feature.rst deleted file mode 100644 index f0e1b091..00000000 --- a/newsfragments/489.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Prioritize valid dists to invalid dists when retrieving by name. \ No newline at end of file