Skip to content

Commit

Permalink
Discover and prefer the parent interpreter when invoked with `python …
Browse files Browse the repository at this point in the history
…-m uv` (#3736)

Closes #2222
Closes #2058
Replaces #2338
See also #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.
  • Loading branch information
zanieb authored May 22, 2024
1 parent b92321b commit 5fe8910
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 40 deletions.
91 changes: 57 additions & 34 deletions crates/uv-interpreter/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(||
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"),
}
}
}
Expand Down
31 changes: 28 additions & 3 deletions crates/uv-interpreter/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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),
Expand All @@ -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()
Expand All @@ -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) {
Expand Down
Loading

0 comments on commit 5fe8910

Please sign in to comment.