Discard insufficient fork markers (#10682)

In #10669, a pyproject.toml with requires-python but no environment had
a lockfile covering only a subset of the requires-python space:

```toml
resolution-markers = [
    "python_full_version >= '3.10' and platform_python_implementation == 'CPython'",
    "python_full_version == '3.9.*'",
    "python_full_version < '3.9'",
]
```

This marker set is invalid, we have to reject the lockfile. (We can
still use the versions though, to avoid churn).

Part 1/2 of #10669
This commit is contained in:
konsti 2025-03-13 15:49:37 +01:00 committed by GitHub
parent 797f1fbac0
commit 33b70b17ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 125 additions and 0 deletions

View file

@ -723,8 +723,43 @@ impl Lock {
self.fork_markers.as_slice()
}
/// Checks whether the fork markers cover the entire supported marker space.
///
/// Returns the actually covered and the expected marker space on validation error.
pub fn check_marker_coverage(&self) -> Result<(), (MarkerTree, MarkerTree)> {
let fork_markers_union = if self.fork_markers().is_empty() {
self.requires_python.to_marker_tree()
} else {
let mut fork_markers_union = MarkerTree::FALSE;
for fork_marker in self.fork_markers() {
fork_markers_union.or(fork_marker.pep508());
}
fork_markers_union
};
let mut environments_union = if !self.supported_environments.is_empty() {
let mut environments_union = MarkerTree::FALSE;
for fork_marker in &self.supported_environments {
environments_union.or(*fork_marker);
}
environments_union
} else {
MarkerTree::TRUE
};
// When a user defines environments, they are implicitly constrained by requires-python.
environments_union.and(self.requires_python.to_marker_tree());
if fork_markers_union.negate().is_disjoint(environments_union) {
Ok(())
} else {
Err((fork_markers_union, environments_union))
}
}
/// Returns the TOML representation of this lockfile.
pub fn to_toml(&self) -> Result<String, toml_edit::ser::Error> {
// Catch a lockfile where the union of fork markers doesn't cover the supported
// environments.
debug_assert!(self.check_marker_coverage().is_ok());
// We construct a TOML document manually instead of going through Serde to enable
// the use of inline tables.
let mut doc = toml_edit::DocumentMut::new();

View file

@ -965,6 +965,15 @@ impl ValidatedLock {
return Ok(Self::Versions(lock));
}
if let Err((fork_markers_union, environments_union)) = lock.check_marker_coverage() {
warn_user!(
"Ignoring existing lockfile due to fork markers not covering the supported environments: `{}` vs `{}`",
fork_markers_union.try_to_string().unwrap_or("true".to_string()),
environments_union.try_to_string().unwrap_or("true".to_string()),
);
return Ok(Self::Versions(lock));
}
// If the set of required platforms has changed, we have to perform a clean resolution.
let expected = lock.simplified_required_environments();
let actual = required_environments

View file

@ -26372,3 +26372,84 @@ fn lock_empty_extra() -> Result<()> {
Ok(())
}
/// The fork markers in the lockfile don't cover the supported environments (here: universal). We
/// need to discard the lockfile.
#[test]
fn lock_invalid_fork_markers() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("pyproject.toml").write_str(
r#"
[project]
name = "attrs"
requires-python = ">=3.8"
version = "1.0.0"
[dependency-groups]
dev = ["idna"]
"#,
)?;
context.temp_dir.child("uv.lock").write_str(
r#"
version = 1
requires-python = ">=3.8"
resolution-markers = [
"python_full_version >= '3.10' and platform_python_implementation == 'CPython'",
"python_full_version == '3.9.*'",
"python_full_version < '3.9'",
]
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[[package]]
name = "attrs"
version = "1.0.0"
source = { editable = "." }
[package.dev-dependencies]
dev = [
{ name = "idna", marker = "python_full_version < '3.10' or platform_python_implementation == 'CPython'" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [{ name = "idna" }]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Ignoring existing lockfile due to fork markers not covering the supported environments: `(python_full_version >= '3.8' and python_full_version < '3.10') or (python_full_version >= '3.8' and platform_python_implementation == 'CPython')` vs `python_full_version >= '3.8'`
Resolved 2 packages in [TIME]
Updated idna v3.10 -> v3.6
"###);
// Check that the lockfile got updated and we don't show the warning anymore.
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
"###);
Ok(())
}