Always ensure wheels are compatible with the required Python versions

This commit is contained in:
Zanie Blue 2025-09-18 09:03:46 -05:00
parent e23da5b315
commit 730bad4a88
2 changed files with 89 additions and 50 deletions

View File

@ -32,7 +32,10 @@ struct PrioritizedDistInner {
/// The hashes for each distribution. /// The hashes for each distribution.
hashes: Vec<HashDigest>, hashes: Vec<HashDigest>,
/// The set of supported platforms for the distribution, described in terms of their markers. /// The set of supported platforms for the distribution, described in terms of their markers.
markers: MarkerTree, platform_markers: MarkerTree,
/// The set of supported Python versions for the distribution, described in terms of their
/// markers.
python_markers: MarkerTree,
} }
impl Default for PrioritizedDistInner { impl Default for PrioritizedDistInner {
@ -42,7 +45,8 @@ impl Default for PrioritizedDistInner {
best_wheel_index: None, best_wheel_index: None,
wheels: Vec::new(), wheels: Vec::new(),
hashes: Vec::new(), hashes: Vec::new(),
markers: MarkerTree::FALSE, platform_markers: MarkerTree::FALSE,
python_markers: MarkerTree::FALSE,
} }
} }
} }
@ -101,10 +105,31 @@ impl CompatibleDist<'_> {
} }
} }
/// Return the set of supported platform the distribution, in terms of their markers. /// Return the set of supported platforms the distribution, in terms of their markers.
pub fn implied_platform_markers(&self) -> MarkerTree {
match self.prioritized() {
Some(prioritized) => prioritized.0.platform_markers,
None => MarkerTree::TRUE,
}
}
/// Return the set of supported Python versions for the distribution, in terms of their markers.
pub fn implied_python_markers(&self) -> MarkerTree {
match self.prioritized() {
Some(prioritized) => prioritized.0.python_markers,
None => MarkerTree::TRUE,
}
}
/// Return the combined set of supported markers for the distribution.
pub fn implied_markers(&self) -> MarkerTree { pub fn implied_markers(&self) -> MarkerTree {
match self.prioritized() { match self.prioritized() {
Some(prioritized) => prioritized.0.markers, Some(prioritized) => {
let mut markers = MarkerTree::TRUE;
markers.and(prioritized.0.platform_markers);
markers.and(prioritized.0.python_markers);
markers
}
None => MarkerTree::TRUE, None => MarkerTree::TRUE,
} }
} }
@ -343,7 +368,8 @@ impl PrioritizedDist {
compatibility: WheelCompatibility, compatibility: WheelCompatibility,
) -> Self { ) -> Self {
Self(Box::new(PrioritizedDistInner { Self(Box::new(PrioritizedDistInner {
markers: implied_markers(&dist.filename), platform_markers: implied_platform_markers(&dist.filename),
python_markers: implied_python_markers(&dist.filename),
best_wheel_index: Some(0), best_wheel_index: Some(0),
wheels: vec![(dist, compatibility)], wheels: vec![(dist, compatibility)],
source: None, source: None,
@ -358,7 +384,8 @@ impl PrioritizedDist {
compatibility: SourceDistCompatibility, compatibility: SourceDistCompatibility,
) -> Self { ) -> Self {
Self(Box::new(PrioritizedDistInner { Self(Box::new(PrioritizedDistInner {
markers: MarkerTree::TRUE, python_markers: MarkerTree::TRUE,
platform_markers: MarkerTree::TRUE,
best_wheel_index: None, best_wheel_index: None,
wheels: vec![], wheels: vec![],
source: Some((dist, compatibility)), source: Some((dist, compatibility)),
@ -375,8 +402,15 @@ impl PrioritizedDist {
) { ) {
// Track the implied markers. // Track the implied markers.
if compatibility.is_compatible() { if compatibility.is_compatible() {
if !self.0.markers.is_true() { if !self.0.python_markers.is_true() {
self.0.markers.or(implied_markers(&dist.filename)); self.0
.python_markers
.or(implied_python_markers(&dist.filename));
}
if !self.0.platform_markers.is_true() {
self.0
.platform_markers
.or(implied_platform_markers(&dist.filename));
} }
} }
// Track the hashes. // Track the hashes.
@ -403,7 +437,8 @@ impl PrioritizedDist {
) { ) {
// Track the implied markers. // Track the implied markers.
if compatibility.is_compatible() { if compatibility.is_compatible() {
self.0.markers = MarkerTree::TRUE; self.0.python_markers = MarkerTree::TRUE;
self.0.platform_markers = MarkerTree::TRUE;
} }
// Track the hashes. // Track the hashes.
if !compatibility.is_excluded() { if !compatibility.is_excluded() {
@ -804,14 +839,6 @@ impl IncompatibleWheel {
} }
} }
/// Given a wheel filename, determine the set of supported markers.
pub fn implied_markers(filename: &WheelFilename) -> MarkerTree {
let mut marker = implied_platform_markers(filename);
marker.and(implied_python_markers(filename));
marker
}
/// Given a wheel filename, determine the set of supported platforms, in terms of their markers. /// Given a wheel filename, determine the set of supported platforms, in terms of their markers.
/// ///
/// This is roughly the inverse of platform tag generation: given a tag, we want to infer the /// This is roughly the inverse of platform tag generation: given a tag, we want to infer the
@ -1027,15 +1054,6 @@ mod tests {
); );
} }
#[track_caller]
fn assert_implied_markers(filename: &str, expected: &str) {
let filename = WheelFilename::from_str(filename).unwrap();
assert_eq!(
implied_markers(&filename),
expected.parse::<MarkerTree>().unwrap()
);
}
#[test] #[test]
fn test_implied_platform_markers() { fn test_implied_platform_markers() {
let filename = WheelFilename::from_str("example-1.0-py3-none-any.whl").unwrap(); let filename = WheelFilename::from_str("example-1.0-py3-none-any.whl").unwrap();
@ -1121,28 +1139,4 @@ mod tests {
"python_full_version >= '3.11' and python_full_version < '3.13'", "python_full_version >= '3.11' and python_full_version < '3.13'",
); );
} }
#[test]
fn test_implied_markers() {
assert_implied_markers(
"numpy-1.0-cp310-cp310-win32.whl",
"python_full_version == '3.10.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'x86'",
);
assert_implied_markers(
"pywin32-311-cp314-cp314-win_arm64.whl",
"python_full_version == '3.14.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'ARM64'",
);
assert_implied_markers(
"numpy-1.0-cp311-cp311-macosx_10_9_x86_64.whl",
"python_full_version == '3.11.*' and platform_python_implementation == 'CPython' and sys_platform == 'darwin' and platform_machine == 'x86_64'",
);
assert_implied_markers(
"numpy-1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
"python_full_version == '3.12.*' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'aarch64'",
);
assert_implied_markers(
"example-1.0-py3-none-any.whl",
"python_full_version >= '3' and python_full_version < '4'",
);
}
} }

View File

@ -1418,6 +1418,51 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
return Ok(None); return Ok(None);
} }
// If the package's Python markers are incompatible with the current environment, we need to
// fork. We do not require users to explicitly add Python versions to their
// `required-environments`.
let dist_python_markers = dist.implied_python_markers();
if !env.included_by_marker(dist_python_markers) {
// Then we need to fork.
let Some((left, right)) = fork_version_by_marker(env, dist_python_markers) else {
return Ok(Some(ResolverVersion::Unavailable(
candidate.version().clone(),
UnavailableVersion::IncompatibleDist(IncompatibleDist::Wheel(
// TODO(zanieb): Consider adding a Python-specific variant
IncompatibleWheel::MissingPlatform(dist_python_markers),
)),
)));
};
// TODO(zanieb): Consider a message that's focused on Python versions here
debug!(
"Forking on required Python `{}` for {}=={} ({})",
dist_python_markers
.try_to_string()
.unwrap_or_else(|| "true".to_string()),
name,
candidate.version(),
[&left, &right]
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
);
let forks = vec![
VersionFork {
env: left,
id,
version: None,
},
VersionFork {
env: right,
id,
version: None,
},
];
return Ok(Some(ResolverVersion::Forked(forks)));
}
// If the user explicitly marked a platform as required, ensure it has coverage. // If the user explicitly marked a platform as required, ensure it has coverage.
for marker in self.options.required_environments.iter().copied() { for marker in self.options.required_environments.iter().copied() {
// If the platform is part of the current environment... // If the platform is part of the current environment...