From 79139220cf1f4bb071ddd9dfa524167c71d42b41 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 15 Jun 2026 17:54:07 -0400 Subject: [PATCH] Validate pylock.toml package Python requirements --- .../src/lock/export/pylock_toml.rs | 13 ++++ crates/uv/tests/pip_install/pip_install.rs | 60 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/crates/uv-resolver/src/lock/export/pylock_toml.rs b/crates/uv-resolver/src/lock/export/pylock_toml.rs index a2b76344c7074..eae6eb0c24816 100644 --- a/crates/uv-resolver/src/lock/export/pylock_toml.rs +++ b/crates/uv-resolver/src/lock/export/pylock_toml.rs @@ -45,6 +45,8 @@ use crate::{Installable, LockError, ResolverOutput}; #[derive(Debug, thiserror::Error)] pub enum PylockTomlErrorKind { + #[error("Package `{0}` requires Python {2}, but the target Python version is {1}")] + IncompatibleRequiresPython(PackageName, Version, RequiresPython), #[error( "Package `{0}` includes both a registry (`packages.wheels`) and a directory source (`packages.directory`)" )] @@ -1045,6 +1047,17 @@ impl<'lock> PylockToml { continue; } + if let Some(requires_python) = package.requires_python.as_ref() + && !requires_python.contains(&markers.python_full_version().version) + { + return Err(PylockTomlErrorKind::IncompatibleRequiresPython( + package.name.clone(), + markers.python_full_version().version.clone(), + requires_python.clone(), + ) + .into()); + } + match ( package.wheels.is_some(), package.sdist.is_some(), diff --git a/crates/uv/tests/pip_install/pip_install.rs b/crates/uv/tests/pip_install/pip_install.rs index 94bf93d8d8817..733d8228d8bea 100644 --- a/crates/uv/tests/pip_install/pip_install.rs +++ b/crates/uv/tests/pip_install/pip_install.rs @@ -13393,6 +13393,66 @@ requires_python = "==3.13.*" Ok(()) } +#[test] +fn pep_751_package_requires_python() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let pylock_toml = context.temp_dir.child("pylock.toml"); + pylock_toml.write_str( + r#" + lock-version = "1.0" + created-by = "uv" + + [[packages]] + name = "example" + marker = "python_version < '3.0'" + requires-python = ">=99" + directory = { path = "." } + "#, + )?; + + // Skip the Python requirement for packages excluded by their marker. + uv_snapshot!(context.filters(), context.pip_install() + .arg("--preview") + .arg("-r") + .arg("pylock.toml"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Checked in [TIME] + "# + ); + + pylock_toml.write_str( + r#" + lock-version = "1.0" + created-by = "uv" + + [[packages]] + name = "example" + requires-python = ">=99" + directory = { path = "." } + "#, + )?; + + uv_snapshot!(context.filters(), context.pip_install() + .arg("--preview") + .arg("-r") + .arg("pylock.toml"), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Package `example` requires Python >=99, but the target Python version is 3.12.[X] + "# + ); + + Ok(()) +} + #[test] fn pep_751_requires_python() -> Result<()> { let context = uv_test::test_context_with_versions!(&["3.12", "3.13"]);