Do not treat interpereters discovered via CONDA_PREFIX as system interpreters (#3771)
Closes https://github.com/astral-sh/uv/issues/3769
This commit is contained in:
@@ -11,7 +11,8 @@ use crate::interpreter::Error as InterpreterError;
|
||||
use crate::managed::toolchains_for_current_platform;
|
||||
use crate::py_launcher::py_list_paths;
|
||||
use crate::virtualenv::{
|
||||
virtualenv_from_env, virtualenv_from_working_dir, virtualenv_python_executable,
|
||||
conda_prefix_from_env, virtualenv_from_env, virtualenv_from_working_dir,
|
||||
virtualenv_python_executable,
|
||||
};
|
||||
use crate::{Interpreter, PythonVersion};
|
||||
use std::borrow::Cow;
|
||||
@@ -125,6 +126,8 @@ pub enum InterpreterSource {
|
||||
ProvidedPath,
|
||||
/// An environment was active e.g. via `VIRTUAL_ENV`
|
||||
ActiveEnvironment,
|
||||
/// A conda environment was active e.g. via `CONDA_PREFIX`
|
||||
CondaPrefix,
|
||||
/// An environment was discovered e.g. via `.venv`
|
||||
DiscoveredEnvironment,
|
||||
/// An executable was found in the search path i.e. `PATH`
|
||||
@@ -195,7 +198,7 @@ fn python_executables<'a>(
|
||||
.into_iter()
|
||||
.map(|path| Ok((InterpreterSource::ParentInterpreter, PathBuf::from(path))))
|
||||
).into_iter().flatten()
|
||||
// (2) The active environment
|
||||
// (2) An active virtual environment
|
||||
.chain(
|
||||
sources.contains(InterpreterSource::ActiveEnvironment).then(||
|
||||
virtualenv_from_env()
|
||||
@@ -204,7 +207,16 @@ fn python_executables<'a>(
|
||||
.map(|path| Ok((InterpreterSource::ActiveEnvironment, path)))
|
||||
).into_iter().flatten()
|
||||
)
|
||||
// (3) A discovered environment
|
||||
// (3) An active conda environment
|
||||
.chain(
|
||||
sources.contains(InterpreterSource::CondaPrefix).then(||
|
||||
conda_prefix_from_env()
|
||||
.into_iter()
|
||||
.map(virtualenv_python_executable)
|
||||
.map(|path| Ok((InterpreterSource::CondaPrefix, path)))
|
||||
).into_iter().flatten()
|
||||
)
|
||||
// (4) A discovered environment
|
||||
.chain(
|
||||
sources.contains(InterpreterSource::DiscoveredEnvironment).then(||
|
||||
std::iter::once(
|
||||
@@ -219,7 +231,7 @@ fn python_executables<'a>(
|
||||
).flatten_ok()
|
||||
).into_iter().flatten()
|
||||
)
|
||||
// (4) Managed toolchains
|
||||
// (5) Managed toolchains
|
||||
.chain(
|
||||
sources.contains(InterpreterSource::ManagedToolchain).then(move ||
|
||||
std::iter::once(
|
||||
@@ -237,14 +249,14 @@ fn python_executables<'a>(
|
||||
).flatten_ok()
|
||||
).into_iter().flatten()
|
||||
)
|
||||
// (5) The search path
|
||||
// (6) 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()
|
||||
)
|
||||
// (6) The `py` launcher (windows only)
|
||||
// (7) 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(||
|
||||
@@ -362,7 +374,11 @@ fn python_interpreters<'a>(
|
||||
})
|
||||
.filter(move |result| match result {
|
||||
// Filter the returned interpreters to conform to the system request
|
||||
Ok((source, interpreter)) => match (system, interpreter.is_virtualenv()) {
|
||||
Ok((source, interpreter)) => match (
|
||||
system,
|
||||
// Conda environments are not conformant virtual environments but we should not treat them as system interpreters
|
||||
interpreter.is_virtualenv() || matches!(source, InterpreterSource::CondaPrefix),
|
||||
) {
|
||||
(SystemPython::Allowed, _) => true,
|
||||
(SystemPython::Explicit, false) => {
|
||||
if matches!(
|
||||
@@ -1100,6 +1116,7 @@ impl SourceSelector {
|
||||
Self::VirtualEnv => [
|
||||
InterpreterSource::DiscoveredEnvironment,
|
||||
InterpreterSource::ActiveEnvironment,
|
||||
InterpreterSource::CondaPrefix,
|
||||
]
|
||||
.contains(&source),
|
||||
Self::Custom(sources) => sources.contains(&source),
|
||||
@@ -1163,6 +1180,7 @@ impl fmt::Display for InterpreterSource {
|
||||
match self {
|
||||
Self::ProvidedPath => f.write_str("provided path"),
|
||||
Self::ActiveEnvironment => f.write_str("active virtual environment"),
|
||||
Self::CondaPrefix => f.write_str("conda prefix"),
|
||||
Self::DiscoveredEnvironment => f.write_str("virtual environment"),
|
||||
Self::SearchPath => f.write_str("search path"),
|
||||
Self::PyLauncher => f.write_str("`py` launcher output"),
|
||||
|
||||
@@ -50,13 +50,16 @@ impl PythonEnvironment {
|
||||
}
|
||||
|
||||
/// Create a [`PythonEnvironment`] for an existing virtual environment.
|
||||
///
|
||||
/// Allows Conda environments (via `CONDA_PREFIX`) though they are not technically virtual environments.
|
||||
pub fn from_virtualenv(cache: &Cache) -> Result<Self, Error> {
|
||||
let sources = SourceSelector::VirtualEnv;
|
||||
let request = InterpreterRequest::Version(VersionRequest::Default);
|
||||
let found = find_interpreter(&request, SystemPython::Disallowed, &sources, cache)??;
|
||||
|
||||
debug_assert!(
|
||||
found.interpreter().is_virtualenv(),
|
||||
found.interpreter().is_virtualenv()
|
||||
|| matches!(found.source(), InterpreterSource::CondaPrefix),
|
||||
"Not a virtualenv (source: {}, prefix: {})",
|
||||
found.source(),
|
||||
found.interpreter().base_prefix().display()
|
||||
|
||||
@@ -262,6 +262,29 @@ mod tests {
|
||||
Ok(venv.to_path_buf())
|
||||
}
|
||||
|
||||
/// Create a mock conda prefix in the given directory.
|
||||
///
|
||||
/// These are like virtual environments but they look like system interpreters because `prefix` and `base_prefix` are equal.
|
||||
///
|
||||
/// Returns the path to the environment.
|
||||
fn mock_conda_prefix(tempdir: &TempDir, version: &'static str) -> Result<PathBuf> {
|
||||
let env = tempdir.child("conda");
|
||||
let executable = virtualenv_python_executable(&env);
|
||||
fs_err::create_dir_all(
|
||||
executable
|
||||
.parent()
|
||||
.expect("A Python executable path should always have a parent"),
|
||||
)?;
|
||||
create_mock_interpreter(
|
||||
&executable,
|
||||
&PythonVersion::from_str(version).expect("A valid Python version is used for tests"),
|
||||
ImplementationName::default(),
|
||||
true,
|
||||
)?;
|
||||
env.child("pyvenv.cfg").touch()?;
|
||||
Ok(env.to_path_buf())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_default_interpreter_empty_path() -> Result<()> {
|
||||
let cache = Cache::temp()?;
|
||||
@@ -1308,6 +1331,74 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_environment_from_conda_prefix() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let cache = Cache::temp()?;
|
||||
let conda_prefix = mock_conda_prefix(&tempdir, "3.12.0")?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.11.2"])?),
|
||||
),
|
||||
("CONDA_PREFIX", Some(conda_prefix.into())),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|| {
|
||||
let environment =
|
||||
// Note this environment is not treated as a system interpreter
|
||||
PythonEnvironment::find(None, SystemPython::Disallowed, &cache)
|
||||
.expect("An environment is found");
|
||||
assert_eq!(
|
||||
environment.interpreter().python_full_version().to_string(),
|
||||
"3.12.0",
|
||||
"We should allow the conda environment"
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_environment_from_conda_prefix_and_virtualenv() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let cache = Cache::temp()?;
|
||||
let generic = mock_venv(&tempdir, "3.12.0")?;
|
||||
let conda = mock_conda_prefix(&tempdir, "3.12.1")?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.2", "3.11.3"])?),
|
||||
),
|
||||
("CONDA_PREFIX", Some(conda.into())),
|
||||
("VIRTUAL_ENV", Some(generic.into())),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|| {
|
||||
let environment =
|
||||
// Note this environment is not treated as a system interpreter
|
||||
PythonEnvironment::find(None, SystemPython::Disallowed, &cache)
|
||||
.expect("An environment is found");
|
||||
assert_eq!(
|
||||
environment.interpreter().python_full_version().to_string(),
|
||||
"3.12.0",
|
||||
"We should prefer the active environment"
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_environment_from_discovered_environment() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
@@ -43,7 +43,7 @@ pub enum Error {
|
||||
|
||||
/// Locate an active virtual environment by inspecting environment variables.
|
||||
///
|
||||
/// Supports `VIRTUAL_ENV` and `CONDA_PREFIX`.
|
||||
/// Supports `VIRTUAL_ENV`.
|
||||
pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
|
||||
if let Some(dir) = env::var_os("VIRTUAL_ENV").filter(|value| !value.is_empty()) {
|
||||
info!(
|
||||
@@ -53,6 +53,13 @@ pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
|
||||
return Some(PathBuf::from(dir));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Locate an active conda environment by inspecting environment variables.
|
||||
///
|
||||
/// Supports `CONDA_PREFIX`.
|
||||
pub(crate) fn conda_prefix_from_env() -> Option<PathBuf> {
|
||||
if let Some(dir) = env::var_os("CONDA_PREFIX").filter(|value| !value.is_empty()) {
|
||||
info!(
|
||||
"Found active virtual environment (via CONDA_PREFIX) at: {}",
|
||||
|
||||
Reference in New Issue
Block a user