From d8845dc444aff1d20f9fa585db8dfd69740049c7 Mon Sep 17 00:00:00 2001 From: konstin Date: Sun, 10 Mar 2024 14:35:57 +0100 Subject: [PATCH] Use python from `python -m uv` as default Python tools run with `python -m ` will use this `python` as their default python, including pip, virtualenv and the built-in venv. Calling Python tools this way is common, the [pip docs](https://pip.pypa.io/en/stable/user_guide/) use `python -m pip` exclusively, the built-in venv can only be called this way and certain project layouts require `python -m pytest` over `pytest`. This python interpreter takes precedence over the currently active (v)env. These tools are all written in python and read `sys.executable`. To emulate this, we're setting `UV_DEFAULT_PYTHON` in the python module-launcher shim and read it in the default python discovery, prioritizing it over the active venv. User can also set `UV_DEFAULT_PYTHON` for their own purposes. The test covers only half of the feature since we don't build the python package before running the tests. Fixes #2058 Fixes #2222 --- crates/uv-interpreter/src/find_python.rs | 60 ++++++++++++++----- .../uv-interpreter/src/python_environment.rs | 4 +- crates/uv-resolver/tests/resolver.rs | 4 +- crates/uv-virtualenv/src/main.rs | 2 +- crates/uv/src/commands/pip_freeze.rs | 4 +- crates/uv/src/commands/pip_install.rs | 2 +- crates/uv/src/commands/pip_list.rs | 4 +- crates/uv/src/commands/pip_show.rs | 4 +- crates/uv/src/commands/pip_sync.rs | 2 +- crates/uv/src/commands/pip_uninstall.rs | 2 +- crates/uv/src/commands/venv.rs | 2 +- crates/uv/tests/venv.rs | 59 ++++++++++++++++++ python/uv/__main__.py | 5 +- 13 files changed, 124 insertions(+), 30 deletions(-) diff --git a/crates/uv-interpreter/src/find_python.rs b/crates/uv-interpreter/src/find_python.rs index b06472d21..19615f522 100644 --- a/crates/uv-interpreter/src/find_python.rs +++ b/crates/uv-interpreter/src/find_python.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use tracing::{debug, instrument}; use uv_cache::Cache; -use uv_fs::normalize_path; +use uv_fs::{normalize_path, Simplified}; use crate::interpreter::InterpreterInfoError; use crate::python_environment::{detect_python_executable, detect_virtual_env}; @@ -61,10 +61,16 @@ pub fn find_requested_python(request: &str, cache: &Cache) -> Result Result { - debug!("Starting interpreter discovery for default Python"); - try_find_default_python(cache)?.ok_or(if cfg!(windows) { +pub fn find_default_python(cache: &Cache, system: bool) -> Result { + let selector = if system { + PythonVersionSelector::System + } else { + PythonVersionSelector::Default + }; + find_python(selector, cache)?.ok_or(if cfg!(windows) { Error::NoPythonInstalledWindows } else if cfg!(unix) { Error::NoPythonInstalledUnix @@ -73,11 +79,6 @@ pub fn find_default_python(cache: &Cache) -> Result { }) } -/// Same as [`find_default_python`] but returns `None` if no python is found instead of returning an `Err`. -pub(crate) fn try_find_default_python(cache: &Cache) -> Result, Error> { - find_python(PythonVersionSelector::Default, cache) -} - /// Find a Python version matching `selector`. /// /// It searches for an existing installation in the following order: @@ -95,6 +96,20 @@ fn find_python( selector: PythonVersionSelector, cache: &Cache, ) -> Result, Error> { + if selector != PythonVersionSelector::System { + // `python -m uv` passes `sys.executable` as `UV_DEFAULT_PYTHON`. Users expect that this Python + // version is used as it is the recommended or sometimes even only way to use tools, e.g. pip + // (`python3.10 -m pip`) and venv (`python3.10 -m venv`). + if let Some(default_python) = env::var_os("UV_DEFAULT_PYTHON") { + debug!( + "Trying UV_DEFAULT_PYTHON at {}", + default_python.simplified_display() + ); + let interpreter = Interpreter::query(default_python, cache)?; + return Ok(Some(interpreter)); + } + } + #[allow(non_snake_case)] let UV_TEST_PYTHON_PATH = env::var_os("UV_TEST_PYTHON_PATH"); @@ -299,7 +314,7 @@ impl PythonInstallation { cache: &Cache, ) -> Result, Error> { let selected = match selector { - PythonVersionSelector::Default => true, + PythonVersionSelector::Default | PythonVersionSelector::System => true, PythonVersionSelector::Major(major) => self.major() == major, @@ -339,9 +354,11 @@ impl PythonInstallation { } } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] enum PythonVersionSelector { Default, + /// Like default, but skip over the `python` from `python -m uv`. + System, Major(u8), MajorMinor(u8, u8), MajorMinorPatch(u8, u8, u8), @@ -360,7 +377,7 @@ impl PythonVersionSelector { }; match self { - Self::Default => [Some(python3), Some(python), None, None], + Self::Default | Self::System => [Some(python3), Some(python), None, None], Self::Major(major) => [ Some(Cow::Owned(format!("python{major}{extension}"))), Some(python), @@ -386,7 +403,7 @@ impl PythonVersionSelector { fn major(self) -> Option { match self { - Self::Default => None, + Self::Default | Self::System => None, Self::Major(major) => Some(major), Self::MajorMinor(major, _) => Some(major), Self::MajorMinorPatch(major, _, _) => Some(major), @@ -471,6 +488,21 @@ fn find_version( } }; + // `python -m uv` passes `sys.executable` as `UV_DEFAULT_PYTHON`. Users expect that this Python + // version is used as it is the recommended or sometimes even only way to use tools, e.g. pip + // (`python3.10 -m pip`) and venv (`python3.10 -m venv`). This is duplicated in + // `find_requested_python`, but we need to do it here to take precedence over the active venv. + if let Some(default_python) = env::var_os("UV_DEFAULT_PYTHON") { + debug!( + "Trying UV_DEFAULT_PYTHON at {}", + default_python.simplified_display() + ); + let interpreter = Interpreter::query(default_python, cache)?; + if version_matches(&interpreter) { + return Ok(Some(interpreter)); + } + } + // Check if the venv Python matches. if let Some(venv) = detect_virtual_env()? { let executable = detect_python_executable(venv); @@ -486,7 +518,7 @@ fn find_version( let interpreter = if let Some(python_version) = python_version { find_requested_python(&python_version.string, cache)? } else { - try_find_default_python(cache)? + find_python(PythonVersionSelector::Default, cache)? }; if let Some(interpreter) = interpreter { diff --git a/crates/uv-interpreter/src/python_environment.rs b/crates/uv-interpreter/src/python_environment.rs index b0415931b..db70b70ba 100644 --- a/crates/uv-interpreter/src/python_environment.rs +++ b/crates/uv-interpreter/src/python_environment.rs @@ -51,8 +51,8 @@ impl PythonEnvironment { } /// Create a [`PythonEnvironment`] for the default Python interpreter. - pub fn from_default_python(cache: &Cache) -> Result { - let interpreter = find_default_python(cache)?; + pub fn from_default_python(cache: &Cache, system: bool) -> Result { + let interpreter = find_default_python(cache, system)?; Ok(Self { root: interpreter.prefix().to_path_buf(), interpreter, diff --git a/crates/uv-resolver/tests/resolver.rs b/crates/uv-resolver/tests/resolver.rs index 9e54155bf..19c3ad268 100644 --- a/crates/uv-resolver/tests/resolver.rs +++ b/crates/uv-resolver/tests/resolver.rs @@ -120,8 +120,8 @@ async fn resolve( let flat_index = FlatIndex::default(); let index = InMemoryIndex::default(); // TODO(konstin): Should we also use the bootstrapped pythons here? - let real_interpreter = - find_default_python(&Cache::temp().unwrap()).expect("Expected a python to be installed"); + let real_interpreter = find_default_python(&Cache::temp().unwrap(), false) + .expect("Expected a python to be installed"); let interpreter = Interpreter::artificial(real_interpreter.platform().clone(), markers.clone()); let build_context = DummyContext::new(Cache::temp()?, interpreter.clone()); let resolver = Resolver::new( diff --git a/crates/uv-virtualenv/src/main.rs b/crates/uv-virtualenv/src/main.rs index 1dda61ed6..2beef6d45 100644 --- a/crates/uv-virtualenv/src/main.rs +++ b/crates/uv-virtualenv/src/main.rs @@ -39,7 +39,7 @@ fn run() -> Result<(), uv_virtualenv::Error> { uv_interpreter::Error::NoSuchPython(python_request.to_string()), )? } else { - find_default_python(&cache)? + find_default_python(&cache, false)? }; create_bare_venv( &location, diff --git a/crates/uv/src/commands/pip_freeze.rs b/crates/uv/src/commands/pip_freeze.rs index 603d7cb34..74bf3a5ee 100644 --- a/crates/uv/src/commands/pip_freeze.rs +++ b/crates/uv/src/commands/pip_freeze.rs @@ -26,12 +26,12 @@ pub(crate) fn pip_freeze( let venv = if let Some(python) = python { PythonEnvironment::from_requested_python(python, cache)? } else if system { - PythonEnvironment::from_default_python(cache)? + PythonEnvironment::from_default_python(cache, true)? } else { match PythonEnvironment::from_virtualenv(cache) { Ok(venv) => venv, Err(uv_interpreter::Error::VenvNotFound) => { - PythonEnvironment::from_default_python(cache)? + PythonEnvironment::from_default_python(cache, false)? } Err(err) => return Err(err.into()), } diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 5e3250984..b5ce0e375 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -110,7 +110,7 @@ pub(crate) async fn pip_install( let venv = if let Some(python) = python.as_ref() { PythonEnvironment::from_requested_python(python, &cache)? } else if system { - PythonEnvironment::from_default_python(&cache)? + PythonEnvironment::from_default_python(&cache, true)? } else { PythonEnvironment::from_virtualenv(&cache)? }; diff --git a/crates/uv/src/commands/pip_list.rs b/crates/uv/src/commands/pip_list.rs index da2aef9d3..237713e52 100644 --- a/crates/uv/src/commands/pip_list.rs +++ b/crates/uv/src/commands/pip_list.rs @@ -37,12 +37,12 @@ pub(crate) fn pip_list( let venv = if let Some(python) = python { PythonEnvironment::from_requested_python(python, cache)? } else if system { - PythonEnvironment::from_default_python(cache)? + PythonEnvironment::from_default_python(cache, true)? } else { match PythonEnvironment::from_virtualenv(cache) { Ok(venv) => venv, Err(uv_interpreter::Error::VenvNotFound) => { - PythonEnvironment::from_default_python(cache)? + PythonEnvironment::from_default_python(cache, false)? } Err(err) => return Err(err.into()), } diff --git a/crates/uv/src/commands/pip_show.rs b/crates/uv/src/commands/pip_show.rs index 7a230537c..7e400ce0e 100644 --- a/crates/uv/src/commands/pip_show.rs +++ b/crates/uv/src/commands/pip_show.rs @@ -42,12 +42,12 @@ pub(crate) fn pip_show( let venv = if let Some(python) = python { PythonEnvironment::from_requested_python(python, cache)? } else if system { - PythonEnvironment::from_default_python(cache)? + PythonEnvironment::from_default_python(cache, true)? } else { match PythonEnvironment::from_virtualenv(cache) { Ok(venv) => venv, Err(uv_interpreter::Error::VenvNotFound) => { - PythonEnvironment::from_default_python(cache)? + PythonEnvironment::from_default_python(cache, false)? } Err(err) => return Err(err.into()), } diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index abe755e39..25854c832 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -74,7 +74,7 @@ pub(crate) async fn pip_sync( let venv = if let Some(python) = python.as_ref() { PythonEnvironment::from_requested_python(python, &cache)? } else if system { - PythonEnvironment::from_default_python(&cache)? + PythonEnvironment::from_default_python(&cache, true)? } else { PythonEnvironment::from_virtualenv(&cache)? }; diff --git a/crates/uv/src/commands/pip_uninstall.rs b/crates/uv/src/commands/pip_uninstall.rs index dda1900e0..18b04adbc 100644 --- a/crates/uv/src/commands/pip_uninstall.rs +++ b/crates/uv/src/commands/pip_uninstall.rs @@ -44,7 +44,7 @@ pub(crate) async fn pip_uninstall( let venv = if let Some(python) = python.as_ref() { PythonEnvironment::from_requested_python(python, &cache)? } else if system { - PythonEnvironment::from_default_python(&cache)? + PythonEnvironment::from_default_python(&cache, true)? } else { PythonEnvironment::from_virtualenv(&cache)? }; diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 38c69362e..b8925ea01 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -102,7 +102,7 @@ async fn venv_impl( .ok_or(Error::NoSuchPython(python_request.to_string())) .into_diagnostic()? } else { - find_default_python(cache).into_diagnostic()? + find_default_python(cache, false).into_diagnostic()? }; writeln!( diff --git a/crates/uv/tests/venv.rs b/crates/uv/tests/venv.rs index 5f46d812b..640ce94d5 100644 --- a/crates/uv/tests/venv.rs +++ b/crates/uv/tests/venv.rs @@ -1,5 +1,6 @@ #![cfg(feature = "python")] +use std::path::PathBuf; use std::process::Command; use anyhow::Result; @@ -731,3 +732,61 @@ fn verify_nested_pyvenv_cfg() -> Result<()> { Ok(()) } + +#[test] +fn uv_default_python() -> Result<()> { + let temp_dir = assert_fs::TempDir::new()?; + let cache_dir = assert_fs::TempDir::new()?; + // The path to a Python 3.12 interpreter + let bin312 = + create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); + // The path to a Python 3.10 interpreter + let bin310 = + create_bin_with_executables(&temp_dir, &["3.10"]).expect("Failed to create bin dir"); + let python310 = PathBuf::from(bin310).join(if cfg!(unix) { + "python3" + } else if cfg!(windows) { + "python.exe" + } else { + unimplemented!("Only Windows and Unix are supported") + }); + let venv = temp_dir.child(".venv"); + + // Create a virtual environment at `.venv`. + let filter_venv = regex::escape(&venv.simplified_display().to_string()); + let filter_prompt = r"Activate with: (?:.*)\\Scripts\\activate"; + let filters = &[ + (r"interpreter at: .+", "interpreter at: [PATH]"), + (&filter_venv, "/home/ferris/project/.venv"), + ( + filter_prompt, + "Activate with: source /home/ferris/project/.venv/bin/activate", + ), + ]; + uv_snapshot!(filters, Command::new(get_bin()) + .arg("venv") + .arg(venv.as_os_str()) + .arg("--cache-dir") + .arg(cache_dir.path()) + .arg("--exclude-newer") + .arg(EXCLUDE_NEWER) + // Simulate a PATH the user may have with Python 3.12 being the default. + .env("UV_TEST_PYTHON_PATH", bin312.clone()) + // Simulate `python3.10 -m uv`. + .env("UV_DEFAULT_PYTHON", python310) + .current_dir(&temp_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.10.13 interpreter at: [PATH] + Creating virtualenv at: /home/ferris/project/.venv + Activate with: source /home/ferris/project/.venv/bin/activate + "### + ); + + venv.assert(predicates::path::is_dir()); + + Ok(()) +} diff --git a/python/uv/__main__.py b/python/uv/__main__.py index 5337ed63c..8facfa379 100644 --- a/python/uv/__main__.py +++ b/python/uv/__main__.py @@ -22,6 +22,7 @@ def _detect_virtualenv() -> str: return "" + def _run() -> None: uv = os.fsdecode(find_uv_bin()) @@ -30,6 +31,9 @@ def _run() -> None: if venv: env.setdefault("VIRTUAL_ENV", venv) + # When running with `python -m uv`, use this `python` as default. + env.setdefault("UV_DEFAULT_PYTHON", sys.executable) + if sys.platform == "win32": import subprocess @@ -39,6 +43,5 @@ def _run() -> None: os.execvpe(uv, [uv, *sys.argv[1:]], env=env) - if __name__ == "__main__": _run()