From 137672f2dc63b374762a063c1fc539d8ca87d9e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 16:46:17 +0000 Subject: [PATCH 1/3] Replace `UV_TEST_PYTHON_PATH` with `UV_PYTHON_SEARCH_PATH` Promote the internal test-only `UV_TEST_PYTHON_PATH` environment variable to a user-facing `UV_PYTHON_SEARCH_PATH` that overrides `PATH` for Python executable discovery. Add dedicated integration test coverage in `python_find_search_path`. https://claude.ai/code/session_01XUesSCJ1o1nHBxFEBEzfZ3 --- crates/uv-python/src/discovery.rs | 6 +- crates/uv-python/src/lib.rs | 2 +- crates/uv-static/src/env_vars.rs | 10 +-- crates/uv-test/src/lib.rs | 8 +-- crates/uv/tests/it/pip_compile.rs | 2 +- crates/uv/tests/it/pip_compile_scenarios.rs | 2 +- crates/uv/tests/it/pip_install.rs | 4 +- crates/uv/tests/it/python_find.rs | 75 +++++++++++++++++--- crates/uv/tests/it/python_install.rs | 2 +- crates/uv/tests/it/python_list.rs | 12 ++-- crates/uv/tests/it/venv.rs | 6 +- scripts/scenarios/templates/compile.mustache | 2 +- 12 files changed, 96 insertions(+), 35 deletions(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 3d8179ce925f6..cfd19576bca23 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -458,7 +458,7 @@ fn python_executables_from_installed<'a>( } }; - env::var_os(EnvVars::UV_TEST_PYTHON_PATH) + env::var_os(EnvVars::UV_PYTHON_SEARCH_PATH) .is_none() .then(|| { registry_pythons() @@ -584,8 +584,8 @@ fn python_executables_from_search_path<'a>( version: &'a VersionRequest, implementation: Option<&'a ImplementationName>, ) -> impl Iterator + 'a { - // `UV_TEST_PYTHON_PATH` can be used to override `PATH` to limit Python executable availability in the test suite - let search_path = env::var_os(EnvVars::UV_TEST_PYTHON_PATH) + // `UV_PYTHON_SEARCH_PATH` can be used to override `PATH` for Python executable discovery + let search_path = env::var_os(EnvVars::UV_PYTHON_SEARCH_PATH) .unwrap_or(env::var_os(EnvVars::PATH).unwrap_or_default()); let possible_names: Vec<_> = version diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index e7d253380e4ee..a12d4f385ff21 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -219,7 +219,7 @@ mod tests { let mut run_vars = vec![ // Ensure `PATH` is used - (EnvVars::UV_TEST_PYTHON_PATH, None), + (EnvVars::UV_PYTHON_SEARCH_PATH, None), // Ignore active virtual environments (i.e. that the dev is using) (EnvVars::VIRTUAL_ENV, None), (EnvVars::PATH, path.as_deref()), diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index a66561d794af9..5d5c29b351220 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -496,10 +496,12 @@ impl EnvVars { #[attr_added_in("0.5.21")] pub const UV_VENV_SEED: &'static str = "UV_VENV_SEED"; - /// Used to override `PATH` to limit Python executable availability in the test suite. - #[attr_hidden] - #[attr_added_in("0.0.5")] - pub const UV_TEST_PYTHON_PATH: &'static str = "UV_TEST_PYTHON_PATH"; + /// Used to override `PATH` for Python executable discovery. + /// + /// When set, uv will search for Python interpreters in the directories specified by this + /// variable instead of `PATH`. + #[attr_added_in("0.7.12")] + pub const UV_PYTHON_SEARCH_PATH: &'static str = "UV_PYTHON_SEARCH_PATH"; /// Include resolver and installer output related to environment modifications. #[attr_hidden] diff --git a/crates/uv-test/src/lib.rs b/crates/uv-test/src/lib.rs index 0549407def3f8..1753417a5aad0 100755 --- a/crates/uv-test/src/lib.rs +++ b/crates/uv-test/src/lib.rs @@ -1183,7 +1183,7 @@ impl TestContext { /// but snapshotted to a string. /// * Use a fake `HOME` to avoid accidentally changing the developer's machine. /// * Hide other Pythons with `UV_PYTHON_INSTALL_DIR` and installed interpreters with - /// `UV_TEST_PYTHON_PATH` and an active venv (if applicable) by removing `VIRTUAL_ENV`. + /// `UV_PYTHON_SEARCH_PATH` and an active venv (if applicable) by removing `VIRTUAL_ENV`. /// * Increase the stack size to avoid stack overflows on windows due to large async functions. pub fn add_shared_options(&self, command: &mut Command, activate_venv: bool) { self.add_shared_args(command); @@ -1232,7 +1232,7 @@ impl TestContext { .env(EnvVars::UV_PYTHON_INSTALL_DIR, "") // Installations are not allowed by default; see `Self::with_managed_python_dirs` .env(EnvVars::UV_PYTHON_DOWNLOADS, "never") - .env(EnvVars::UV_TEST_PYTHON_PATH, self.python_path()) + .env(EnvVars::UV_PYTHON_SEARCH_PATH, self.python_path()) .env(EnvVars::UV_EXCLUDE_NEWER, TEST_TIMESTAMP) .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, TEST_TIMESTAMP) .env(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF, TEST_TIMESTAMP) @@ -2067,7 +2067,7 @@ pub fn create_venv_from_executable>( /// Create a `PATH` with the requested Python versions available in order. /// -/// Generally this should be used with `UV_TEST_PYTHON_PATH`. +/// Generally this should be used with `UV_PYTHON_SEARCH_PATH`. pub fn python_path_with_versions( temp_dir: &ChildPath, python_versions: &[&str], @@ -2082,7 +2082,7 @@ pub fn python_path_with_versions( /// Returns a list of Python executables for the given versions. /// -/// Generally this should be used with `UV_TEST_PYTHON_PATH`. +/// Generally this should be used with `UV_PYTHON_SEARCH_PATH`. pub fn python_installations_for_versions( temp_dir: &ChildPath, python_versions: &[&str], diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 2f71ce1c0ad75..464fa3dbb4d48 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -1908,7 +1908,7 @@ fn compile_fallback_interpreter_broken_in_path() -> Result<()> { .arg("--python-version") .arg("3.12") // In tests, we ignore `PATH` during Python discovery so we need to add the context `bin` - .env(EnvVars::UV_TEST_PYTHON_PATH, context.bin_dir.as_os_str()), @" + .env(EnvVars::UV_PYTHON_SEARCH_PATH, context.bin_dir.as_os_str()), @" success: true exit_code: 0 ----- stdout ----- diff --git a/crates/uv/tests/it/pip_compile_scenarios.rs b/crates/uv/tests/it/pip_compile_scenarios.rs index 17ad35fbe98a8..8215975a3d593 100644 --- a/crates/uv/tests/it/pip_compile_scenarios.rs +++ b/crates/uv/tests/it/pip_compile_scenarios.rs @@ -35,7 +35,7 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command { .arg(build_vendor_links_url()); context.add_shared_options(&mut command, true); command.env_remove(EnvVars::UV_EXCLUDE_NEWER); - command.env(EnvVars::UV_TEST_PYTHON_PATH, python_path); + command.env(EnvVars::UV_PYTHON_SEARCH_PATH, python_path); command } diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index f339bfe84b713..aec16cc03f397 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -9043,7 +9043,7 @@ fn install_incompatible_python_version_interpreter_broken_in_path() -> Result<() .arg("-p").arg("3.12") .arg("anyio") // In tests, we ignore `PATH` during Python discovery so we need to add the context `bin` - .env(EnvVars::UV_TEST_PYTHON_PATH, path.as_os_str()), @" + .env(EnvVars::UV_PYTHON_SEARCH_PATH, path.as_os_str()), @" success: false exit_code: 2 ----- stdout ----- @@ -9070,7 +9070,7 @@ fn install_incompatible_python_version_interpreter_broken_in_path() -> Result<() .arg("-p").arg("3.12") .arg("anyio") // In tests, we ignore `PATH` during Python discovery so we need to add the context `bin` - .env(EnvVars::UV_TEST_PYTHON_PATH, path.as_os_str()), @" + .env(EnvVars::UV_PYTHON_SEARCH_PATH, path.as_os_str()), @" success: false exit_code: 2 ----- stdout ----- diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index 65d5ab5ae5a87..71cd2d5a78c86 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -14,7 +14,7 @@ fn python_find() { uv_test::test_context_with_versions!(&["3.11", "3.12"]).with_filtered_python_sources(); // No interpreters on the path - uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_TEST_PYTHON_PATH, ""), @" + uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_PYTHON_SEARCH_PATH, ""), @" success: false exit_code: 2 ----- stdout ----- @@ -724,7 +724,7 @@ fn python_find_venv() { // Or at the front of the PATH #[cfg(not(windows))] - uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_TEST_PYTHON_PATH, child_dir.join(".venv").join("bin").as_os_str()), @" + uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_PYTHON_SEARCH_PATH, child_dir.join(".venv").join("bin").as_os_str()), @" success: true exit_code: 0 ----- stdout ----- @@ -743,7 +743,7 @@ fn python_find_venv() { ]) .unwrap(); - uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_TEST_PYTHON_PATH, path.as_os_str()), @" + uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_PYTHON_SEARCH_PATH, path.as_os_str()), @" success: true exit_code: 0 ----- stdout ----- @@ -762,7 +762,7 @@ fn python_find_venv() { ) .unwrap(); - uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_TEST_PYTHON_PATH, path.as_os_str()), @" + uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_PYTHON_SEARCH_PATH, path.as_os_str()), @" success: true exit_code: 0 ----- stdout ----- @@ -994,7 +994,7 @@ fn python_required_python_major_minor() { .unwrap(); // Find `python3.11`, which is `>=3.11.4`. - uv_snapshot!(context.filters(), context.python_find().arg(">=3.11.4, <3.12").env(EnvVars::UV_TEST_PYTHON_PATH, context.temp_dir.child("child").path()), @" + uv_snapshot!(context.filters(), context.python_find().arg(">=3.11.4, <3.12").env(EnvVars::UV_PYTHON_SEARCH_PATH, context.temp_dir.child("child").path()), @" success: true exit_code: 0 ----- stdout ----- @@ -1004,7 +1004,7 @@ fn python_required_python_major_minor() { "); // Find `python3.11`, which is `>3.11.4`. - uv_snapshot!(context.filters(), context.python_find().arg(">3.11.4, <3.12").env(EnvVars::UV_TEST_PYTHON_PATH, context.temp_dir.child("child").path()), @" + uv_snapshot!(context.filters(), context.python_find().arg(">3.11.4, <3.12").env(EnvVars::UV_PYTHON_SEARCH_PATH, context.temp_dir.child("child").path()), @" success: true exit_code: 0 ----- stdout ----- @@ -1014,7 +1014,7 @@ fn python_required_python_major_minor() { "); // Fail to find any matching Python interpreter. - uv_snapshot!(context.filters(), context.python_find().arg(">3.11.255, <3.12").env(EnvVars::UV_TEST_PYTHON_PATH, context.temp_dir.child("child").path()), @" + uv_snapshot!(context.filters(), context.python_find().arg(">3.11.255, <3.12").env(EnvVars::UV_PYTHON_SEARCH_PATH, context.temp_dir.child("child").path()), @" success: false exit_code: 2 ----- stdout ----- @@ -1167,7 +1167,7 @@ fn python_find_show_version() { uv_test::test_context_with_versions!(&["3.11", "3.12"]).with_filtered_python_sources(); // No interpreters found - uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_TEST_PYTHON_PATH, "").arg("--show-version"), @" + uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_PYTHON_SEARCH_PATH, "").arg("--show-version"), @" success: false exit_code: 2 ----- stdout ----- @@ -1536,3 +1536,62 @@ fn python_find_equal() { ----- stderr ----- "###); } + +/// Test that `UV_PYTHON_SEARCH_PATH` overrides `PATH` for Python discovery. +#[test] +fn python_find_search_path() { + let context = + uv_test::test_context_with_versions!(&["3.11", "3.12"]).with_filtered_python_sources(); + + // When `UV_PYTHON_SEARCH_PATH` is empty, no interpreters are found + uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_PYTHON_SEARCH_PATH, ""), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No interpreter found in [PYTHON SOURCES] + "); + + // When `UV_PYTHON_SEARCH_PATH` is set, it is used instead of `PATH` + uv_snapshot!(context.filters(), context.python_find(), @" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.11] + + ----- stderr ----- + "); + + // We can use `UV_PYTHON_SEARCH_PATH` to control which Python versions are visible + let python_path_3_12_only = std::env::join_paths( + std::env::split_paths(&context.python_path()) + .filter(|p| p.to_string_lossy().contains("3.12")), + ) + .unwrap(); + uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_PYTHON_SEARCH_PATH, python_path_3_12_only.as_os_str()), @" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.12] + + ----- stderr ----- + "); + + // We can use `UV_PYTHON_SEARCH_PATH` to control the order of Python versions + let reversed_path = std::env::join_paths( + std::env::split_paths(&context.python_path()) + .collect::>() + .into_iter() + .rev(), + ) + .unwrap(); + uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_PYTHON_SEARCH_PATH, reversed_path.as_os_str()), @" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.12] + + ----- stderr ----- + "); +} diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index dbbfac16e6549..9c52ffeab7cde 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -333,7 +333,7 @@ fn python_install_automatic() { uv_snapshot!(context.filters(), context.run() .env_remove(EnvVars::VIRTUAL_ENV) // In tests, we ignore `PATH` during Python discovery so we need to add the context `bin` - .env(EnvVars::UV_TEST_PYTHON_PATH, context.bin_dir.as_os_str()) + .env(EnvVars::UV_PYTHON_SEARCH_PATH, context.bin_dir.as_os_str()) .arg("-p").arg("3.11") .arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @" success: true diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs index 8cd223ada76d8..9a2e41ae5d3a8 100644 --- a/crates/uv/tests/it/python_list.rs +++ b/crates/uv/tests/it/python_list.rs @@ -15,7 +15,7 @@ fn python_list() { .with_filtered_python_keys() .with_collapsed_whitespace(); - uv_snapshot!(context.filters(), context.python_list().env(EnvVars::UV_TEST_PYTHON_PATH, ""), @" + uv_snapshot!(context.filters(), context.python_list().env(EnvVars::UV_PYTHON_SEARCH_PATH, ""), @" success: true exit_code: 0 ----- stdout ----- @@ -303,7 +303,7 @@ fn python_list_duplicate_path_entries() { ) .unwrap(); - uv_snapshot!(context.filters(), context.python_list().env(EnvVars::UV_TEST_PYTHON_PATH, &path), @" + uv_snapshot!(context.filters(), context.python_list().env(EnvVars::UV_PYTHON_SEARCH_PATH, &path), @" success: true exit_code: 0 ----- stdout ----- @@ -325,7 +325,7 @@ fn python_list_duplicate_path_entries() { )) .unwrap(); - uv_snapshot!(context.filters(), context.python_list().env(EnvVars::UV_TEST_PYTHON_PATH, &path), @" + uv_snapshot!(context.filters(), context.python_list().env(EnvVars::UV_PYTHON_SEARCH_PATH, &path), @" success: true exit_code: 0 ----- stdout ----- @@ -346,7 +346,7 @@ fn python_list_duplicate_path_entries() { ) .unwrap(); - uv_snapshot!(context.filters(), context.python_list().env(EnvVars::UV_TEST_PYTHON_PATH, &path), @" + uv_snapshot!(context.filters(), context.python_list().env(EnvVars::UV_PYTHON_SEARCH_PATH, &path), @" success: true exit_code: 0 ----- stdout ----- @@ -527,7 +527,7 @@ fn python_list_managed_symlinks() { .arg("3.10") .arg("--only-installed") .arg("--no-managed-python") - .env(EnvVars::UV_TEST_PYTHON_PATH, &bin_dir), @" + .env(EnvVars::UV_PYTHON_SEARCH_PATH, &bin_dir), @" success: true exit_code: 0 ----- stdout ----- @@ -540,7 +540,7 @@ fn python_list_managed_symlinks() { .arg("3.10") .arg("--only-installed") .arg("--managed-python") - .env(EnvVars::UV_TEST_PYTHON_PATH, &bin_dir), @" + .env(EnvVars::UV_PYTHON_SEARCH_PATH, &bin_dir), @" success: true exit_code: 0 ----- stdout ----- diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index 89460e2500cf8..d3bf5eb7b2227 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -923,7 +923,7 @@ fn create_venv_unknown_python_minor() { .arg("--python") .arg("3.100") // Unset this variable to force what the user would see - .env_remove(EnvVars::UV_TEST_PYTHON_PATH); + .env_remove(EnvVars::UV_PYTHON_SEARCH_PATH); uv_snapshot!(context.filters(), &mut command, @" success: false @@ -949,7 +949,7 @@ fn create_venv_unknown_python_patch() { .arg("--python") .arg("3.12.100") // Unset this variable to force what the user would see - .env_remove(EnvVars::UV_TEST_PYTHON_PATH); + .env_remove(EnvVars::UV_PYTHON_SEARCH_PATH); uv_snapshot!(context.filters(), &mut command, @" success: false @@ -1211,7 +1211,7 @@ fn windows_shims() -> Result<()> { // Create a virtual environment at `.venv` with the shim uv_snapshot!(context.filters(), context.venv() .arg(context.venv.as_os_str()) - .env(EnvVars::UV_TEST_PYTHON_PATH, format!("{};{}", shim_path.display(), context.python_path().to_string_lossy())), @r###" + .env(EnvVars::UV_PYTHON_SEARCH_PATH, format!("{};{}", shim_path.display(), context.python_path().to_string_lossy())), @r###" success: true exit_code: 0 ----- stdout ----- diff --git a/scripts/scenarios/templates/compile.mustache b/scripts/scenarios/templates/compile.mustache index 0955df3bfbf16..066357a7156ca 100644 --- a/scripts/scenarios/templates/compile.mustache +++ b/scripts/scenarios/templates/compile.mustache @@ -35,7 +35,7 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command { .arg(build_vendor_links_url()); context.add_shared_options(&mut command, true); command.env_remove(EnvVars::UV_EXCLUDE_NEWER); - command.env(EnvVars::UV_TEST_PYTHON_PATH, python_path); + command.env(EnvVars::UV_PYTHON_SEARCH_PATH, python_path); command } From 318a42870df4911bedd768956bf396220f907087 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 17 Apr 2026 12:24:01 -0500 Subject: [PATCH 2/3] Apply suggestion from @zanieb --- crates/uv-static/src/env_vars.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 5d5c29b351220..c18082d6a51de 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -500,7 +500,7 @@ impl EnvVars { /// /// When set, uv will search for Python interpreters in the directories specified by this /// variable instead of `PATH`. - #[attr_added_in("0.7.12")] + #[attr_added_in("next release")] pub const UV_PYTHON_SEARCH_PATH: &'static str = "UV_PYTHON_SEARCH_PATH"; /// Include resolver and installer output related to environment modifications. From 21e63b08f1bda26dc22fb0f780cded3e5b265002 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 17 Apr 2026 12:49:52 -0500 Subject: [PATCH 3/3] Fix Windows registry test for UV_PYTHON_SEARCH_PATH --- crates/uv/tests/it/python_install.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index 9c52ffeab7cde..51e68a4c61520 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -1266,7 +1266,7 @@ fn python_install_freethreaded_and_gil_list() { .success(); // List installed versions with registry discovery enabled. - // We remove UV_TEST_PYTHON_PATH to enable registry discovery (it's skipped when set), + // We remove `UV_PYTHON_SEARCH_PATH` to enable registry discovery (it's skipped when set), // and use `--managed-python --only-installed` to exclude unrelated system Pythons. // // Both the GIL and freethreaded variants should show entries from: @@ -1277,7 +1277,7 @@ fn python_install_freethreaded_and_gil_list() { .arg("3.13") .arg("--only-installed") .arg("--managed-python") - .env_remove(EnvVars::UV_TEST_PYTHON_PATH) + .env_remove(EnvVars::UV_PYTHON_SEARCH_PATH) .env(EnvVars::UV_PYTHON_INSTALL_REGISTRY, "1"), @" success: true exit_code: 0 @@ -1293,7 +1293,7 @@ fn python_install_freethreaded_and_gil_list() { .arg("3.13t") .arg("--only-installed") .arg("--managed-python") - .env_remove(EnvVars::UV_TEST_PYTHON_PATH) + .env_remove(EnvVars::UV_PYTHON_SEARCH_PATH) .env(EnvVars::UV_PYTHON_INSTALL_REGISTRY, "1"), @" success: true exit_code: 0