Skip to content

Commit

Permalink
uv-tool/install: ignore existing environments on interpreter mismatch (
Browse files Browse the repository at this point in the history
…#7451)

This changes `uv tool install` behavior with regards to re-using
existing environments.
In particular, this replaces the existing version-matching logic with a
tighter one, enforcing
a same-interpreter match.
This allows to properly switch between system and managed interpreter,
at the cost of
more eagerly invalidating existing environments every time there is an
interpreter change.

Closes: #7320
  • Loading branch information
lucab committed Sep 18, 2024
1 parent fda2276 commit 969b4a2
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 18 deletions.
2 changes: 1 addition & 1 deletion crates/uv-tool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ impl InstalledTools {
match PythonEnvironment::from_root(&environment_path, cache) {
Ok(venv) => {
debug!(
"Using existing environment for tool `{name}`: {}",
"Found existing environment for tool `{name}`: {}",
environment_path.user_display()
);
Ok(Some(venv))
Expand Down
36 changes: 22 additions & 14 deletions crates/uv/src/commands/tool/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use owo_colors::OwoColorize;
use pep440_rs::{VersionSpecifier, VersionSpecifiers};
use pep508_rs::MarkerTree;
use pypi_types::{Requirement, RequirementSource};
use tracing::debug;
use tracing::trace;
use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
use uv_client::{BaseClientBuilder, Connectivity};
Expand Down Expand Up @@ -276,19 +276,27 @@ pub(crate) async fn install(
installed_tools
.get_environment(&from.name, &cache)?
.filter(|environment| {
python_request.as_ref().map_or(true, |python_request| {
if python_request.satisfied(environment.interpreter(), &cache) {
debug!("Found existing environment for `{from}`", from = from.name.cyan());
true
} else {
let _ = writeln!(
printer.stderr(),
"Existing environment for `{from}` does not satisfy the requested Python interpreter",
from = from.name.cyan(),
);
false
}
})
// NOTE(lucab): this compares `base_prefix` paths as a proxy for
// detecting interpreters mismatches. Directly comparing interpreters
// (by paths or binaries on-disk) would result in several false
// positives on Windows due to file-copying and shims.
let old_base_prefix = environment.interpreter().sys_base_prefix();
let selected_base_prefix = interpreter.sys_base_prefix();
if old_base_prefix == selected_base_prefix {
trace!(
"Existing interpreter matches the requested interpreter for `{}`: {}",
from.name,
environment.interpreter().sys_executable().display()
);
true
} else {
let _ = writeln!(
printer.stderr(),
"Ignoring existing environment for `{from}`: the requested Python interpreter does not match the environment interpreter",
from = from.name.cyan(),
);
false
}
});

// If the requested and receipt requirements are the same...
Expand Down
150 changes: 147 additions & 3 deletions crates/uv/tests/tool_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2074,7 +2074,7 @@ fn tool_install_upgrade() {

/// Test reinstalling tools with varying `--python` requests.
#[test]
fn tool_install_python_request() {
fn tool_install_python_requests() {
let context = TestContext::new_with_versions(&["3.11", "3.12"])
.with_filtered_counts()
.with_filtered_exe_suffix();
Expand Down Expand Up @@ -2122,7 +2122,7 @@ fn tool_install_python_request() {
`black` is already installed
"###);

// Install with Python 3.11 (incompatible).
// // Install with Python 3.11 (incompatible).
uv_snapshot!(context.filters(), context.tool_install()
.arg("-p")
.arg("3.11")
Expand All @@ -2135,7 +2135,7 @@ fn tool_install_python_request() {
----- stdout -----
----- stderr -----
Existing environment for `black` does not satisfy the requested Python interpreter
Ignoring existing environment for `black`: the requested Python interpreter does not match the environment interpreter
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
Expand All @@ -2149,6 +2149,150 @@ fn tool_install_python_request() {
"###);
}

/// Test reinstalling tools with varying `--python` and
/// `--python-preference` parameters.
#[ignore = "https://github.com/astral-sh/uv/issues/7473"]
#[test]
fn tool_install_python_preference() {
let context = TestContext::new_with_versions(&["3.11", "3.12"])
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");

// Install `black`.
uv_snapshot!(context.filters(), context.tool_install()
.arg("-p")
.arg("3.12")
.arg("black")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ black==24.3.0
+ click==8.1.7
+ mypy-extensions==1.0.0
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed 2 executables: black, blackd
"###);

// Install with Python 3.12 (compatible).
uv_snapshot!(context.filters(), context.tool_install()
.arg("-p")
.arg("3.12")
.arg("black")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
`black` is already installed
"###);

// Install with system Python 3.11 (different version, incompatible).
uv_snapshot!(context.filters(), context.tool_install()
.arg("-p")
.arg("3.11")
.arg("--python-preference")
.arg("only-system")
.arg("black")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Ignoring existing environment for `black`: the requested Python interpreter does not match the environment interpreter
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ black==24.3.0
+ click==8.1.7
+ mypy-extensions==1.0.0
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed 2 executables: black, blackd
"###);

// Install with system Python 3.11 (compatible).
uv_snapshot!(context.filters(), context.tool_install()
.arg("-p")
.arg("3.11")
.arg("--python-preference")
.arg("only-system")
.arg("black")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
`black` is already installed
"###);

// Install with managed Python 3.11 (different source, incompatible).
uv_snapshot!(context.filters(), context.tool_install()
.arg("-p")
.arg("3.11")
.arg("--python-preference")
.arg("only-managed")
.arg("black")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Ignoring existing environment for `black`: the requested Python interpreter does not match the environment interpreter
Resolved [N] packages in [TIME]
Installed [N] packages in [TIME]
+ black==24.3.0
+ click==8.1.7
+ mypy-extensions==1.0.0
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed 2 executables: black, blackd
"###);

// Install with managed Python 3.11 (compatible).
uv_snapshot!(context.filters(), context.tool_install()
.arg("-p")
.arg("3.11")
.arg("--python-preference")
.arg("only-managed")
.arg("black")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
`black` is already installed
"###);
}

/// Test preserving a tool environment when new but incompatible requirements are requested.
#[test]
fn tool_install_preserve_environment() {
Expand Down

0 comments on commit 969b4a2

Please sign in to comment.