diff --git a/crates/uv-python/src/version_files.rs b/crates/uv-python/src/version_files.rs index 3c50c42f4..894654a3c 100644 --- a/crates/uv-python/src/version_files.rs +++ b/crates/uv-python/src/version_files.rs @@ -6,6 +6,7 @@ use itertools::Itertools; use tracing::debug; use uv_dirs::user_uv_config_dir; use uv_fs::Simplified; +use uv_warnings::warn_user_once; use crate::PythonRequest; @@ -171,6 +172,17 @@ impl PythonVersionFile { }) .map(ToString::to_string) .map(|version| PythonRequest::parse(&version)) + .filter(|request| { + if let PythonRequest::ExecutableName(name) = request { + warn_user_once!( + "Ignoring unsupported Python request `{name}` in version file: {}", + path.display() + ); + false + } else { + true + } + }) .collect(); Ok(Some(Self { path, versions })) } diff --git a/crates/uv/src/commands/python/pin.rs b/crates/uv/src/commands/python/pin.rs index a07b07a96..dc11b3fe7 100644 --- a/crates/uv/src/commands/python/pin.rs +++ b/crates/uv/src/commands/python/pin.rs @@ -76,6 +76,10 @@ pub(crate) async fn pin( }; let request = PythonRequest::parse(&request); + if let PythonRequest::ExecutableName(name) = request { + bail!("Requests for arbitrary names (e.g., `{name}`) are not supported in version files"); + } + let python = match PythonInstallation::find( &request, EnvironmentPreference::OnlySystem, diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index 923133fe3..0acf825c3 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -217,6 +217,82 @@ fn python_find_pin() { "###); } +#[test] +fn python_find_pin_arbitrary_name() { + let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]); + + // Try to pin to an arbitrary name + uv_snapshot!(context.filters(), context.python_pin().arg("foo"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Requests for arbitrary names (e.g., `foo`) are not supported in version files + "); + + // Pin to an arbitrary name, bypassing uv + context + .temp_dir + .child(".python-version") + .write_str("foo") + .unwrap(); + + // The arbitrary name should be ignored + uv_snapshot!(context.filters(), context.python_find(), @r" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.11] + + ----- stderr ----- + warning: Ignoring unsupported Python request `foo` in version file: [TEMP_DIR]/.python-version + "); + + // The pin should be updatable + uv_snapshot!(context.filters(), context.python_pin().arg("3.11"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Pinned `.python-version` to `3.11` + + ----- stderr ----- + warning: Ignoring unsupported Python request `foo` in version file: [TEMP_DIR]/.python-version + "); + + // Warnings shouldn't appear afterwards... + uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Updated `.python-version` from `3.11` -> `3.12` + + ----- stderr ----- + "); + + // Pin in a sub-directory + context.temp_dir.child("foo").create_dir_all().unwrap(); + context + .temp_dir + .child("foo") + .child(".python-version") + .write_str("foo") + .unwrap(); + + // The arbitrary name should be ignored, but we won't walk up to the parent `.python-version` + // file (which contains 3.12); this behavior is a little questionable but we probably want to + // ignore all empty version files if we want to change this? + uv_snapshot!(context.filters(), context.python_find().current_dir(context.temp_dir.child("foo").path()), @r" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.11] + + ----- stderr ----- + warning: Ignoring unsupported Python request `foo` in version file: [TEMP_DIR]/foo/.python-version + "); +} + #[test] fn python_find_project() { let context: TestContext = TestContext::new_with_versions(&["3.10", "3.11", "3.12"]);