mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Discover and prefer the parent interpreter when invoked with python -m uv
(#3736)
Closes #2222 Closes https://github.com/astral-sh/uv/issues/2058 Replaces https://github.com/astral-sh/uv/pull/2338 See also https://github.com/astral-sh/uv/issues/2649 We use an environment variable (`UV_INTERNAL__PARENT_INTERPRETER`) to track the invoking interpreter when `python -m uv` is used. The parent interpreter is preferred over all other sources (though it will be skipped if it does not meet a `--python` request or if `--system` is used and it belongs to a virtual environment). We warn if `--system` is not provided and this interpreter would mutate system packages, but allow it.
This commit is contained in:
parent
b92321bd2d
commit
5fe891082d
7 changed files with 296 additions and 40 deletions
|
@ -62,8 +62,10 @@ pub enum VersionRequest {
|
|||
/// The policy for discovery of "system" Python interpreters.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum SystemPython {
|
||||
/// Do not allow a system Python
|
||||
/// Only allow a system Python if passed directly i.e. via [`InterpreterSource::ProvidedPath`] or [`InterpreterSource::ParentInterpreter`]
|
||||
#[default]
|
||||
Explicit,
|
||||
/// Do not allow a system Python
|
||||
Disallowed,
|
||||
/// Allow a system Python to be used if no virtual environment is active.
|
||||
Allowed,
|
||||
|
@ -125,8 +127,9 @@ pub enum InterpreterSource {
|
|||
PyLauncher,
|
||||
/// The interpreter was found in the uv toolchain directory
|
||||
ManagedToolchain,
|
||||
/// The interpreter invoked uv i.e. via `python -m uv ...`
|
||||
ParentInterpreter,
|
||||
// TODO(zanieb): Add support for fetching the interpreter from a remote source
|
||||
// TODO(zanieb): Add variant for: The interpreter path was inherited from the parent process
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -158,6 +161,7 @@ pub enum Error {
|
|||
///
|
||||
/// In order, we look in:
|
||||
///
|
||||
/// - The spawning interpreter
|
||||
/// - The active environment
|
||||
/// - A discovered environment (e.g. `.venv`)
|
||||
/// - Installed managed toolchains
|
||||
|
@ -179,14 +183,22 @@ fn python_executables<'a>(
|
|||
) -> impl Iterator<Item = Result<(InterpreterSource, PathBuf), Error>> + 'a {
|
||||
// Note we are careful to ensure the iterator chain is lazy to avoid unnecessary work
|
||||
|
||||
// (1) The active environment
|
||||
sources.contains(InterpreterSource::ActiveEnvironment).then(||
|
||||
virtualenv_from_env()
|
||||
// (1) The parent interpreter
|
||||
sources.contains(InterpreterSource::ParentInterpreter).then(||
|
||||
std::env::var_os("UV_INTERNAL__PARENT_INTERPRETER")
|
||||
.into_iter()
|
||||
.map(virtualenv_python_executable)
|
||||
.map(|path| Ok((InterpreterSource::ActiveEnvironment, path)))
|
||||
.map(|path| Ok((InterpreterSource::ParentInterpreter, PathBuf::from(path))))
|
||||
).into_iter().flatten()
|
||||
// (2) A discovered environment
|
||||
// (2) The active environment
|
||||
.chain(
|
||||
sources.contains(InterpreterSource::ActiveEnvironment).then(||
|
||||
virtualenv_from_env()
|
||||
.into_iter()
|
||||
.map(virtualenv_python_executable)
|
||||
.map(|path| Ok((InterpreterSource::ActiveEnvironment, path)))
|
||||
).into_iter().flatten()
|
||||
)
|
||||
// (3) A discovered environment
|
||||
.chain(
|
||||
sources.contains(InterpreterSource::DiscoveredEnvironment).then(||
|
||||
std::iter::once(
|
||||
|
@ -201,7 +213,7 @@ fn python_executables<'a>(
|
|||
).flatten_ok()
|
||||
).into_iter().flatten()
|
||||
)
|
||||
// (3) Managed toolchains
|
||||
// (4) Managed toolchains
|
||||
.chain(
|
||||
sources.contains(InterpreterSource::ManagedToolchain).then(move ||
|
||||
std::iter::once(
|
||||
|
@ -219,14 +231,14 @@ fn python_executables<'a>(
|
|||
).flatten_ok()
|
||||
).into_iter().flatten()
|
||||
)
|
||||
// (4) The search path
|
||||
// (5) The search path
|
||||
.chain(
|
||||
sources.contains(InterpreterSource::SearchPath).then(move ||
|
||||
python_executables_from_search_path(version, implementation)
|
||||
.map(|path| Ok((InterpreterSource::SearchPath, path))),
|
||||
).into_iter().flatten()
|
||||
)
|
||||
// (5) The `py` launcher (windows only)
|
||||
// (6) The `py` launcher (windows only)
|
||||
// TODO(konstin): Implement <https://peps.python.org/pep-0514/> to read python installations from the registry instead.
|
||||
.chain(
|
||||
(sources.contains(InterpreterSource::PyLauncher) && cfg!(windows)).then(||
|
||||
|
@ -344,8 +356,27 @@ fn python_interpreters<'a>(
|
|||
})
|
||||
.filter(move |result| match result {
|
||||
// Filter the returned interpreters to conform to the system request
|
||||
Ok((_, interpreter)) => match (system, interpreter.is_virtualenv()) {
|
||||
Ok((source, interpreter)) => match (system, interpreter.is_virtualenv()) {
|
||||
(SystemPython::Allowed, _) => true,
|
||||
(SystemPython::Explicit, false) => {
|
||||
if matches!(
|
||||
source,
|
||||
InterpreterSource::ProvidedPath | InterpreterSource::ParentInterpreter
|
||||
) {
|
||||
debug!(
|
||||
"Allowing system Python interpreter at `{}`",
|
||||
interpreter.sys_executable().display()
|
||||
);
|
||||
true
|
||||
} else {
|
||||
debug!(
|
||||
"Ignoring Python interpreter at `{}`: system intepreter not explicit",
|
||||
interpreter.sys_executable().display()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
(SystemPython::Explicit, true) => true,
|
||||
(SystemPython::Disallowed, false) => {
|
||||
debug!(
|
||||
"Ignoring Python interpreter at `{}`: system intepreter not allowed",
|
||||
|
@ -1073,31 +1104,22 @@ impl SourceSelector {
|
|||
])
|
||||
} else {
|
||||
match system {
|
||||
SystemPython::Allowed => Self::All,
|
||||
SystemPython::Required => {
|
||||
debug!("Excluding virtual environment Python due to system flag");
|
||||
Self::from_sources([
|
||||
InterpreterSource::ProvidedPath,
|
||||
InterpreterSource::SearchPath,
|
||||
#[cfg(windows)]
|
||||
InterpreterSource::PyLauncher,
|
||||
InterpreterSource::ManagedToolchain,
|
||||
])
|
||||
}
|
||||
SystemPython::Disallowed => {
|
||||
debug!("Only considering virtual environment Python interpreters");
|
||||
Self::virtualenvs()
|
||||
}
|
||||
SystemPython::Allowed | SystemPython::Explicit => Self::All,
|
||||
SystemPython::Required => Self::from_sources([
|
||||
InterpreterSource::ProvidedPath,
|
||||
InterpreterSource::SearchPath,
|
||||
#[cfg(windows)]
|
||||
InterpreterSource::PyLauncher,
|
||||
InterpreterSource::ManagedToolchain,
|
||||
InterpreterSource::ParentInterpreter,
|
||||
]),
|
||||
SystemPython::Disallowed => Self::from_sources([
|
||||
InterpreterSource::DiscoveredEnvironment,
|
||||
InterpreterSource::ActiveEnvironment,
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn virtualenvs() -> Self {
|
||||
Self::from_sources([
|
||||
InterpreterSource::DiscoveredEnvironment,
|
||||
InterpreterSource::ActiveEnvironment,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemPython {
|
||||
|
@ -1138,6 +1160,7 @@ impl fmt::Display for InterpreterSource {
|
|||
Self::SearchPath => f.write_str("search path"),
|
||||
Self::PyLauncher => f.write_str("`py` launcher output"),
|
||||
Self::ManagedToolchain => f.write_str("managed toolchains"),
|
||||
Self::ParentInterpreter => f.write_str("parent interpreter"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ use uv_fs::{LockedFile, Simplified};
|
|||
|
||||
use crate::discovery::{InterpreterRequest, SourceSelector, SystemPython, VersionRequest};
|
||||
use crate::virtualenv::{virtualenv_python_executable, PyVenvConfiguration};
|
||||
use crate::{find_default_interpreter, find_interpreter, Error, Interpreter, Target};
|
||||
use crate::{
|
||||
find_default_interpreter, find_interpreter, Error, Interpreter, InterpreterSource, Target,
|
||||
};
|
||||
|
||||
/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -31,6 +33,14 @@ impl PythonEnvironment {
|
|||
} else if system.is_preferred() {
|
||||
Self::from_default_python(cache)
|
||||
} else {
|
||||
// First check for a parent intepreter
|
||||
match Self::from_parent_interpreter(system, cache) {
|
||||
Ok(env) => return Ok(env),
|
||||
Err(Error::NotFound(_)) => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
// Then a virtual environment
|
||||
match Self::from_virtualenv(cache) {
|
||||
Ok(venv) => Ok(venv),
|
||||
Err(Error::NotFound(_)) if system.is_allowed() => Self::from_default_python(cache),
|
||||
|
@ -41,12 +51,15 @@ impl PythonEnvironment {
|
|||
|
||||
/// Create a [`PythonEnvironment`] for an existing virtual environment.
|
||||
pub fn from_virtualenv(cache: &Cache) -> Result<Self, Error> {
|
||||
let sources = SourceSelector::virtualenvs();
|
||||
let sources = SourceSelector::from_sources([
|
||||
InterpreterSource::DiscoveredEnvironment,
|
||||
InterpreterSource::ActiveEnvironment,
|
||||
]);
|
||||
let request = InterpreterRequest::Version(VersionRequest::Default);
|
||||
let found = find_interpreter(&request, SystemPython::Disallowed, &sources, cache)??;
|
||||
|
||||
debug_assert!(
|
||||
found.interpreter().base_prefix() == found.interpreter().base_exec_prefix(),
|
||||
found.interpreter().is_virtualenv(),
|
||||
"Not a virtualenv (source: {}, prefix: {})",
|
||||
found.source(),
|
||||
found.interpreter().base_prefix().display()
|
||||
|
@ -58,6 +71,18 @@ impl PythonEnvironment {
|
|||
})))
|
||||
}
|
||||
|
||||
/// Create a [`PythonEnvironment`] for the parent interpreter i.e. the executable in `python -m uv ...`
|
||||
pub fn from_parent_interpreter(system: SystemPython, cache: &Cache) -> Result<Self, Error> {
|
||||
let sources = SourceSelector::from_sources([InterpreterSource::ParentInterpreter]);
|
||||
let request = InterpreterRequest::Version(VersionRequest::Default);
|
||||
let found = find_interpreter(&request, system, &sources, cache)??;
|
||||
|
||||
Ok(Self(Arc::new(PythonEnvironmentShared {
|
||||
root: found.interpreter().prefix().to_path_buf(),
|
||||
interpreter: found.into_interpreter(),
|
||||
})))
|
||||
}
|
||||
|
||||
/// Create a [`PythonEnvironment`] from the virtual environment at the given root.
|
||||
pub fn from_root(root: &Path, cache: &Cache) -> Result<Self, Error> {
|
||||
let venv = match fs_err::canonicalize(root) {
|
||||
|
|
|
@ -1360,6 +1360,174 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_environment_from_parent_interpreter() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let cache = Cache::temp()?;
|
||||
let pwd = tempdir.child("pwd");
|
||||
pwd.create_dir_all()?;
|
||||
let venv = mock_venv(&tempdir, "3.12.0")?;
|
||||
let python = tempdir.child("python").to_path_buf();
|
||||
create_mock_interpreter(
|
||||
&python,
|
||||
&PythonVersion::from_str("3.12.1").unwrap(),
|
||||
ImplementationName::CPython,
|
||||
true,
|
||||
)?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
|
||||
),
|
||||
("UV_INTERNAL__PARENT_INTERPRETER", Some(python.into())),
|
||||
("VIRTUAL_ENV", Some(venv.into())),
|
||||
("PWD", Some(pwd.path().into())),
|
||||
],
|
||||
|| {
|
||||
let environment =
|
||||
PythonEnvironment::find(None, crate::SystemPython::Allowed, &cache)
|
||||
.expect("An environment is found");
|
||||
assert_eq!(
|
||||
environment.interpreter().python_full_version().to_string(),
|
||||
"3.12.1",
|
||||
"We should prefer parent interpreter"
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_environment_from_parent_interpreter_system_explicit() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let cache = Cache::temp()?;
|
||||
let pwd = tempdir.child("pwd");
|
||||
pwd.create_dir_all()?;
|
||||
let venv = mock_venv(&tempdir, "3.12.0")?;
|
||||
let python = tempdir.child("python").to_path_buf();
|
||||
create_mock_interpreter(
|
||||
&python,
|
||||
&PythonVersion::from_str("3.12.1").unwrap(),
|
||||
ImplementationName::CPython,
|
||||
true,
|
||||
)?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
|
||||
),
|
||||
("UV_INTERNAL__PARENT_INTERPRETER", Some(python.into())),
|
||||
("VIRTUAL_ENV", Some(venv.into())),
|
||||
("PWD", Some(pwd.path().into())),
|
||||
],
|
||||
|| {
|
||||
let environment =
|
||||
PythonEnvironment::find(None, crate::SystemPython::Explicit, &cache)
|
||||
.expect("An environment is found");
|
||||
assert_eq!(
|
||||
environment.interpreter().python_full_version().to_string(),
|
||||
"3.12.1",
|
||||
"We prefer the parent interpreter even though it is system"
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_environment_from_parent_interpreter_system_disallowed() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let cache = Cache::temp()?;
|
||||
let pwd = tempdir.child("pwd");
|
||||
pwd.create_dir_all()?;
|
||||
let venv = mock_venv(&tempdir, "3.12.0")?;
|
||||
let python = tempdir.child("python").to_path_buf();
|
||||
create_mock_interpreter(
|
||||
&python,
|
||||
&PythonVersion::from_str("3.12.1").unwrap(),
|
||||
ImplementationName::CPython,
|
||||
true,
|
||||
)?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
|
||||
),
|
||||
("UV_INTERNAL__PARENT_INTERPRETER", Some(python.into())),
|
||||
("VIRTUAL_ENV", Some(venv.into())),
|
||||
("PWD", Some(pwd.path().into())),
|
||||
],
|
||||
|| {
|
||||
let environment =
|
||||
PythonEnvironment::find(None, crate::SystemPython::Disallowed, &cache)
|
||||
.expect("An environment is found");
|
||||
assert_eq!(
|
||||
environment.interpreter().python_full_version().to_string(),
|
||||
"3.12.0",
|
||||
"We find the virtual environment Python because the system is explicitly not allowed"
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_environment_from_parent_interpreter_system_required() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let cache = Cache::temp()?;
|
||||
let pwd = tempdir.child("pwd");
|
||||
pwd.create_dir_all()?;
|
||||
let venv = mock_venv(&tempdir, "3.12.0")?;
|
||||
let python = tempdir.child("python").to_path_buf();
|
||||
create_mock_interpreter(
|
||||
&python,
|
||||
&PythonVersion::from_str("3.12.1").unwrap(),
|
||||
ImplementationName::CPython,
|
||||
false,
|
||||
)?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
|
||||
),
|
||||
("UV_INTERNAL__PARENT_INTERPRETER", Some(python.into())),
|
||||
("VIRTUAL_ENV", Some(venv.into())),
|
||||
("PWD", Some(pwd.path().into())),
|
||||
],
|
||||
|| {
|
||||
let environment =
|
||||
PythonEnvironment::find(None, crate::SystemPython::Required, &cache)
|
||||
.expect("An environment is found");
|
||||
assert_eq!(
|
||||
environment.interpreter().python_full_version().to_string(),
|
||||
"3.12.2",
|
||||
"We should skip the parent interpreter since its in a virtual environment"
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_environment_active_environment_skipped_if_system_required() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
@ -1604,6 +1772,43 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_environment_allows_file_path_with_system_explicit() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let cache = Cache::temp()?;
|
||||
tempdir.child("foo").create_dir_all()?;
|
||||
let python = tempdir.child("foo").join("bar");
|
||||
create_mock_interpreter(
|
||||
&python,
|
||||
&PythonVersion::from_str("3.10.0").unwrap(),
|
||||
ImplementationName::default(),
|
||||
true,
|
||||
)?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("PATH", None),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|| {
|
||||
let environment = PythonEnvironment::find(
|
||||
Some(python.to_str().expect("Test path is valid unicode")),
|
||||
crate::SystemPython::Explicit,
|
||||
&cache,
|
||||
)
|
||||
.expect("Environment should be found");
|
||||
assert_eq!(
|
||||
environment.interpreter().python_full_version().to_string(),
|
||||
"3.10.0",
|
||||
"We should find the interpreter"
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_environment_does_not_allow_file_path_with_system_disallowed() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
|
|
@ -117,7 +117,7 @@ pub(crate) async fn pip_install(
|
|||
let system = if system {
|
||||
SystemPython::Required
|
||||
} else {
|
||||
SystemPython::Disallowed
|
||||
SystemPython::Explicit
|
||||
};
|
||||
let venv = PythonEnvironment::find(python.as_deref(), system, &cache)?;
|
||||
|
||||
|
|
|
@ -121,7 +121,7 @@ pub(crate) async fn pip_sync(
|
|||
let system = if system {
|
||||
SystemPython::Required
|
||||
} else {
|
||||
SystemPython::Disallowed
|
||||
SystemPython::Explicit
|
||||
};
|
||||
let venv = PythonEnvironment::find(python.as_deref(), system, &cache)?;
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ pub(crate) async fn pip_uninstall(
|
|||
let system = if system {
|
||||
SystemPython::Required
|
||||
} else {
|
||||
SystemPython::Disallowed
|
||||
SystemPython::Explicit
|
||||
};
|
||||
let venv = PythonEnvironment::find(python.as_deref(), system, &cache)?;
|
||||
|
||||
|
|
|
@ -31,6 +31,9 @@ def _run() -> None:
|
|||
if venv:
|
||||
env.setdefault("VIRTUAL_ENV", venv)
|
||||
|
||||
# Let `uv` know that it was spawned by this Python interpreter
|
||||
env["UV_INTERNAL__PARENT_INTERPRETER"] = sys.executable
|
||||
|
||||
if sys.platform == "win32":
|
||||
import subprocess
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue