Skip to content

Commit d05a42c

Browse files
committed
Treat the base Conda environment as a system environment
Closes #7124
1 parent 106633a commit d05a42c

File tree

3 files changed

+108
-21
lines changed

3 files changed

+108
-21
lines changed

crates/uv-python/src/discovery.rs

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::microsoft_store::find_microsoft_store_pythons;
2424
#[cfg(windows)]
2525
use crate::py_launcher::{registry_pythons, WindowsPython};
2626
use crate::virtualenv::{
27-
conda_prefix_from_env, virtualenv_from_env, virtualenv_from_working_dir,
27+
conda_environment_from_env, virtualenv_from_env, virtualenv_from_working_dir,
2828
virtualenv_python_executable,
2929
};
3030
use crate::which::is_executable;
@@ -171,6 +171,8 @@ pub enum PythonSource {
171171
ActiveEnvironment,
172172
/// A conda environment was active e.g. via `CONDA_PREFIX`
173173
CondaPrefix,
174+
/// A base conda environment was active e.g. via `CONDA_PREFIX`
175+
BaseCondaPrefix,
174176
/// An environment was discovered e.g. via `.venv`
175177
DiscoveredEnvironment,
176178
/// An executable was found in the search path i.e. `PATH`
@@ -215,27 +217,27 @@ pub enum Error {
215217
SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
216218
}
217219

218-
/// Lazily iterate over Python executables in mutable environments.
220+
/// Lazily iterate over Python executables in mutable virtual environments.
219221
///
220222
/// The following sources are supported:
221223
///
222224
/// - Active virtual environment (via `VIRTUAL_ENV`)
223-
/// - Active conda environment (via `CONDA_PREFIX`)
224225
/// - Discovered virtual environment (e.g. `.venv` in a parent directory)
225226
///
226227
/// Notably, "system" environments are excluded. See [`python_executables_from_installed`].
227-
fn python_executables_from_environments<'a>(
228+
fn python_executables_from_virtual_environments<'a>(
228229
) -> impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a {
229-
let from_virtual_environment = std::iter::once_with(|| {
230+
let from_active_environment = std::iter::once_with(|| {
230231
virtualenv_from_env()
231232
.into_iter()
232233
.map(virtualenv_python_executable)
233234
.map(|path| Ok((PythonSource::ActiveEnvironment, path)))
234235
})
235236
.flatten();
236237

238+
// N.B. we prefer the conda environment over discovered virtual environments
237239
let from_conda_environment = std::iter::once_with(|| {
238-
conda_prefix_from_env()
240+
conda_environment_from_env(false)
239241
.into_iter()
240242
.map(virtualenv_python_executable)
241243
.map(|path| Ok((PythonSource::CondaPrefix, path)))
@@ -253,7 +255,7 @@ fn python_executables_from_environments<'a>(
253255
})
254256
.flatten_ok();
255257

256-
from_virtual_environment
258+
from_active_environment
257259
.chain(from_conda_environment)
258260
.chain(from_discovered_environment)
259261
}
@@ -396,23 +398,35 @@ fn python_executables<'a>(
396398
})
397399
.flatten();
398400

399-
let from_environments = python_executables_from_environments();
401+
// Check if the the base conda environment is active
402+
let from_base_conda_environment = std::iter::once_with(|| {
403+
conda_environment_from_env(true)
404+
.into_iter()
405+
.map(virtualenv_python_executable)
406+
.map(|path| Ok((PythonSource::BaseCondaPrefix, path)))
407+
})
408+
.flatten();
409+
410+
let from_virtual_environments = python_executables_from_virtual_environments();
400411
let from_installed = python_executables_from_installed(version, implementation, preference);
401412

402413
// Limit the search to the relevant environment preference; we later validate that they match
403414
// the preference but queries are expensive and we query less interpreters this way.
404415
match environments {
405416
EnvironmentPreference::OnlyVirtual => {
406-
Box::new(from_parent_interpreter.chain(from_environments))
417+
Box::new(from_parent_interpreter.chain(from_virtual_environments))
407418
}
408419
EnvironmentPreference::ExplicitSystem | EnvironmentPreference::Any => Box::new(
409420
from_parent_interpreter
410-
.chain(from_environments)
421+
.chain(from_virtual_environments)
422+
.chain(from_base_conda_environment)
423+
.chain(from_installed),
424+
),
425+
EnvironmentPreference::OnlySystem => Box::new(
426+
from_parent_interpreter
427+
.chain(from_base_conda_environment)
411428
.chain(from_installed),
412429
),
413-
EnvironmentPreference::OnlySystem => {
414-
Box::new(from_parent_interpreter.chain(from_installed))
415-
}
416430
}
417431
}
418432

