mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
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:
parent
7964bfbb2b
commit
d8845dc444
13 changed files with 124 additions and 30 deletions
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
|
|
|
@ -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)?
|
||||
};
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
|
|
|
@ -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)?
|
||||
};
|
||||
|
|
|
@ -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)?
|
||||
};
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue