Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/lock/export/pylock_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ pub struct PylockToml {
lock_version: Version,
created_by: String,
#[serde(skip_serializing_if = "Option::is_none")]
requires_python: Option<RequiresPython>,
pub requires_python: Option<RequiresPython>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub extras: Vec<ExtraName>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
Expand Down
11 changes: 11 additions & 0 deletions crates/uv/src/commands/pip/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,17 @@ pub(crate) async fn pip_install(
format!("Not a valid `pylock.toml` file: {}", pylock.user_display())
})?;

// Verify that the Python version is compatible with the lock file.
if let Some(requires_python) = lock.requires_python.as_ref() {
if !requires_python.contains(interpreter.python_version()) {
return Err(anyhow::anyhow!(
"The requested interpreter resolved to Python {}, which is incompatible with the `pylock.toml`'s Python requirement: `{}`",
interpreter.python_version(),
requires_python,
));
}
}

// Convert the extras and groups specifications into a concrete form.
let extras = extras.with_defaults(DefaultExtras::default());
let extras = extras
Expand Down
11 changes: 11 additions & 0 deletions crates/uv/src/commands/pip/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,17 @@ pub(crate) async fn pip_sync(
format!("Not a valid `pylock.toml` file: {}", pylock.user_display())
})?;

// Verify that the Python version is compatible with the lock file.
if let Some(requires_python) = lock.requires_python.as_ref() {
if !requires_python.contains(interpreter.python_version()) {
return Err(anyhow::anyhow!(
"The requested interpreter resolved to Python {}, which is incompatible with the `pylock.toml`'s Python requirement: `{}`",
interpreter.python_version(),
requires_python,
));
}
}

// Convert the extras and groups specifications into a concrete form.
let extras = extras.with_defaults(DefaultExtras::default());
let extras = extras
Expand Down
45 changes: 45 additions & 0 deletions crates/uv/tests/it/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11591,6 +11591,51 @@ requires_python = "==3.13.*"
Ok(())
}

#[test]
fn pep_751_requires_python() -> Result<()> {
let context = TestContext::new_with_versions(&["3.12", "3.13"]);

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = ["iniconfig"]
"#,
)?;

context
.export()
.arg("-o")
.arg("pylock.toml")
.assert()
.success();

context
.venv()
.arg("--python")
.arg("3.12")
.assert()
.success();

uv_snapshot!(context.filters(), context.pip_install()
.arg("--preview")
.arg("-r")
.arg("pylock.toml"), @r"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: The requested interpreter resolved to Python 3.12.[X], which is incompatible with the `pylock.toml`'s Python requirement: `>=3.13`
"
);

Ok(())
}

/// Test that uv doesn't hang if an index returns a distribution for the wrong package.
#[tokio::test]
async fn bogus_redirect() -> Result<()> {
Expand Down
Loading