Skip to content

Commit d5ddec9

Browse files
committed
fix(pypi) backport python_full_version fix to Python
This has been fixed in the Starlark implementation in bazel-contrib#2793 and in this PR I am backporting the changes to handle the full python version target platform strings so that we can have the same behaviour for now. At the same time I have simplified and got rid of the specialization handling in the Python algorithm just like I did in the starlark, which simplifies the tests and makes the algorithm more correct. Work towards bazel-contrib#2830
1 parent 61c91fe commit d5ddec9

File tree

4 files changed

+185
-333
lines changed

4 files changed

+185
-333
lines changed

python/private/pypi/whl_installer/platform.py

+43-44
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import sys
1919
from dataclasses import dataclass
2020
from enum import Enum
21-
from typing import Any, Dict, Iterator, List, Optional, Union
21+
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
2222

2323

2424
class OS(Enum):
@@ -77,25 +77,32 @@ def _as_int(value: Optional[Union[OS, Arch]]) -> int:
7777
return int(value.value)
7878

7979

80-
def host_interpreter_minor_version() -> int:
81-
return sys.version_info.minor
80+
def host_interpreter_version() -> Tuple[int, int]:
81+
return (sys.version_info.minor, sys.version_info.micro)
8282

8383

8484
@dataclass(frozen=True)
8585
class Platform:
8686
os: Optional[OS] = None
8787
arch: Optional[Arch] = None
8888
minor_version: Optional[int] = None
89+
micro_version: Optional[int] = None
8990

