Skip to content

Commit 9e613d5

Browse files
authored
fix(pypi) backport python_full_version fix to Python (#2833)
Handling of `python_full_version` correctly has been fixed in the Starlark implementation in #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. Summary: * Handle `cp3x.y_os_arch` strings in the `platform.py` * Produce correct strings when the `micro_version` is unset. Note, that we use version `0` in evaluating but we use the default version in the config setting. This is to keep compatibility with the current behaviour when the target platform is not fully specified (which would be the case for WORKSPACE users). * Adjust the tests and the code to be more similar to the starlark impl. Work towards #2830
1 parent 61c91fe commit 9e613d5

File tree

4 files changed

+185
-336
lines changed

4 files changed

+185
-336
lines changed

python/private/pypi/whl_installer/platform.py

+43-47
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,9 @@ 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}",
296-
# FIXME @aignas 2024-01-14: is putting zero last a good idea? Maybe we should
297-
# use `20` or something else to avoid having weird issues where the full version is used for
298-
# 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",
294+
"python_version": f"3.{minor}",
295+
"implementation_version": f"3.{minor}.{micro}",
296+
"python_full_version": f"3.{minor}.{micro}",
301297
# we assume that the following are the same as the interpreter used to setup the deps:
302298
# "implementation_name": "cpython"
303299
# "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)