Use python from python -m uv as default

Python tools run with `python -m <tool>` 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
This commit is contained in:
konstin 2024-03-10 14:35:57 +01:00
parent 7964bfbb2b
commit d8845dc444
13 changed files with 124 additions and 30 deletions

View file

@ -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<Option<Inte
///
/// We prefer the test overwrite `UV_TEST_PYTHON_PATH` if it is set, otherwise `python3`/`python` or
/// `python.exe` respectively.
///
/// When `system` is set, we ignore the `python -m uv` due to the `--system` flag.
#[instrument(skip_all)]
pub fn find_default_python(cache: &Cache) -> Result<Interpreter, Error> {
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<Interpreter, Error> {
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<Interpreter, Error> {
})
}
/// 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<Option<Interpreter>, 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<Option<Interpreter>, 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<Option<Interpreter>, 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<u8> {
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 {

View file

@ -51,8 +51,8 @@ impl PythonEnvironment {
}
/// Create a [`PythonEnvironment`] for the default Python interpreter.
pub fn from_default_python(cache: &Cache) -> Result<Self, Error> {
let interpreter = find_default_python(cache)?;
pub fn from_default_python(cache: &Cache, system: bool) -> Result<Self, Error> {
let interpreter = find_default_python(cache, system)?;
Ok(Self {
root: interpreter.prefix().to_path_buf(),
interpreter,

View file

@ -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(

View file

@ -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,

View file

@ -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()),
}

View file

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

View file

@ -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()),
}

View file

@ -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()),
}

View file

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

View file

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

View file

@ -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!(

View file

@ -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(())
}

View file

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