Error on lockfiles with incoherent wheel versions (#12235)

Reject lockfiles where the package version and the wheel versions are
incoherent. This implicitly checks that all wheel files have the same
version.

It does not check for the source dist version, since a source dist may
not contain a version in the filename and attempting to deserialize
source dist filenames we may not need is a performance overhead for
something that's already slow in `uv run`.

Fixes #12164
This commit is contained in:
konsti 2025-03-17 23:33:32 +01:00 committed by GitHub
parent 7ea2f657fa
commit 0c352c68e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 114 additions and 0 deletions

View File

@ -2871,6 +2871,19 @@ impl PackageWire {
requires_python: &RequiresPython, requires_python: &RequiresPython,
unambiguous_package_ids: &FxHashMap<PackageName, PackageId>, unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
) -> Result<Package, LockError> { ) -> Result<Package, LockError> {
// Consistency check
if let Some(version) = &self.id.version {
for wheel in &self.wheels {
if version != &wheel.filename.version {
return Err(LockError::from(LockErrorKind::InconsistentVersions {
name: self.id.name,
}));
}
}
// We can't check the source dist version since it does not need to contain the version
// in the filename.
}
let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> { let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
deps.into_iter() deps.into_iter()
.map(|dep| dep.unwire(requires_python, unambiguous_package_ids)) .map(|dep| dep.unwire(requires_python, unambiguous_package_ids))
@ -5156,6 +5169,13 @@ enum LockErrorKind {
#[source] #[source]
err: uv_distribution::Error, err: uv_distribution::Error,
}, },
/// A package has inconsistent versions in a single entry
// Using name instead of id since the version in the id is part of the conflict.
#[error("Locked package and file versions are inconsistent for `{name}`", name = name.cyan())]
InconsistentVersions {
/// The name of the package with the inconsistent entry.
name: PackageName,
},
#[error( #[error(
"Found conflicting extras `{package1}[{extra1}]` \ "Found conflicting extras `{package1}[{extra1}]` \
and `{package2}[{extra2}]` enabled simultaneously" and `{package2}[{extra2}]` enabled simultaneously"

View File

@ -8471,3 +8471,97 @@ fn prune_cache_url_subdirectory() -> Result<()> {
Ok(()) Ok(())
} }
/// Test that incoherence in the versions in a package entry of the lockfile versions is caught.
///
/// See <https://github.com/astral-sh/uv/issues/12164>
#[test]
fn locked_version_coherence() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
");
let lock = context.read("uv.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 1
requires-python = ">=3.12"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]
[[package]]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "iniconfig" },
]
[package.metadata]
requires-dist = [{ name = "iniconfig" }]
"#);
});
// Write an inconsistent iniconfig entry
context
.temp_dir
.child("uv.lock")
.write_str(&lock.replace(r#"version = "2.0.0""#, r#"version = "1.0.0""#))?;
// An inconsistent lockfile should fail with `--locked`
uv_snapshot!(context.filters(), context.sync().arg("--locked"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `uv.lock`
Caused by: Locked package and file versions are inconsistent for `iniconfig`
");
// Without `--locked`, we could fail or recreate the lockfile, currently, we fail.
uv_snapshot!(context.filters(), context.lock(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `uv.lock`
Caused by: Locked package and file versions are inconsistent for `iniconfig`
");
Ok(())
}