Include virtual environment interpreters in `uv python find` (#6521)

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.
This commit is contained in:
Zanie Blue 2024-08-23 16:06:57 -05:00 committed by GitHub
parent d1cbcb30e3
commit 6cf5d13183
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 196 additions and 8 deletions

View File

@ -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)]

View File

@ -18,9 +18,16 @@ pub(crate) async fn find(
request: Option<String>,
no_project: bool,
no_config: bool,
system: bool,
python_preference: PythonPreference,
cache: &Cache,
) -> Result<ExitStatus> {
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)
}

View File

@ -943,6 +943,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
args.request,
args.no_project,
cli.no_config,
args.system,
globals.python_preference,
&cache,
)

View File

@ -564,6 +564,7 @@ impl PythonUninstallSettings {
pub(crate) struct PythonFindSettings {
pub(crate) request: Option<String>,
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(),
}
}
}

View File

@ -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) {

View File

@ -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 -----
"###);
}

View File

@ -3233,6 +3233,12 @@ uv python find [OPTIONS] [REQUEST]
</ul>
</dd><dt><code>--quiet</code>, <code>-q</code></dt><dd><p>Do not print any output</p>
</dd><dt><code>--system</code></dt><dd><p>Only find system Python interpreters.</p>
<p>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.</p>
<p>The <code>--system</code> option instructs uv to skip virtual environment Python interpreters and restrict its search to the system path.</p>
</dd><dt><code>--verbose</code>, <code>-v</code></dt><dd><p>Use verbose output.</p>
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (&lt;https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives&gt;)</p>