|
15 | 15 | """Utility class to inspect an extracted wheel directory""" |
16 | 16 |
|
17 | 17 | import email |
18 | | -import re |
19 | | -from collections import defaultdict |
20 | | -from dataclasses import dataclass |
21 | 18 | from pathlib import Path |
22 | | -from typing import Dict, List, Optional, Set, Tuple |
| 19 | +from typing import Dict, Optional, Set, Tuple |
23 | 20 |
|
24 | 21 | import installer |
25 | | -from packaging.requirements import Requirement |
26 | 22 | from pip._vendor.packaging.utils import canonicalize_name |
27 | 23 |
|
28 | | -from python.private.pypi.whl_installer.platform import ( |
29 | | - Platform, |
30 | | - host_interpreter_version, |
31 | | -) |
32 | | - |
33 | | - |
34 | | -@dataclass(frozen=True) |
35 | | -class FrozenDeps: |
36 | | - deps: List[str] |
37 | | - deps_select: Dict[str, List[str]] |
38 | | - |
39 | | - |
40 | | -class Deps: |
41 | | - """Deps is a dependency builder that has a build() method to return FrozenDeps.""" |
42 | | - |
43 | | - def __init__( |
44 | | - self, |
45 | | - name: str, |
46 | | - requires_dist: List[str], |
47 | | - *, |
48 | | - extras: Optional[Set[str]] = None, |
49 | | - platforms: Optional[Set[Platform]] = None, |
50 | | - ): |
51 | | - """Create a new instance and parse the requires_dist |
52 | | -
|
53 | | - Args: |
54 | | - name (str): The name of the whl distribution |
55 | | - requires_dist (list[Str]): The Requires-Dist from the METADATA of the whl |
56 | | - distribution. |
57 | | - extras (set[str], optional): The list of requested extras, defaults to None. |
58 | | - platforms (set[Platform], optional): The list of target platforms, defaults to |
59 | | - None. If the list of platforms has multiple `minor_version` values, it |
60 | | - will change the code to generate the select statements using |
61 | | - `@rules_python//python/config_settings:is_python_3.y` conditions. |
62 | | - """ |
63 | | - self.name: str = Deps._normalize(name) |
64 | | - self._platforms: Set[Platform] = platforms or set() |
65 | | - self._target_versions = { |
66 | | - (p.minor_version, p.micro_version) for p in platforms or {} |
67 | | - } |
68 | | - if platforms and len(self._target_versions) > 1: |
69 | | - # TODO @aignas 2024-06-23: enable this to be set via a CLI arg |
70 | | - # for being more explicit. |
71 | | - self._default_minor_version, _ = host_interpreter_version() |
72 | | - else: |
73 | | - self._default_minor_version = None |
74 | | - |
75 | | - if None in self._target_versions and len(self._target_versions) > 2: |
76 | | - raise ValueError( |
77 | | - f"all python versions need to be specified explicitly, got: {platforms}" |
78 | | - ) |
79 | | - |
80 | | - # Sort so that the dictionary order in the FrozenDeps is deterministic |
81 | | - # without the final sort because Python retains insertion order. That way |
82 | | - # the sorting by platform is limited within the Platform class itself and |
83 | | - # the unit-tests for the Deps can be simpler. |
84 | | - reqs = sorted( |
85 | | - (Requirement(wheel_req) for wheel_req in requires_dist), |
86 | | - key=lambda x: f"{x.name}:{sorted(x.extras)}", |
87 | | - ) |
88 | | - |
89 | | - want_extras = self._resolve_extras(reqs, extras) |
90 | | - |
91 | | - # Then add all of the requirements in order |
92 | | - self._deps: Set[str] = set() |
93 | | - self._select: Dict[Platform, Set[str]] = defaultdict(set) |
94 | | - |
95 | | - reqs_by_name = {} |
96 | | - for req in reqs: |
97 | | - reqs_by_name.setdefault(req.name, []).append(req) |
98 | | - |
99 | | - for req_name, reqs in reqs_by_name.items(): |
100 | | - self._add_req(req_name, reqs, want_extras) |
101 | | - |
102 | | - def _add(self, dep: str, platform: Optional[Platform]): |
103 | | - dep = Deps._normalize(dep) |
104 | | - |
105 | | - # Self-edges are processed in _resolve_extras |
106 | | - if dep == self.name: |
107 | | - return |
108 | | - |
109 | | - if not platform: |
110 | | - self._deps.add(dep) |
111 | | - |
112 | | - # If the dep is in the platform-specific list, remove it from the select. |
113 | | - pop_keys = [] |
114 | | - for p, deps in self._select.items(): |
115 | | - if dep not in deps: |
116 | | - continue |
117 | | - |
118 | | - deps.remove(dep) |
119 | | - if not deps: |
120 | | - pop_keys.append(p) |
121 | | - |
122 | | - for p in pop_keys: |
123 | | - self._select.pop(p) |
124 | | - return |
125 | | - |
126 | | - if dep in self._deps: |
127 | | - # If the dep is already in the main dependency list, no need to add it in the |
128 | | - # platform-specific dependency list. |
129 | | - return |
130 | | - |
131 | | - # Add the platform-specific dep |
132 | | - self._select[platform].add(dep) |
133 | | - |
134 | | - @staticmethod |
135 | | - def _normalize(name: str) -> str: |
136 | | - return re.sub(r"[-_.]+", "_", name).lower() |
137 | | - |
138 | | - def _resolve_extras( |
139 | | - self, reqs: List[Requirement], want_extras: Optional[Set[str]] |
140 | | - ) -> Set[str]: |
141 | | - """Resolve extras which are due to depending on self[some_other_extra]. |
142 | | -
|
143 | | - Some packages may have cyclic dependencies resulting from extras being used, one example is |
144 | | - `etils`, where we have one set of extras as aliases for other extras |
145 | | - and we have an extra called 'all' that includes all other extras. |
146 | | -
|
147 | | - Example: github.com/google/etils/blob/a0b71032095db14acf6b33516bca6d885fe09e35/pyproject.toml#L32. |
148 | | -
|
149 | | - When the `requirements.txt` is generated by `pip-tools`, then it is likely that |
150 | | - this step is not needed, but for other `requirements.txt` files this may be useful. |
151 | | -
|
152 | | - NOTE @aignas 2023-12-08: the extra resolution is not platform dependent, |
153 | | - but in order for it to become platform dependent we would have to have |
154 | | - separate targets for each extra in extras. |
155 | | - """ |
156 | | - |
157 | | - # Resolve any extra extras due to self-edges, empty string means no |
158 | | - # extras The empty string in the set is just a way to make the handling |
159 | | - # of no extras and a single extra easier and having a set of {"", "foo"} |
160 | | - # is equivalent to having {"foo"}. |
161 | | - extras: Set[str] = want_extras or {""} |
162 | | - |
163 | | - self_reqs = [] |
164 | | - for req in reqs: |
165 | | - if Deps._normalize(req.name) != self.name: |
166 | | - continue |
167 | | - |
168 | | - if req.marker is None: |
169 | | - # I am pretty sure we cannot reach this code as it does not |
170 | | - # make sense to specify packages in this way, but since it is |
171 | | - # easy to handle, lets do it. |
172 | | - # |
173 | | - # TODO @aignas 2023-12-08: add a test |
174 | | - extras = extras | req.extras |
175 | | - else: |
176 | | - # process these in a separate loop |
177 | | - self_reqs.append(req) |
178 | | - |
179 | | - # A double loop is not strictly optimal, but always correct without recursion |
180 | | - for req in self_reqs: |
181 | | - if any(req.marker.evaluate({"extra": extra}) for extra in extras): |
182 | | - extras = extras | req.extras |
183 | | - else: |
184 | | - continue |
185 | | - |
186 | | - # Iterate through all packages to ensure that we include all of the extras from previously |
187 | | - # visited packages. |
188 | | - for req_ in self_reqs: |
189 | | - if any(req_.marker.evaluate({"extra": extra}) for extra in extras): |
190 | | - extras = extras | req_.extras |
191 | | - |
192 | | - return extras |
193 | | - |
194 | | - def _add_req(self, req_name, reqs: List[Requirement], extras: Set[str]) -> None: |
195 | | - platforms_to_add = set() |
196 | | - for req in reqs: |
197 | | - if req.marker is None: |
198 | | - self._add(req.name, None) |
199 | | - return |
200 | | - |
201 | | - if not self._platforms: |
202 | | - if any(req.marker.evaluate({"extra": extra}) for extra in extras): |
203 | | - self._add(req.name, None) |
204 | | - return |
205 | | - |
206 | | - for plat in self._platforms: |
207 | | - if plat in platforms_to_add: |
208 | | - # marker evaluation is more expensive than this check |
209 | | - continue |
210 | | - |
211 | | - added = False |
212 | | - for extra in extras: |
213 | | - if added: |
214 | | - break |
215 | | - |
216 | | - if req.marker.evaluate(plat.env_markers(extra)): |
217 | | - platforms_to_add.add(plat) |
218 | | - added = True |
219 | | - break |
220 | | - |
221 | | - if not self._platforms: |
222 | | - return |
223 | | - |
224 | | - if len(platforms_to_add) == len(self._platforms): |
225 | | - # the dep is in all target platforms, let's just add it to the regular |
226 | | - # list |
227 | | - self._add(req_name, None) |
228 | | - return |
229 | | - |
230 | | - for plat in platforms_to_add: |
231 | | - if self._default_minor_version is not None: |
232 | | - self._add(req_name, plat) |
233 | | - |
234 | | - if ( |
235 | | - self._default_minor_version is None |
236 | | - or plat.minor_version == self._default_minor_version |
237 | | - ): |
238 | | - self._add(req_name, Platform(os=plat.os, arch=plat.arch)) |
239 | | - |
240 | | - def build(self) -> FrozenDeps: |
241 | | - return FrozenDeps( |
242 | | - deps=sorted(self._deps), |
243 | | - deps_select={str(p): sorted(deps) for p, deps in self._select.items()}, |
244 | | - ) |
| 24 | +from python.private.pypi.whl_installer.deps import Deps, FrozenDeps |
| 25 | +from python.private.pypi.whl_installer.platform import Platform |
245 | 26 |
|
246 | 27 |
|
247 | 28 | class Wheel: |
|
0 commit comments