diff options
| author | Ross Burton <ross.burton@arm.com> | 2025-04-29 16:29:30 +0100 |
|---|---|---|
| committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2025-05-01 14:22:53 +0100 |
| commit | 83d0d473d6d5a8d59875b0af581d27267e804ba9 (patch) | |
| tree | 52144fc0087e5f8e9836813b2611d3f5d585c76b /meta/recipes-devtools/python/python3 | |
| parent | cf9c5ac2c152a7b53a1306cbde88bc3c0131028f (diff) | |
| download | poky-83d0d473d6d5a8d59875b0af581d27267e804ba9.tar.gz | |
python3: backport the full fix for importlib scanning invalid distributions
Even with our fixes in deterministic_imports.patch the
importlib.metadata package scan was still returning Distribution objects
for empty directories. This interacts badly with rebuilds when recipes
are changing as when a recipe is removed from the sysroot directories
are not removed[1].
In particular this breaks python3-meson-python-native rebuilds when
Meson upgrades from 1.7 to 1.8: the site-packages directory has an empty
meson-1.7.dist-info/ and populated meson-1.8.dist-info/. Whilst it's
deterministic to return the empty 1.7 first, this breaks pypa/build as
it looks through the distributions in order.
We had discussed this with upstream previously and there's a more
comprehensive fix upstream (actually in importlib_metadata, not cpython)
which ensures that valid distribution objects are listed first. So we
can drop our patch and replace it with a backport to fix these rebuilds.
[1] oe-core 4f94d929639 ("sstate/staging: Handle directory creation race issue")
(From OE-Core rev: 73de8daa6293403f5b92d313af32882c47bce396)
Signed-off-by: Ross Burton <ross.burton@arm.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/recipes-devtools/python/python3')
| -rw-r--r-- | meta/recipes-devtools/python/python3/deterministic_imports.patch | 39 | ||||
| -rw-r--r-- | meta/recipes-devtools/python/python3/valid-dists.patch | 160 |
2 files changed, 160 insertions, 39 deletions
diff --git a/meta/recipes-devtools/python/python3/deterministic_imports.patch b/meta/recipes-devtools/python/python3/deterministic_imports.patch deleted file mode 100644 index 61f136ef42..0000000000 --- a/meta/recipes-devtools/python/python3/deterministic_imports.patch +++ /dev/null | |||
| @@ -1,39 +0,0 @@ | |||
| 1 | From 0a02e3b85176a5ce4dd98830bb65dac8596142e9 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Richard Purdie <richard.purdie@linuxfoundation.org> | ||
| 3 | Date: Fri, 27 May 2022 17:05:44 +0100 | ||
| 4 | Subject: [PATCH] python3: Ensure stale empty python module directories don't | ||
| 5 | |||
| 6 | There are two issues here. Firstly, the modules are accessed in on disk order. This | ||
| 7 | means behaviour seen on one system might not reproduce on another and is a real headache. | ||
| 8 | |||
| 9 | Secondly, empty directories left behind by previous modules might be looked at. This | ||
| 10 | has caused a long string of different issues for us. | ||
| 11 | |||
| 12 | As a result, patch this to a behaviour which works for us. | ||
| 13 | |||
| 14 | Upstream-Status: Submitted [https://github.com/python/cpython/issues/120492; need to first talk to upstream to see if they'll take one or both fixes] | ||
| 15 | Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org> | ||
| 16 | --- | ||
| 17 | Lib/importlib/metadata/__init__.py | 9 ++++++++- | ||
| 18 | 1 file changed, 8 insertions(+), 1 deletion(-) | ||
| 19 | |||
| 20 | diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py | ||
| 21 | index 8ce62dd..a6ea6e9 100644 | ||
| 22 | --- a/Lib/importlib/metadata/__init__.py | ||
| 23 | +++ b/Lib/importlib/metadata/__init__.py | ||
| 24 | @@ -786,7 +786,14 @@ class Lookup: | ||
| 25 | self.infos = FreezableDefaultDict(list) | ||
| 26 | self.eggs = FreezableDefaultDict(list) | ||
| 27 | |||
| 28 | - for child in path.children(): | ||
| 29 | + for child in sorted(path.children()): | ||
| 30 | + childpath = pathlib.Path(path.root, child) | ||
| 31 | + try: | ||
| 32 | + if childpath.is_dir() and not any(childpath.iterdir()): | ||
| 33 | + # Empty directories aren't interesting | ||
| 34 | + continue | ||
| 35 | + except PermissionError: | ||
| 36 | + continue | ||
| 37 | low = child.lower() | ||
| 38 | if low.endswith((".dist-info", ".egg-info")): | ||
| 39 | # rpartition is faster than splitext and suitable for this purpose. | ||
diff --git a/meta/recipes-devtools/python/python3/valid-dists.patch b/meta/recipes-devtools/python/python3/valid-dists.patch new file mode 100644 index 0000000000..1b2c078c21 --- /dev/null +++ b/meta/recipes-devtools/python/python3/valid-dists.patch | |||
| @@ -0,0 +1,160 @@ | |||
| 1 | From a65c29adc027b3615154cab73aaedd58a6aa23da Mon Sep 17 00:00:00 2001 | ||
| 2 | From: "Jason R. Coombs" <jaraco@jaraco.com> | ||
| 3 | Date: Tue, 23 Jul 2024 08:36:16 -0400 | ||
| 4 | Subject: [PATCH] Prioritize valid dists to invalid dists when retrieving by | ||
| 5 | name. | ||
| 6 | |||
| 7 | Closes python/importlib_metadata#489 | ||
| 8 | |||
| 9 | Upstream-Status: Backport [https://github.com/python/importlib_metadata/commit/a65c29adc027b3615154cab73aaedd58a6aa23da] | ||
| 10 | Signed-off-by: Ross Burton <ross.burton@arm.com> | ||
| 11 | |||
| 12 | diff --git i/Lib/importlib/metadata/__init__.py w/Lib/importlib/metadata/__init__.py | ||
| 13 | index 8ce62dd864f..085378caabc 100644 | ||
| 14 | --- i/Lib/importlib/metadata/__init__.py | ||
| 15 | +++ w/Lib/importlib/metadata/__init__.py | ||
| 16 | @@ -21,7 +21,7 @@ | ||
| 17 | from . import _meta | ||
| 18 | from ._collections import FreezableDefaultDict, Pair | ||
| 19 | from ._functools import method_cache, pass_none | ||
| 20 | -from ._itertools import always_iterable, unique_everseen | ||
| 21 | +from ._itertools import always_iterable, bucket, unique_everseen | ||
| 22 | from ._meta import PackageMetadata, SimplePath | ||
| 23 | |||
| 24 | from contextlib import suppress | ||
| 25 | @@ -404,7 +404,7 @@ def from_name(cls, name: str) -> Distribution: | ||
| 26 | if not name: | ||
| 27 | raise ValueError("A distribution name is required.") | ||
| 28 | try: | ||
| 29 | - return next(iter(cls.discover(name=name))) | ||
| 30 | + return next(iter(cls._prefer_valid(cls.discover(name=name)))) | ||
| 31 | except StopIteration: | ||
| 32 | raise PackageNotFoundError(name) | ||
| 33 | |||
| 34 | @@ -428,6 +428,16 @@ def discover( | ||
| 35 | resolver(context) for resolver in cls._discover_resolvers() | ||
| 36 | ) | ||
| 37 | |||
| 38 | + @staticmethod | ||
| 39 | + def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]: | ||
| 40 | + """ | ||
| 41 | + Prefer (move to the front) distributions that have metadata. | ||
| 42 | + | ||
| 43 | + Ref python/importlib_resources#489. | ||
| 44 | + """ | ||
| 45 | + buckets = bucket(dists, lambda dist: bool(dist.metadata)) | ||
| 46 | + return itertools.chain(buckets[True], buckets[False]) | ||
| 47 | + | ||
| 48 | @staticmethod | ||
| 49 | def at(path: str | os.PathLike[str]) -> Distribution: | ||
| 50 | """Return a Distribution for the indicated metadata path. | ||
| 51 | diff --git i/Lib/importlib/metadata/_itertools.py w/Lib/importlib/metadata/_itertools.py | ||
| 52 | index d4ca9b9140e..79d37198ce7 100644 | ||
| 53 | --- i/Lib/importlib/metadata/_itertools.py | ||
| 54 | +++ w/Lib/importlib/metadata/_itertools.py | ||
| 55 | @@ -1,3 +1,4 @@ | ||
| 56 | +from collections import defaultdict, deque | ||
| 57 | from itertools import filterfalse | ||
| 58 | |||
| 59 | |||
| 60 | @@ -71,3 +72,100 @@ def always_iterable(obj, base_type=(str, bytes)): | ||
| 61 | return iter(obj) | ||
| 62 | except TypeError: | ||
| 63 | return iter((obj,)) | ||
| 64 | + | ||
| 65 | + | ||
| 66 | +# Copied from more_itertools 10.3 | ||
| 67 | +class bucket: | ||
| 68 | + """Wrap *iterable* and return an object that buckets the iterable into | ||
| 69 | + child iterables based on a *key* function. | ||
| 70 | + | ||
| 71 | + >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] | ||
| 72 | + >>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character | ||
| 73 | + >>> sorted(list(s)) # Get the keys | ||
| 74 | + ['a', 'b', 'c'] | ||
| 75 | + >>> a_iterable = s['a'] | ||
| 76 | + >>> next(a_iterable) | ||
| 77 | + 'a1' | ||
| 78 | + >>> next(a_iterable) | ||
| 79 | + 'a2' | ||
| 80 | + >>> list(s['b']) | ||
| 81 | + ['b1', 'b2', 'b3'] | ||
| 82 | + | ||
| 83 | + The original iterable will be advanced and its items will be cached until | ||
| 84 | + they are used by the child iterables. This may require significant storage. | ||
| 85 | + | ||
| 86 | + By default, attempting to select a bucket to which no items belong will | ||
| 87 | + exhaust the iterable and cache all values. | ||
| 88 | + If you specify a *validator* function, selected buckets will instead be | ||
| 89 | + checked against it. | ||
| 90 | + | ||
| 91 | + >>> from itertools import count | ||
| 92 | + >>> it = count(1, 2) # Infinite sequence of odd numbers | ||
| 93 | + >>> key = lambda x: x % 10 # Bucket by last digit | ||
| 94 | + >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only | ||
| 95 | + >>> s = bucket(it, key=key, validator=validator) | ||
| 96 | + >>> 2 in s | ||
| 97 | + False | ||
| 98 | + >>> list(s[2]) | ||
| 99 | + [] | ||
| 100 | + | ||
| 101 | + """ | ||
| 102 | + | ||
| 103 | + def __init__(self, iterable, key, validator=None): | ||
| 104 | + self._it = iter(iterable) | ||
| 105 | + self._key = key | ||
| 106 | + self._cache = defaultdict(deque) | ||
| 107 | + self._validator = validator or (lambda x: True) | ||
| 108 | + | ||
| 109 | + def __contains__(self, value): | ||
| 110 | + if not self._validator(value): | ||
| 111 | + return False | ||
| 112 | + | ||
| 113 | + try: | ||
| 114 | + item = next(self[value]) | ||
| 115 | + except StopIteration: | ||
| 116 | + return False | ||
| 117 | + else: | ||
| 118 | + self._cache[value].appendleft(item) | ||
| 119 | + | ||
| 120 | + return True | ||
| 121 | + | ||
| 122 | + def _get_values(self, value): | ||
| 123 | + """ | ||
| 124 | + Helper to yield items from the parent iterator that match *value*. | ||
| 125 | + Items that don't match are stored in the local cache as they | ||
| 126 | + are encountered. | ||
| 127 | + """ | ||
| 128 | + while True: | ||
| 129 | + # If we've cached some items that match the target value, emit | ||
| 130 | + # the first one and evict it from the cache. | ||
| 131 | + if self._cache[value]: | ||
| 132 | + yield self._cache[value].popleft() | ||
| 133 | + # Otherwise we need to advance the parent iterator to search for | ||
| 134 | + # a matching item, caching the rest. | ||
| 135 | + else: | ||
| 136 | + while True: | ||
| 137 | + try: | ||
| 138 | + item = next(self._it) | ||
| 139 | + except StopIteration: | ||
| 140 | + return | ||
| 141 | + item_value = self._key(item) | ||
| 142 | + if item_value == value: | ||
| 143 | + yield item | ||
| 144 | + break | ||
| 145 | + elif self._validator(item_value): | ||
| 146 | + self._cache[item_value].append(item) | ||
| 147 | + | ||
| 148 | + def __iter__(self): | ||
| 149 | + for item in self._it: | ||
| 150 | + item_value = self._key(item) | ||
| 151 | + if self._validator(item_value): | ||
| 152 | + self._cache[item_value].append(item) | ||
| 153 | + | ||
| 154 | + yield from self._cache.keys() | ||
| 155 | + | ||
| 156 | + def __getitem__(self, value): | ||
| 157 | + if not self._validator(value): | ||
| 158 | + return iter(()) | ||
| 159 | + | ||
| 160 | + return self._get_values(value) | ||
