From 6cf5d131832ddf9abd35f125cb88eef0f56fa67b Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 23 Aug 2024 16:06:57 -0500 Subject: [PATCH] Include virtual environment interpreters in `uv python find` (#6521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, we excluded these and only looked at system interpreters. However, it makes sense for this to match the typical Python discovery experience. We could consider swapping the default... I'm not sure what makes more sense. If we change the default (as written now) — this could arguably be a breaking change. --- crates/uv-cli/src/lib.rs | 19 ++++ crates/uv/src/commands/python/find.rs | 14 ++- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 4 + crates/uv/tests/common/mod.rs | 3 +- crates/uv/tests/python_find.rs | 157 +++++++++++++++++++++++++- docs/reference/cli.md | 6 + 7 files changed, 196 insertions(+), 8 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index b6b0ecbcf..729a93c09 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3063,6 +3063,25 @@ pub struct PythonFindArgs { /// directory or parent directories will be used. #[arg(long, alias = "no_workspace")] pub no_project: bool, + + /// Only find system Python interpreters. + /// + /// By default, uv will report the first Python interpreter it would use, including those in an + /// active virtual environment or a virtual environment in the current working directory or any + /// parent directory. + /// + /// The `--system` option instructs uv to skip virtual environment Python interpreters and + /// restrict its search to the system path. + #[arg( + long, + env = "UV_SYSTEM_PYTHON", + value_parser = clap::builder::BoolishValueParser::new(), + overrides_with("no_system") + )] + pub system: bool, + + #[arg(long, overrides_with("system"), hide = true)] + pub no_system: bool, } #[derive(Args)] diff --git a/crates/uv/src/commands/python/find.rs b/crates/uv/src/commands/python/find.rs index a1384f677..e17123938 100644 --- a/crates/uv/src/commands/python/find.rs +++ b/crates/uv/src/commands/python/find.rs @@ -18,9 +18,16 @@ pub(crate) async fn find( request: Option, no_project: bool, no_config: bool, + system: bool, python_preference: PythonPreference, cache: &Cache, ) -> Result { + let environment_preference = if system { + EnvironmentPreference::OnlySystem + } else { + EnvironmentPreference::Any + }; + // (1) Explicit request from user let mut request = request.map(|request| PythonRequest::parse(&request)); @@ -56,12 +63,15 @@ pub(crate) async fn find( let python = PythonInstallation::find( &request.unwrap_or_default(), - EnvironmentPreference::OnlySystem, + environment_preference, python_preference, cache, )?; - println!("{}", python.interpreter().sys_executable().user_display()); + println!( + "{}", + uv_fs::absolutize_path(python.interpreter().sys_executable())?.simplified_display() + ); Ok(ExitStatus::Success) } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index c119b9080..5a2db2468 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -943,6 +943,7 @@ async fn run(cli: Cli) -> Result { args.request, args.no_project, cli.no_config, + args.system, globals.python_preference, &cache, ) diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index ad2b5b2a3..3284a3ba1 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -564,6 +564,7 @@ impl PythonUninstallSettings { pub(crate) struct PythonFindSettings { pub(crate) request: Option, pub(crate) no_project: bool, + pub(crate) system: bool, } impl PythonFindSettings { @@ -573,11 +574,14 @@ impl PythonFindSettings { let PythonFindArgs { request, no_project, + system, + no_system, } = args; Self { request, no_project, + system: flag(system, no_system).unwrap_or_default(), } } } diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index de3558354..b33c09e11 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -134,7 +134,8 @@ impl TestContext { self } - /// Add extra standard filtering for Python executable names. + /// Add extra standard filtering for Python executable names, e.g., stripping version number + /// and `.exe` suffixes. #[must_use] pub fn with_filtered_python_names(mut self) -> Self { if cfg!(windows) { diff --git a/crates/uv/tests/python_find.rs b/crates/uv/tests/python_find.rs index 7b27406a4..92ce40053 100644 --- a/crates/uv/tests/python_find.rs +++ b/crates/uv/tests/python_find.rs @@ -1,7 +1,8 @@ #![cfg(all(feature = "python", feature = "pypi"))] -use assert_fs::fixture::FileWriteStr; use assert_fs::prelude::PathChild; +use assert_fs::{fixture::FileWriteStr, prelude::PathCreateDir}; +use fs_err::remove_dir_all; use indoc::indoc; use common::{uv_snapshot, TestContext}; @@ -21,7 +22,7 @@ fn python_find() { ----- stdout ----- ----- stderr ----- - error: No interpreter found in system path or `py` launcher + error: No interpreter found in virtual environments, system path, or `py` launcher "###); } else { uv_snapshot!(context.filters(), context.python_find().env("UV_TEST_PYTHON_PATH", ""), @r###" @@ -30,7 +31,7 @@ fn python_find() { ----- stdout ----- ----- stderr ----- - error: No interpreter found in system path + error: No interpreter found in virtual environments or system path "###); } @@ -117,7 +118,7 @@ fn python_find() { ----- stdout ----- ----- stderr ----- - error: No interpreter found for PyPy in system path or `py` launcher + error: No interpreter found for PyPy in virtual environments, system path, or `py` launcher "###); } else { uv_snapshot!(context.filters(), context.python_find().arg("pypy"), @r###" @@ -126,7 +127,7 @@ fn python_find() { ----- stdout ----- ----- stderr ----- - error: No interpreter found for PyPy in system path + error: No interpreter found for PyPy in virtual environments or system path "###); } @@ -243,3 +244,149 @@ fn python_find_project() { ----- stderr ----- "###); } + +#[test] +fn python_find_venv() { + let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]) + // Enable additional filters for Windows compatibility + .with_filtered_exe_suffix() + .with_filtered_python_names() + .with_filtered_virtualenv_bin(); + + // Create a virtual environment + uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.12").arg("-q"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "###); + + // We should find it first + // TODO(zanieb): On Windows, this has in a different display path for virtual environments which + // is super annoying and requires some changes to how we represent working directories in the + // test context to resolve. + #[cfg(not(windows))] + uv_snapshot!(context.filters(), context.python_find(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [VENV]/[BIN]/python + + ----- stderr ----- + "###); + + // Even if the `VIRTUAL_ENV` is not set (the test context includes this by default) + #[cfg(not(windows))] + uv_snapshot!(context.filters(), context.python_find().env_remove("VIRTUAL_ENV"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [VENV]/[BIN]/python + + ----- stderr ----- + "###); + + let child_dir = context.temp_dir.child("child"); + child_dir.create_dir_all().unwrap(); + + // Unless the system flag is passed + uv_snapshot!(context.filters(), context.python_find().arg("--system"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.11] + + ----- stderr ----- + "###); + + // Or, `UV_SYSTEM_PYTHON` is set + uv_snapshot!(context.filters(), context.python_find().env("UV_SYSTEM_PYTHON", "1"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.11] + + ----- stderr ----- + "###); + + // Unless, `--no-system` is included + // TODO(zanieb): Report this as a bug upstream — this should be allowed. + uv_snapshot!(context.filters(), context.python_find().arg("--no-system").env("UV_SYSTEM_PYTHON", "1"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--no-system' cannot be used with '--system' + + Usage: uv python find --cache-dir [CACHE_DIR] [REQUEST] + + For more information, try '--help'. + "###); + + // We should find virtual environments from a child directory + #[cfg(not(windows))] + uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir).env_remove("VIRTUAL_ENV"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [VENV]/[BIN]/python + + ----- stderr ----- + "###); + + // A virtual environment in the child directory takes precedence over the parent + uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11").arg("-q").current_dir(&child_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "###); + + #[cfg(not(windows))] + uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir).env_remove("VIRTUAL_ENV"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/child/.venv/[BIN]/python + + ----- stderr ----- + "###); + + // But if we delete the parent virtual environment + remove_dir_all(context.temp_dir.child(".venv")).unwrap(); + + // And query from there... we should not find the child virtual environment + uv_snapshot!(context.filters(), context.python_find(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.11] + + ----- stderr ----- + "###); + + // Unless, it is requested by path + #[cfg(not(windows))] + uv_snapshot!(context.filters(), context.python_find().arg("child/.venv"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/child/.venv/[BIN]/python + + ----- stderr ----- + "###); + + // Or activated via `VIRTUAL_ENV` + #[cfg(not(windows))] + uv_snapshot!(context.filters(), context.python_find().env("VIRTUAL_ENV", child_dir.join(".venv").as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/child/.venv/[BIN]/python + + ----- stderr ----- + "###); +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 7d314747c..f93621a70 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -3233,6 +3233,12 @@ uv python find [OPTIONS] [REQUEST]
--quiet, -q

Do not print any output

+
--system

Only find system Python interpreters.

+ +

By default, uv will report the first Python interpreter it would use, including those in an active virtual environment or a virtual environment in the current working directory or any parent directory.

+ +

The --system option instructs uv to skip virtual environment Python interpreters and restrict its search to the system path.

+
--verbose, -v

Use verbose output.

You can configure fine-grained logging using the RUST_LOG environment variable. (<https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>)