From 989b103171f17d1ac6b3f2f2d730ec1109861c08 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 5 Feb 2025 10:12:19 -0600 Subject: [PATCH] Add support for respecting `VIRTUAL_ENV` in project commands via `--active` (#11189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I think `UV_PROJECT_ENVIRONMENT` is too complicated for use-cases where the user wants to sync to the active environment. I don't see a compelling reason not to make opt-in easier. I see a lot of questions about how to deal with this warning in the issue tracker, but it seems painful to collect them here for posterity. A notable behavior here — we'll treat this as equivalent to `UV_PROJECT_ENVIRONMENT` so... if you point us to a valid virtual environment that needs to be recreated for some reason (e.g., new Python version request), we'll happily delete it and start over. --- crates/uv-cli/src/lib.rs | 52 +++++++++ crates/uv-workspace/src/workspace.rs | 16 ++- crates/uv/src/commands/project/add.rs | 3 + crates/uv/src/commands/project/export.rs | 1 + crates/uv/src/commands/project/lock.rs | 1 + crates/uv/src/commands/project/mod.rs | 7 +- crates/uv/src/commands/project/remove.rs | 3 + crates/uv/src/commands/project/run.rs | 2 + crates/uv/src/commands/project/sync.rs | 2 + crates/uv/src/commands/project/tree.rs | 1 + crates/uv/src/commands/venv.rs | 2 +- crates/uv/src/lib.rs | 4 + crates/uv/src/settings.rs | 16 +++ crates/uv/tests/it/init.rs | 6 +- crates/uv/tests/it/lock.rs | 1 + crates/uv/tests/it/run.rs | 108 ++++++++++++++++-- crates/uv/tests/it/sync.rs | 133 ++++++++++++++++++++++- crates/uv/tests/it/workspace.rs | 14 +-- docs/concepts/projects/config.md | 5 +- docs/reference/cli.md | 24 +++- 20 files changed, 368 insertions(+), 33 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 27a0edfc8..ddbcd4b97 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2805,6 +2805,19 @@ pub struct RunArgs { #[arg(long)] pub isolated: bool, + /// Prefer the active virtual environment over the project's virtual environment. + /// + /// If the project virtual environment is active or no virtual environment is active, this has + /// no effect. + #[arg(long, overrides_with = "no_active")] + pub active: bool, + + /// Prefer project's virtual environment over an active environment. + /// + /// This is the default behavior. + #[arg(long, overrides_with = "active", hide = true)] + pub no_active: bool, + /// Avoid syncing the virtual environment. /// /// Implies `--frozen`, as the project dependencies will be ignored (i.e., the lockfile will not @@ -3004,6 +3017,19 @@ pub struct SyncArgs { #[arg(long, overrides_with("inexact"), hide = true)] pub exact: bool, + /// Prefer the active virtual environment over the project's virtual environment. + /// + /// If the project virtual environment is active or no virtual environment is active, this has + /// no effect. + #[arg(long, overrides_with = "no_active")] + pub active: bool, + + /// Prefer project's virtual environment over an active environment. + /// + /// This is the default behavior. + #[arg(long, overrides_with = "active", hide = true)] + pub no_active: bool, + /// Do not install the current project. /// /// By default, the current project is installed into the environment with all of its @@ -3247,6 +3273,19 @@ pub struct AddArgs { #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "locked")] pub frozen: bool, + /// Prefer the active virtual environment over the project's virtual environment. + /// + /// If the project virtual environment is active or no virtual environment is active, this has + /// no effect. + #[arg(long, overrides_with = "no_active")] + pub active: bool, + + /// Prefer project's virtual environment over an active environment. + /// + /// This is the default behavior. + #[arg(long, overrides_with = "active", hide = true)] + pub no_active: bool, + #[command(flatten)] pub installer: ResolverInstallerArgs, @@ -3313,6 +3352,19 @@ pub struct RemoveArgs { #[arg(long, env = EnvVars::UV_NO_SYNC, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "frozen")] pub no_sync: bool, + /// Prefer the active virtual environment over the project's virtual environment. + /// + /// If the project virtual environment is active or no virtual environment is active, this has + /// no effect. + #[arg(long, overrides_with = "no_active")] + pub active: bool, + + /// Prefer project's virtual environment over an active environment. + /// + /// This is the default behavior. + #[arg(long, overrides_with = "active", hide = true)] + pub no_active: bool, + /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index b974ab018..e2d67c583 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -538,7 +538,7 @@ impl Workspace { /// /// If `UV_PROJECT_ENVIRONMENT` is set, it will take precedence. If a relative path is provided, /// it is resolved relative to the install path. - pub fn venv(&self) -> PathBuf { + pub fn venv(&self, active: bool) -> PathBuf { /// Resolve the `UV_PROJECT_ENVIRONMENT` value, if any. fn from_project_environment_variable(workspace: &Workspace) -> Option { let value = std::env::var_os(EnvVars::UV_PROJECT_ENVIRONMENT)?; @@ -606,12 +606,24 @@ impl Workspace { // Warn if it conflicts with `VIRTUAL_ENV` if let Some(from_virtual_env) = from_virtual_env_variable() { if !is_same_dir(&from_virtual_env, &project_env).unwrap_or(false) { + if active { + debug!( + "Using active virtual environment `{}` instead of project environment `{}`", + from_virtual_env.user_display(), + project_env.user_display() + ); + return from_virtual_env; + } warn_user_once!( - "`VIRTUAL_ENV={}` does not match the project environment path `{}` and will be ignored", + "`VIRTUAL_ENV={}` does not match the project environment path `{}` and will be ignored; use `--active` to target the active environment instead", from_virtual_env.user_display(), project_env.user_display() ); } + } else { + debug!( + "Use of the active virtual environment was requested, but `VIRTUAL_ENV` is not set" + ); } project_env diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 16ad66126..04d2fc7d3 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -61,6 +61,7 @@ pub(crate) async fn add( project_dir: &Path, locked: bool, frozen: bool, + active: bool, no_sync: bool, requirements: Vec, editable: Option, @@ -213,6 +214,7 @@ pub(crate) async fn add( allow_insecure_host, &install_mirrors, no_config, + active, cache, printer, ) @@ -231,6 +233,7 @@ pub(crate) async fn add( connectivity, native_tls, allow_insecure_host, + active, no_config, cache, printer, diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index b7deb5853..b99af5dc6 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -163,6 +163,7 @@ pub(crate) async fn export( allow_insecure_host, &install_mirrors, no_config, + false, cache, printer, ) diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 886b59961..2d59db1bb 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -121,6 +121,7 @@ pub(crate) async fn lock( allow_insecure_host, &install_mirrors, no_config, + false, cache, printer, ) diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 3672bca03..829353b96 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -602,6 +602,7 @@ impl ProjectInterpreter { allow_insecure_host: &[TrustedHost], install_mirrors: &PythonInstallMirrors, no_config: bool, + active: bool, cache: &Cache, printer: Printer, ) -> Result { @@ -614,7 +615,7 @@ impl ProjectInterpreter { .await?; // Read from the virtual environment first. - let venv = workspace.venv(); + let venv = workspace.venv(active); match PythonEnvironment::from_root(&venv, cache) { Ok(venv) => { if python_request.as_ref().map_or(true, |request| { @@ -924,6 +925,7 @@ pub(crate) async fn get_or_init_environment( native_tls: bool, allow_insecure_host: &[TrustedHost], no_config: bool, + active: bool, cache: &Cache, printer: Printer, ) -> Result { @@ -938,6 +940,7 @@ pub(crate) async fn get_or_init_environment( allow_insecure_host, install_mirrors, no_config, + active, cache, printer, ) @@ -948,7 +951,7 @@ pub(crate) async fn get_or_init_environment( // Otherwise, create a virtual environment with the discovered interpreter. ProjectInterpreter::Interpreter(interpreter) => { - let venv = workspace.venv(); + let venv = workspace.venv(active); // Avoid removing things that are not virtual environments let should_remove = match (venv.try_exists(), venv.join("pyvenv.cfg").try_exists()) { diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 1cced9d13..68cf85886 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -43,6 +43,7 @@ pub(crate) async fn remove( project_dir: &Path, locked: bool, frozen: bool, + active: bool, no_sync: bool, packages: Vec, dependency_type: DependencyType, @@ -209,6 +210,7 @@ pub(crate) async fn remove( allow_insecure_host, &install_mirrors, no_config, + active, cache, printer, ) @@ -228,6 +230,7 @@ pub(crate) async fn remove( native_tls, allow_insecure_host, no_config, + active, cache, printer, ) diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 758da031b..ff13a37eb 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -65,6 +65,7 @@ pub(crate) async fn run( show_resolution: bool, locked: bool, frozen: bool, + active: bool, no_sync: bool, isolated: bool, all_packages: bool, @@ -666,6 +667,7 @@ pub(crate) async fn run( native_tls, allow_insecure_host, no_config, + active, cache, printer, ) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 3b7e54a6f..6d1de4e8a 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -45,6 +45,7 @@ pub(crate) async fn sync( project_dir: &Path, locked: bool, frozen: bool, + active: bool, all_packages: bool, package: Option, extras: ExtrasSpecification, @@ -125,6 +126,7 @@ pub(crate) async fn sync( native_tls, allow_insecure_host, no_config, + active, cache, printer, ) diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 37a993ab2..ad6af1d07 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -118,6 +118,7 @@ pub(crate) async fn tree( allow_insecure_host, &install_mirrors, no_config, + false, cache, printer, ) diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index cf47154ff..4df5fba3e 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -187,7 +187,7 @@ async fn venv_impl( // This isn't strictly necessary and we may want to change it later, but this // avoids a breaking change when adding project environment support to `uv venv`. (project.workspace().install_path() == project_dir) - .then(|| project.workspace().venv()) + .then(|| project.workspace().venv(false)) }) .unwrap_or(PathBuf::from(".venv")), ); diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index b24ac5de0..3bb5a4b2b 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1484,6 +1484,7 @@ async fn run_project( args.show_resolution || globals.verbose > 0, args.locked, args.frozen, + args.active, args.no_sync, args.isolated, args.all_packages, @@ -1528,6 +1529,7 @@ async fn run_project( project_dir, args.locked, args.frozen, + args.active, args.all_packages, args.package, args.extras, @@ -1619,6 +1621,7 @@ async fn run_project( project_dir, args.locked, args.frozen, + args.active, args.no_sync, requirements, args.editable, @@ -1671,6 +1674,7 @@ async fn run_project( project_dir, args.locked, args.frozen, + args.active, args.no_sync, args.packages, args.dependency_type, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index b1ec7072e..f02345d6c 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -281,6 +281,7 @@ pub(crate) struct RunSettings { pub(crate) all_packages: bool, pub(crate) package: Option, pub(crate) no_project: bool, + pub(crate) active: bool, pub(crate) no_sync: bool, pub(crate) python: Option, pub(crate) install_mirrors: PythonInstallMirrors, @@ -318,6 +319,8 @@ impl RunSettings { with_editable, with_requirements, isolated, + active, + no_active, no_sync, locked, frozen, @@ -380,6 +383,7 @@ impl RunSettings { package, no_project, no_sync, + active: flag(active, no_active).unwrap_or_default(), python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), settings: ResolverInstallerSettings::combine( @@ -944,6 +948,7 @@ impl PythonPinSettings { pub(crate) struct SyncSettings { pub(crate) locked: bool, pub(crate) frozen: bool, + pub(crate) active: bool, pub(crate) extras: ExtrasSpecification, pub(crate) dev: DevGroupsSpecification, pub(crate) editable: EditableMode, @@ -982,6 +987,8 @@ impl SyncSettings { no_install_package, locked, frozen, + active, + no_active, installer, build, refresh, @@ -1002,6 +1009,7 @@ impl SyncSettings { Self { locked, frozen, + active: flag(active, no_active).unwrap_or_default(), extras: ExtrasSpecification::from_args( flag(all_extras, no_all_extras).unwrap_or_default(), no_extra, @@ -1091,6 +1099,7 @@ impl LockSettings { pub(crate) struct AddSettings { pub(crate) locked: bool, pub(crate) frozen: bool, + pub(crate) active: bool, pub(crate) no_sync: bool, pub(crate) packages: Vec, pub(crate) requirements: Vec, @@ -1130,6 +1139,8 @@ impl AddSettings { no_sync, locked, frozen, + active, + no_active, installer, build, refresh, @@ -1201,6 +1212,7 @@ impl AddSettings { Self { locked, frozen, + active: flag(active, no_active).unwrap_or_default(), no_sync, packages, requirements, @@ -1231,6 +1243,7 @@ impl AddSettings { pub(crate) struct RemoveSettings { pub(crate) locked: bool, pub(crate) frozen: bool, + pub(crate) active: bool, pub(crate) no_sync: bool, pub(crate) packages: Vec, pub(crate) dependency_type: DependencyType, @@ -1254,6 +1267,8 @@ impl RemoveSettings { no_sync, locked, frozen, + active, + no_active, installer, build, refresh, @@ -1285,6 +1300,7 @@ impl RemoveSettings { Self { locked, frozen, + active: flag(active, no_active).unwrap_or_default(), no_sync, packages, dependency_type, diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index c8514c7b1..3dc8a4f91 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -124,7 +124,7 @@ fn init_application() -> Result<()> { Hello from foo! ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 1 package in [TIME] @@ -305,7 +305,7 @@ fn init_application_package() -> Result<()> { Hello from foo! ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 1 package in [TIME] @@ -387,7 +387,7 @@ fn init_library() -> Result<()> { Hello from foo! ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 1 package in [TIME] diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 63feeb446..6c5ab8c9a 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -14643,6 +14643,7 @@ fn lock_explicit_default_index() -> Result<()> { DEBUG Found workspace root: `[TEMP_DIR]/` DEBUG Adding current workspace member: `[TEMP_DIR]/` DEBUG Using Python request `>=3.12` from `requires-python` metadata + DEBUG Use of the active virtual environment was requested, but `VIRTUAL_ENV` is not set DEBUG Checking for Python environment at `.venv` DEBUG The virtual environment's Python version satisfies `>=3.12` DEBUG Using request timeout of [TIME] diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index a900f4148..d069c09f7 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -5,7 +5,7 @@ use assert_cmd::assert::OutputAssertExt; use assert_fs::{fixture::ChildPath, prelude::*}; use indoc::indoc; use insta::assert_snapshot; -use predicates::str::contains; +use predicates::{prelude::predicate, str::contains}; use std::path::Path; use uv_fs::copy_dir_all; use uv_python::PYTHON_VERSION_FILENAME; @@ -2354,7 +2354,7 @@ fn run_from_directory() -> Result<()> { 3.12.[X] ----- stderr ----- - warning: `VIRTUAL_ENV=.venv` does not match the project environment path `[PROJECT_VENV]/` and will be ignored + warning: `VIRTUAL_ENV=.venv` does not match the project environment path `[PROJECT_VENV]/` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: [PROJECT_VENV]/ Resolved 1 package in [TIME] @@ -2370,7 +2370,7 @@ fn run_from_directory() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: `VIRTUAL_ENV=.venv` does not match the project environment path `[PROJECT_VENV]/` and will be ignored + warning: `VIRTUAL_ENV=.venv` does not match the project environment path `[PROJECT_VENV]/` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: [PROJECT_VENV]/ Resolved 1 package in [TIME] @@ -2387,7 +2387,7 @@ fn run_from_directory() -> Result<()> { 3.12.[X] ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 1 package in [TIME] @@ -2402,7 +2402,7 @@ fn run_from_directory() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 1 package in [TIME] @@ -2417,7 +2417,7 @@ fn run_from_directory() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 1 package in [TIME] @@ -2446,7 +2446,7 @@ fn run_from_directory() -> Result<()> { 3.10.[X] ----- stderr ----- - warning: `VIRTUAL_ENV=.venv` does not match the project environment path `[PROJECT_VENV]/` and will be ignored + warning: `VIRTUAL_ENV=.venv` does not match the project environment path `[PROJECT_VENV]/` and will be ignored; use `--active` to target the active environment instead Using CPython 3.10.[X] interpreter at: [PYTHON-3.10] Creating virtual environment at: [PROJECT_VENV]/ Resolved 1 package in [TIME] @@ -2462,7 +2462,7 @@ fn run_from_directory() -> Result<()> { 3.10.[X] ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Using CPython 3.10.[X] interpreter at: [PYTHON-3.10] Creating virtual environment at: .venv Resolved 1 package in [TIME] @@ -3431,6 +3431,98 @@ fn run_linked_environment_path() -> Result<()> { Ok(()) } +#[test] +fn run_active_environment() -> Result<()> { + let context = TestContext::new_with_versions(&["3.11", "3.12"]) + .with_filtered_virtualenv_bin() + .with_filtered_python_names(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.11" + dependencies = ["iniconfig"] + "#, + )?; + + // Running `uv run` with `VIRTUAL_ENV` should warn + uv_snapshot!(context.filters(), context.run() + .arg("python").arg("--version") + .env(EnvVars::VIRTUAL_ENV, "foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.11.[X] + + ----- stderr ----- + warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead + Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + Creating virtual environment at: .venv + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + context + .temp_dir + .child(".venv") + .assert(predicate::path::is_dir()); + + context + .temp_dir + .child("foo") + .assert(predicate::path::missing()); + + // Using `--active` should create the environment + uv_snapshot!(context.filters(), context.run() + .arg("--active") + .arg("python").arg("--version") + .env(EnvVars::VIRTUAL_ENV, "foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.11.[X] + + ----- stderr ----- + Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + Creating virtual environment at: foo + Resolved 2 packages in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + context + .temp_dir + .child("foo") + .assert(predicate::path::is_dir()); + + // Requesting a different Python version should invalidate the environment + uv_snapshot!(context.filters(), context.run() + .arg("--active") + .arg("-p").arg("3.12") + .arg("python").arg("--version") + .env(EnvVars::VIRTUAL_ENV, "foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.[X] + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Removed virtual environment at: foo + Creating virtual environment at: foo + Resolved 2 packages in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + Ok(()) +} + #[test] #[cfg(not(windows))] fn run_gui_script_explicit_stdin_unix() -> Result<()> { diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 5dd858305..44e99803b 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -3182,6 +3182,131 @@ fn sync_custom_environment_path() -> Result<()> { Ok(()) } +#[test] +fn sync_active_environment() -> Result<()> { + let context = TestContext::new_with_versions(&["3.11", "3.12"]) + .with_filtered_virtualenv_bin() + .with_filtered_python_names(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.11" + dependencies = ["iniconfig"] + "#, + )?; + + // Running `uv sync` with `VIRTUAL_ENV` should warn + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead + Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + Creating virtual environment at: .venv + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + context + .temp_dir + .child(".venv") + .assert(predicate::path::is_dir()); + + context + .temp_dir + .child("foo") + .assert(predicate::path::missing()); + + // Using `--active` should create the environment + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + Creating virtual environment at: foo + Resolved 2 packages in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + context + .temp_dir + .child("foo") + .assert(predicate::path::is_dir()); + + // A subsequent sync will re-use the environment + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Audited 1 package in [TIME] + "###); + + // Setting both the `VIRTUAL_ENV` and `UV_PROJECT_ENVIRONMENT` is fine if they agree + uv_snapshot!(context.filters(), context.sync() + .arg("--active") + .env(EnvVars::VIRTUAL_ENV, "foo") + .env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Audited 1 package in [TIME] + "###); + + // If they disagree, we use `VIRTUAL_ENV` because of `--active` + uv_snapshot!(context.filters(), context.sync() + .arg("--active") + .env(EnvVars::VIRTUAL_ENV, "foo") + .env(EnvVars::UV_PROJECT_ENVIRONMENT, "bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Audited 1 package in [TIME] + "###); + + context + .temp_dir + .child("bar") + .assert(predicate::path::missing()); + + // Requesting another Python version will invalidate the environment + uv_snapshot!(context.filters(), context.sync() + .env(EnvVars::VIRTUAL_ENV, "foo").arg("--active").arg("-p").arg("3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Removed virtual environment at: foo + Creating virtual environment at: foo + Resolved 2 packages in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + Ok(()) +} + #[test] #[cfg(feature = "git")] fn sync_workspace_custom_environment_path() -> Result<()> { @@ -3417,7 +3542,7 @@ fn sync_legacy_non_project_warning() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Resolved 2 packages in [TIME] Audited 1 package in [TIME] "###); @@ -3429,7 +3554,7 @@ fn sync_legacy_non_project_warning() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Resolved 2 packages in [TIME] Audited 1 package in [TIME] "###); @@ -3455,7 +3580,7 @@ fn sync_legacy_non_project_warning() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: `VIRTUAL_ENV=foo` does not match the project environment path `bar` and will be ignored + warning: `VIRTUAL_ENV=foo` does not match the project environment path `bar` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: bar Resolved 2 packages in [TIME] @@ -3474,7 +3599,7 @@ fn sync_legacy_non_project_warning() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: `VIRTUAL_ENV=foo` does not match the project environment path `[TEMP_DIR]/foo` and will be ignored + warning: `VIRTUAL_ENV=foo` does not match the project environment path `[TEMP_DIR]/foo` and will be ignored; use `--active` to target the active environment instead Resolved 2 packages in [TIME] Audited 1 package in [TIME] "###); diff --git a/crates/uv/tests/it/workspace.rs b/crates/uv/tests/it/workspace.rs index 432c3216a..62b9db0bd 100644 --- a/crates/uv/tests/it/workspace.rs +++ b/crates/uv/tests/it/workspace.rs @@ -414,7 +414,7 @@ fn test_uv_run_with_package_virtual_workspace() -> Result<()> { Success ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 8 packages in [TIME] @@ -440,7 +440,7 @@ fn test_uv_run_with_package_virtual_workspace() -> Result<()> { Success ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Resolved 8 packages in [TIME] Prepared 2 packages in [TIME] Installed 2 packages in [TIME] @@ -474,7 +474,7 @@ fn test_uv_run_virtual_workspace_root() -> Result<()> { Success ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 8 packages in [TIME] @@ -519,7 +519,7 @@ fn test_uv_run_with_package_root_workspace() -> Result<()> { Success ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 8 packages in [TIME] @@ -545,7 +545,7 @@ fn test_uv_run_with_package_root_workspace() -> Result<()> { Success ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Resolved 8 packages in [TIME] Prepared 2 packages in [TIME] Installed 2 packages in [TIME] @@ -584,7 +584,7 @@ fn test_uv_run_isolate() -> Result<()> { Success ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 8 packages in [TIME] @@ -615,7 +615,7 @@ fn test_uv_run_isolate() -> Result<()> { Success ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Resolved 8 packages in [TIME] Audited 5 packages in [TIME] "### diff --git a/docs/concepts/projects/config.md b/docs/concepts/projects/config.md index 6b3f4eb66..859fc7ccc 100644 --- a/docs/concepts/projects/config.md +++ b/docs/concepts/projects/config.md @@ -190,8 +190,9 @@ To target this environment, you'd export `UV_PROJECT_ENVIRONMENT=/usr/local`. !!! note - uv does not read the `VIRTUAL_ENV` environment variable during project operations. A warning - will be displayed if `VIRTUAL_ENV` is set to a different path than the project's environment. + By default, uv does not read the `VIRTUAL_ENV` environment variable during project operations. + A warning will be displayed if `VIRTUAL_ENV` is set to a different path than the project's + environment. The `--active` flag can be used to opt-in to respecting `VIRTUAL_ENV`. ## Limited resolution environments diff --git a/docs/reference/cli.md b/docs/reference/cli.md index b13681416..357096efe 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -72,7 +72,11 @@ uv run [OPTIONS] [COMMAND]

Options

-
--all-extras

Include all optional dependencies.

+
--active

Prefer the active virtual environment over the project’s virtual environment.

+ +

If the project virtual environment is active or no virtual environment is active, this has no effect.

+ +
--all-extras

Include all optional dependencies.

Optional dependencies are defined via project.optional-dependencies in a pyproject.toml.

@@ -754,7 +758,11 @@ uv add [OPTIONS] >

Options

-
--allow-insecure-host allow-insecure-host

Allow insecure connections to a host.

+
--active

Prefer the active virtual environment over the project’s virtual environment.

+ +

If the project virtual environment is active or no virtual environment is active, this has no effect.

+ +
--allow-insecure-host allow-insecure-host

Allow insecure connections to a host.

Can be provided multiple times.

@@ -1123,7 +1131,11 @@ uv remove [OPTIONS] ...

Options

-
--allow-insecure-host allow-insecure-host

Allow insecure connections to a host.

+
--active

Prefer the active virtual environment over the project’s virtual environment.

+ +

If the project virtual environment is active or no virtual environment is active, this has no effect.

+ +
--allow-insecure-host allow-insecure-host

Allow insecure connections to a host.

Can be provided multiple times.

@@ -1460,7 +1472,11 @@ uv sync [OPTIONS]

Options

-
--all-extras

Include all optional dependencies.

+
--active

Prefer the active virtual environment over the project’s virtual environment.

+ +

If the project virtual environment is active or no virtual environment is active, this has no effect.

+ +
--all-extras

Include all optional dependencies.

When two or more extras are declared as conflicting in tool.uv.conflicts, using this flag will always result in an error.