9091
@classmethod
9192
def all(
9293
cls,
9394
want_os: Optional[OS] = None,
9495
minor_version: Optional[int] = None,
96+
micro_version: Optional[int] = None,
9597
) -> List["Platform"]:
9698
return sorted(
9799
[
98-
cls(os=os, arch=arch, minor_version=minor_version)
100+
cls(
101+
os=os,
102+
arch=arch,
103+
minor_version=minor_version,
104+
micro_version=micro_version,
105+
)
99106
for os in OS
100107
for arch in Arch
101108
if not want_os or want_os == os
@@ -112,32 +119,16 @@ def host(cls) -> List["Platform"]:
112119
A list of parsed values which makes the signature the same as
113120
`Platform.all` and `Platform.from_string`.
114121
"""
122+
minor, micro = host_interpreter_version()
115123
return [
116124
Platform(
117125
os=OS.interpreter(),
118126
arch=Arch.interpreter(),
119-
minor_version=host_interpreter_minor_version(),
127+
minor_version=minor,
128+
micro_version=micro,
120129
)
121130
]
122131

123-
def all_specializations(self) -> Iterator["Platform"]:
124-
"""Return the platform itself and all its unambiguous specializations.
125-
126-
For more info about specializations see
127-
https://bazel.build/docs/configurable-attributes
128-
"""
129-
yield self
130-
if self.arch is None:
131-
for arch in Arch:
132-
yield Platform(os=self.os, arch=arch, minor_version=self.minor_version)
133-
if self.os is None:
134-
for os in OS:
135-
yield Platform(os=os, arch=self.arch, minor_version=self.minor_version)
136-
if self.arch is None and self.os is None:
137-
for os in OS:
138-
for arch in Arch:
139-
yield Platform(os=os, arch=arch, minor_version=self.minor_version)
140-
141132
def __lt__(self, other: Any) -> bool:
142133
"""Add a comparison method, so that `sorted` returns the most specialized platforms first."""
143134
if not isinstance(other, Platform) or other is None:
@@ -153,24 +144,15 @@ def __lt__(self, other: Any) -> bool:
153144

154145
def __str__(self) -> str:
155146
if self.minor_version is None:
156-
if self.os is None and self.arch is None:
157-
return "//conditions:default"
158-
159-
if self.arch is None:
160-
return f"@platforms//os:{self.os}"
161-
else:
162-
return f"{self.os}_{self.arch}"
163-
164-
if self.arch is None and self.os is None:
165-
return f"@//python/config_settings:is_python_3.{self.minor_version}"
147+
return f"{self.os}_{self.arch}"
166148

167-
if self.arch is None:
168-
return f"cp3{self.minor_version}_{self.os}_anyarch"
149+
minor_version = self.minor_version
150+
micro_version = self.micro_version
169151

170-
if self.os is None:
171-
return f"cp3{self.minor_version}_anyos_{self.arch}"
172-
173-
return f"cp3{self.minor_version}_{self.os}_{self.arch}"
152+
if micro_version is None:
153+
return f"cp3{minor_version}_{self.os}_{self.arch}"
154+
else:
155+
return f"cp3{minor_version}.{micro_version}_{self.os}_{self.arch}"
174156

175157
@classmethod
176158
def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]:
@@ -190,14 +172,25 @@ def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]:
190172
os, _, arch = tail.partition("_")
191173
arch = arch or "*"
192174

193-
minor_version = int(abi[len("cp3") :]) if abi else None
175+
if abi:
176+
tail = abi[len("cp3") :]
177+
minor_version, _, micro_version = tail.partition(".")
178+
minor_version = int(minor_version)
179+
if micro_version == "":
180+
micro_version = None
181+
else:
182+
micro_version = int(micro_version)
183+
else:
184+
minor_version = None
185+
micro_version = None
194186

195187
if arch != "*":
196188
ret.add(
197189
cls(
198190
os=OS[os] if os != "*" else None,
199191
arch=Arch[arch],
200192
minor_version=minor_version,
193+
micro_version=micro_version,
201194
)
202195
)
203196

@@ -206,6 +199,7 @@ def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]:
206199
cls.all(
207200
want_os=OS[os] if os != "*" else None,
208201
minor_version=minor_version,
202+
micro_version=micro_version,
209203
)
210204
)
211205

@@ -282,7 +276,12 @@ def platform_machine(self) -> str:
282276

283277
def env_markers(self, extra: str) -> Dict[str, str]:
284278
# If it is None, use the host version
285-
minor_version = self.minor_version or host_interpreter_minor_version()
279+
if self.minor_version is None:
280+
minor, micro = host_interpreter_version()
281+
else:
282+
minor, micro = self.minor_version, self.micro_version
283+
284+
micro = micro or 0
286285

287286
return {
288287
"extra": extra,
@@ -292,12 +291,12 @@ def env_markers(self, extra: str) -> Dict[str, str]:
292291
"platform_system": self.platform_system,
293292
"platform_release": "", # unset
294293
"platform_version": "", # unset
295-
"python_version": f"3.{minor_version}",
294+
"python_version": f"3.{minor}",
296295
# FIXME @aignas 2024-01-14: is putting zero last a good idea? Maybe we should
297296
# use `20` or something else to avoid having weird issues where the full version is used for
298297
# matching and the author decides to only support 3.y.5 upwards.
299-
"implementation_version": f"3.{minor_version}.0",
300-
"python_full_version": f"3.{minor_version}.0",
298+
"implementation_version": f"3.{minor}.{micro}",
299+
"python_full_version": f"3.{minor}.{micro}",
301300
# we assume that the following are the same as the interpreter used to setup the deps:
302301
# "implementation_name": "cpython"
303302
# "platform_python_implementation: "CPython",

python/private/pypi/whl_installer/wheel.py

+38-102
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from python.private.pypi.whl_installer.platform import (
2929
Platform,
30-
host_interpreter_minor_version,
30+
host_interpreter_version,
3131
)
3232

3333

@@ -62,12 +62,13 @@ def __init__(
6262
"""
6363
self.name: str = Deps._normalize(name)
6464
self._platforms: Set[Platform] = platforms or set()
65-
self._target_versions = {p.minor_version for p in platforms or {}}
66-
self._default_minor_version = None
67-
if platforms and len(self._target_versions) > 2:
65+
self._target_versions = {(p.minor_version, p.micro_version) for p in platforms or {}}
66+
if platforms and len(self._target_versions) > 1:
6867
# TODO @aignas 2024-06-23: enable this to be set via a CLI arg
6968
# for being more explicit.
70-
self._default_minor_version = host_interpreter_minor_version()
69+
self._default_minor_version, _ = host_interpreter_version()
70+
else:
71+
self._default_minor_version = None
7172

7273
if None in self._target_versions and len(self._target_versions) > 2:
7374
raise ValueError(
@@ -88,8 +89,13 @@ def __init__(
8889
# Then add all of the requirements in order
8990
self._deps: Set[str] = set()
9091
self._select: Dict[Platform, Set[str]] = defaultdict(set)
92+
93+
reqs_by_name = {}
9194
for req in reqs:
92-
self._add_req(req, want_extras)
95+
reqs_by_name.setdefault(req.name, []).append(req)
96+
97+
for reqs in reqs_by_name.values():
98+
self._add_req(reqs, want_extras)
9399

94100
def _add(self, dep: str, platform: Optional[Platform]):
95101
dep = Deps._normalize(dep)
@@ -123,50 +129,6 @@ def _add(self, dep: str, platform: Optional[Platform]):
123129
# Add the platform-specific dep
124130
self._select[platform].add(dep)
125131

126-
# Add the dep to specializations of the given platform if they
127-
# exist in the select statement.
128-
for p in platform.all_specializations():
129-
if p not in self._select:
130-
continue
131-
132-
self._select[p].add(dep)
133-
134-
if len(self._select[platform]) == 1:
135-
# We are adding a new item to the select and we need to ensure that
136-
# existing dependencies from less specialized platforms are propagated
137-
# to the newly added dependency set.
138-
for p, deps in self._select.items():
139-
# Check if the existing platform overlaps with the given platform
140-
if p == platform or platform not in p.all_specializations():
141-
continue
142-
143-
self._select[platform].update(self._select[p])
144-
145-
def _maybe_add_common_dep(self, dep):
146-
if len(self._target_versions) < 2:
147-
return
148-
149-
platforms = [Platform()] + [
150-
Platform(minor_version=v) for v in self._target_versions
151-
]
152-
153-
# If the dep is targeting all target python versions, lets add it to
154-
# the common dependency list to simplify the select statements.
155-
for p in platforms:
156-
if p not in self._select:
157-
return
158-
159-
if dep not in self._select[p]:
160-
return
161-
162-
# All of the python version-specific branches have the dep, so lets add
163-
# it to the common deps.
164-
self._deps.add(dep)
165-
for p in platforms:
166-
self._select[p].remove(dep)
167-
if not self._select[p]:
168-
self._select.pop(p)
169-
170132
@staticmethod
171133
def _normalize(name: str) -> str:
172134
return re.sub(r"[-_.]+", "_", name).lower()
@@ -227,66 +189,40 @@ def _resolve_extras(
227189

228190
return extras
229191

230-
def _add_req(self, req: Requirement, extras: Set[str]) -> None:
231-
if req.marker is None:
232-
self._add(req.name, None)
233-
return
192+
def _add_req(self, reqs: List[Requirement], extras: Set[str]) -> None:
193+
platforms_to_add = set()
194+
for req in reqs:
195+
if req.marker is None:
196+
self._add(req.name, None)
197+
return
234198

235-
marker_str = str(req.marker)
199+
for plat in self._platforms:
200+
if plat in platforms_to_add:
201+
# marker evaluation is more expensive than this check
202+
continue
236203

237-
if not self._platforms:
238-
if any(req.marker.evaluate({"extra": extra}) for extra in extras):
239-
self._add(req.name, None)
240-
return
204+
added = False
205+
for extra in extras:
206+
if added:
207+
break
241208

242-
# NOTE @aignas 2023-12-08: in order to have reasonable select statements
243-
# we do have to have some parsing of the markers, so it begs the question
244-
# if packaging should be reimplemented in Starlark to have the best solution
245-
# for now we will implement it in Python and see what the best parsing result
246-
# can be before making this decision.
247-
match_os = any(
248-
tag in marker_str
249-
for tag in [
250-
"os_name",
251-
"sys_platform",
252-
"platform_system",
253-
]
254-
)
255-
match_arch = "platform_machine" in marker_str
256-
match_version = "version" in marker_str
209+
if req.marker.evaluate(plat.env_markers(extra)):
210+
platforms_to_add.add(plat)
211+
added = True
212+
break
257213

258-
if not (match_os or match_arch or match_version):
259-
if any(req.marker.evaluate({"extra": extra}) for extra in extras):
260-
self._add(req.name, None)
214+
if len(platforms_to_add) == len(self._platforms):
215+
# the dep is in all target platforms, let's just add it to the regular
216+
# list
217+
self._add(req.name, None)
261218
return
262219

263-
for plat in self._platforms:
264-
if not any(
265-
req.marker.evaluate(plat.env_markers(extra)) for extra in extras
266-
):
267-
continue
268-
269-
if match_arch and self._default_minor_version:
220+
for plat in platforms_to_add:
221+
if self._default_minor_version is not None:
270222
self._add(req.name, plat)
271-
if plat.minor_version == self._default_minor_version:
272-
self._add(req.name, Platform(plat.os, plat.arch))
273-
elif match_arch:
274-
self._add(req.name, Platform(plat.os, plat.arch))
275-
elif match_os and self._default_minor_version:
276-
self._add(req.name, Platform(plat.os, minor_version=plat.minor_version))
277-
if plat.minor_version == self._default_minor_version:
278-
self._add(req.name, Platform(plat.os))
279-
elif match_os:
280-
self._add(req.name, Platform(plat.os))
281-
elif match_version and self._default_minor_version:
282-
self._add(req.name, Platform(minor_version=plat.minor_version))
283-
if plat.minor_version == self._default_minor_version:
284-
self._add(req.name, Platform())
285-
elif match_version:
286-
self._add(req.name, None)
287223

288-
# Merge to common if possible after processing all platforms
289-
self._maybe_add_common_dep(req.name)
224+
if self._default_minor_version is None or plat.minor_version == self._default_minor_version:
225+
self._add(req.name, Platform(os = plat.os, arch = plat.arch))
290226

291227
def build(self) -> FrozenDeps:
292228
return FrozenDeps(

0 commit comments

Comments
 (0)