@@ -607,8 +621,8 @@ fn satisfies_environment_preference(
607621
) -> bool {
608622
match (
609623
preference,
610-
// Conda environments are not conformant virtual environments but we treat them as such
611-
interpreter.is_virtualenv() || matches!(source, PythonSource::CondaPrefix),
624+
// Conda environments are not conformant virtual environments but we treat them as such.
625+
interpreter.is_virtualenv() || (matches!(source, PythonSource::CondaPrefix)),
612626
) {
613627
(EnvironmentPreference::Any, _) => true,
614628
(EnvironmentPreference::OnlyVirtual, true) => true,
@@ -1458,6 +1472,7 @@ impl PythonSource {
14581472
Self::Managed | Self::Registry | Self::MicrosoftStore => false,
14591473
Self::SearchPath
14601474
| Self::CondaPrefix
1475+
| Self::BaseCondaPrefix
14611476
| Self::ProvidedPath
14621477
| Self::ParentInterpreter
14631478
| Self::ActiveEnvironment
@@ -1470,6 +1485,7 @@ impl PythonSource {
14701485
match self {
14711486
Self::Managed | Self::Registry | Self::SearchPath | Self::MicrosoftStore => false,
14721487
Self::CondaPrefix
1488+
| Self::BaseCondaPrefix
14731489
| Self::ProvidedPath
14741490
| Self::ParentInterpreter
14751491
| Self::ActiveEnvironment
@@ -2076,7 +2092,7 @@ impl fmt::Display for PythonSource {
20762092
match self {
20772093
Self::ProvidedPath => f.write_str("provided path"),
20782094
Self::ActiveEnvironment => f.write_str("active virtual environment"),
2079-
Self::CondaPrefix => f.write_str("conda prefix"),
2095+
Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
20802096
Self::DiscoveredEnvironment => f.write_str("virtual environment"),
20812097
Self::SearchPath => f.write_str("search path"),
20822098
Self::Registry => f.write_str("registry"),

crates/uv-python/src/lib.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,12 +1037,56 @@ mod tests {
10371037
&context.cache,
10381038
)
10391039
})??;
1040+
10401041
assert_eq!(
10411042
python.interpreter().python_full_version().to_string(),
10421043
"3.12.0",
10431044
"We should allow the active conda python"
10441045
);
10451046

1047+
// But not if it's a base environment
1048+
let result = context.run_with_vars(
1049+
&[
1050+
("CONDA_PREFIX", Some(condaenv.as_os_str())),
1051+
("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))),
1052+
],
1053+
|| {
1054+
find_python_installation(
1055+
&PythonRequest::Default,
1056+
EnvironmentPreference::OnlyVirtual,
1057+
PythonPreference::OnlySystem,
1058+
&context.cache,
1059+
)
1060+
},
1061+
)?;
1062+
1063+
assert!(
1064+
matches!(result, Err(PythonNotFound { .. })),
1065+
"We should not allow the non-virtual environment; got {result:?}"
1066+
);
1067+
1068+
// Unless, system interpreters are included...
1069+
let python = context.run_with_vars(
1070+
&[
1071+
("CONDA_PREFIX", Some(condaenv.as_os_str())),
1072+
("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))),
1073+
],
1074+
|| {
1075+
find_python_installation(
1076+
&PythonRequest::Default,
1077+
EnvironmentPreference::OnlySystem,
1078+
PythonPreference::OnlySystem,
1079+
&context.cache,
1080+
)
1081+
},
1082+
)??;
1083+
1084+
assert_eq!(
1085+
python.interpreter().python_full_version().to_string(),
1086+
"3.12.0",
1087+
"We should find the base conda environment"
1088+
);
1089+
10461090
Ok(())
10471091
}
10481092

crates/uv-python/src/virtualenv.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,40 @@ pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
5858

5959
/// Locate an active conda environment by inspecting environment variables.
6060
///
61-
/// Supports `CONDA_PREFIX`.
62-
pub(crate) fn conda_prefix_from_env() -> Option<PathBuf> {
63-
if let Some(dir) = env::var_os("CONDA_PREFIX").filter(|value| !value.is_empty()) {
64-
return Some(PathBuf::from(dir));
61+
/// If `base` is true, the active environment must be the base environment or `None` is returned,
62+
/// and vice-versa.
63+
pub(crate) fn conda_environment_from_env(base: bool) -> Option<PathBuf> {
64+
let dir = env::var_os("CONDA_PREFIX").filter(|value| !value.is_empty())?;
65+
let path = PathBuf::from(dir);
66+
67+
if base != is_conda_base_env(&path) {
68+
return None;
69+
};
70+
71+
Some(path)
72+
}
73+
74+
/// Whether the given `CONDA_PREFIX` path is the base Conda environment.
75+
///
76+
/// When the base environment is used, `CONDA_DEFAULT_ENV` will be set to a name, i.e., `base` or
77+
/// `root` which does not match the prefix, e.g. `/usr/local` instead of
78+
/// `/usr/local/conda/envs/<name>`.
79+
fn is_conda_base_env(path: &Path) -> bool {
80+
// If we cannot read `CONDA_DEFAULT_ENV`, there's no way to know if the base environment
81+
let Ok(default_env) = env::var("CONDA_DEFAULT_ENV") else {
82+
return false;
83+
};
84+
85+
// These are the expected names for the base environment
86+
if default_env != "base" && default_env != "root" {
87+
return false;
6588
}
6689

67-
None
90+
let Some(name) = path.file_name() else {
91+
return false;
92+
};
93+
94+
name.to_string_lossy() != default_env
6895
}
6996

7097
/// Locate a virtual environment by searching the file system.

0 commit comments

Comments
 (0)