Skip to content

Commit

Permalink
Include virtual environment interpreters in uv python find (astral-…
Browse files Browse the repository at this point in the history
…sh#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.
  • Loading branch information
zanieb authored Aug 23, 2024
1 parent d1cbcb3 commit 6cf5d13
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 8 deletions.
19 changes: 19 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
14 changes: 12 additions & 2 deletions crates/uv/src/commands/python/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
4 changes: 4 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(),
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/uv/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
157 changes: 152 additions & 5 deletions crates/uv/tests/python_find.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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###"
Expand All @@ -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
"###);
}

Expand Down Expand Up @@ -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###"
Expand All @@ -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
"###);
}

Expand Down Expand Up @@ -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 -----
"###);
}
6 changes: 6 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit 6cf5d13

Please sign in to comment.