Skip to content

Commit 35d14ab

Browse files
committed
Treat the base Conda environment as a system environment (#7691)
Closes #7124 Closes #7137
1 parent d47e93f commit 35d14ab

File tree

5 files changed

+153
-23
lines changed

5 files changed

+153
-23
lines changed

crates/uv-python/src/discovery.rs

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ use crate::microsoft_store::find_microsoft_store_pythons;
2626
#[cfg(windows)]
2727
use crate::py_launcher::{registry_pythons, WindowsPython};
2828
use crate::virtualenv::{
29-
conda_prefix_from_env, virtualenv_from_env, virtualenv_from_working_dir,
30-
virtualenv_python_executable,
29+
conda_environment_from_env, virtualenv_from_env, virtualenv_from_working_dir,
30+
virtualenv_python_executable, CondaEnvironmentKind,
3131
};
3232
use crate::{Interpreter, PythonVersion};
3333

@@ -185,6 +185,8 @@ pub enum PythonSource {
185185
ActiveEnvironment,
186186
/// A conda environment was active e.g. via `CONDA_PREFIX`
187187
CondaPrefix,
188+
/// A base conda environment was active e.g. via `CONDA_PREFIX`
189+
BaseCondaPrefix,
188190
/// An environment was discovered e.g. via `.venv`
189191
DiscoveredEnvironment,
190192
/// An executable was found in the search path i.e. `PATH`
@@ -233,27 +235,27 @@ pub enum Error {
233235
SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
234236
}
235237

236-
/// Lazily iterate over Python executables in mutable environments.
238+
/// Lazily iterate over Python executables in mutable virtual environments.
237239
///
238240
/// The following sources are supported:
239241
///
240242
/// - Active virtual environment (via `VIRTUAL_ENV`)
241-
/// - Active conda environment (via `CONDA_PREFIX`)
242243
/// - Discovered virtual environment (e.g. `.venv` in a parent directory)
243244
///
244245
/// Notably, "system" environments are excluded. See [`python_executables_from_installed`].
245-
fn python_executables_from_environments<'a>(
246+
fn python_executables_from_virtual_environments<'a>(
246247
) -> impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a {
247-
let from_virtual_environment = std::iter::once_with(|| {
248+
let from_active_environment = std::iter::once_with(|| {
248249
virtualenv_from_env()
249250
.into_iter()
250251
.map(virtualenv_python_executable)
251252
.map(|path| Ok((PythonSource::ActiveEnvironment, path)))
252253
})
253254
.flatten();
254255

256+
// N.B. we prefer the conda environment over discovered virtual environments
255257
let from_conda_environment = std::iter::once_with(|| {
256-
conda_prefix_from_env()
258+
conda_environment_from_env(CondaEnvironmentKind::Child)
257259
.into_iter()
258260
.map(virtualenv_python_executable)
259261
.map(|path| Ok((PythonSource::CondaPrefix, path)))
@@ -271,7 +273,7 @@ fn python_executables_from_environments<'a>(
271273
})
272274
.flatten_ok();
273275

274-
from_virtual_environment
276+
from_active_environment
275277
.chain(from_conda_environment)
276278
.chain(from_discovered_environment)
277279
}
@@ -406,23 +408,35 @@ fn python_executables<'a>(
406408
})
407409
.flatten();
408410

409-
let from_environments = python_executables_from_environments();
411+
// Check if the the base conda environment is active
412+
let from_base_conda_environment = std::iter::once_with(|| {
413+
conda_environment_from_env(CondaEnvironmentKind::Base)
414+
.into_iter()
415+
.map(virtualenv_python_executable)
416+
.map(|path| Ok((PythonSource::BaseCondaPrefix, path)))
417+
})
418+
.flatten();
419+
420+
let from_virtual_environments = python_executables_from_virtual_environments();
410421
let from_installed = python_executables_from_installed(version, implementation, preference);
411422

412423
// Limit the search to the relevant environment preference; we later validate that they match
413424
// the preference but queries are expensive and we query less interpreters this way.
414425
match environments {
415426
EnvironmentPreference::OnlyVirtual => {
416-
Box::new(from_parent_interpreter.chain(from_environments))
427+
Box::new(from_parent_interpreter.chain(from_virtual_environments))
417428
}
418429
EnvironmentPreference::ExplicitSystem | EnvironmentPreference::Any => Box::new(
419430
from_parent_interpreter
420-
.chain(from_environments)
431+
.chain(from_virtual_environments)
432+
.chain(from_base_conda_environment)
433+
.chain(from_installed),
434+
),
435+
EnvironmentPreference::OnlySystem => Box::new(
436+
from_parent_interpreter
437+
.chain(from_base_conda_environment)
421438
.chain(from_installed),
422439
),
423-
EnvironmentPreference::OnlySystem => {
424-
Box::new(from_parent_interpreter.chain(from_installed))
425-
}
426440
}
427441
}
428442

@@ -617,8 +631,8 @@ fn satisfies_environment_preference(
617631
) -> bool {
618632
match (
619633
preference,
620-
// Conda environments are not conformant virtual environments but we treat them as such
621-
interpreter.is_virtualenv() || matches!(source, PythonSource::CondaPrefix),
634+
// Conda environments are not conformant virtual environments but we treat them as such.
635+
interpreter.is_virtualenv() || (matches!(source, PythonSource::CondaPrefix)),
622636
) {
623637
(EnvironmentPreference::Any, _) => true,
624638
(EnvironmentPreference::OnlyVirtual, true) => true,
@@ -1515,6 +1529,7 @@ impl PythonSource {
15151529
Self::Managed | Self::Registry | Self::MicrosoftStore => false,
15161530
Self::SearchPath
15171531
| Self::CondaPrefix
1532+
| Self::BaseCondaPrefix
15181533
| Self::ProvidedPath
15191534
| Self::ParentInterpreter
15201535
| Self::ActiveEnvironment
@@ -1527,6 +1542,7 @@ impl PythonSource {
15271542
match self {
15281543
Self::Managed | Self::Registry | Self::SearchPath | Self::MicrosoftStore => false,
15291544
Self::CondaPrefix
1545+
| Self::BaseCondaPrefix
15301546
| Self::ProvidedPath
15311547
| Self::ParentInterpreter
15321548
| Self::ActiveEnvironment
@@ -1846,6 +1862,7 @@ impl VersionRequest {
18461862
Self::Default => match source {
18471863
PythonSource::ParentInterpreter
18481864
| PythonSource::CondaPrefix
1865+
| PythonSource::BaseCondaPrefix
18491866
| PythonSource::ProvidedPath
18501867
| PythonSource::DiscoveredEnvironment
18511868
| PythonSource::ActiveEnvironment => Self::Any,
@@ -2256,7 +2273,7 @@ impl fmt::Display for PythonSource {
22562273
match self {
22572274
Self::ProvidedPath => f.write_str("provided path"),
22582275
Self::ActiveEnvironment => f.write_str("active virtual environment"),
2259-
Self::CondaPrefix => f.write_str("conda prefix"),
2276+
Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
22602277
Self::DiscoveredEnvironment => f.write_str("virtual environment"),
22612278
Self::SearchPath => f.write_str("search path"),
22622279
Self::Registry => f.write_str("registry"),

crates/uv-python/src/tests.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,74 @@ fn find_python_from_conda_prefix() -> Result<()> {
949949
"We should allow the active conda python"
950950
);
951951

952+
let baseenv = context.tempdir.child("base");
953+
TestContext::mock_conda_prefix(&baseenv, "3.12.1")?;
954+
955+
// But not if it's a base environment
956+
let result = context.run_with_vars(
957+
&[
958+
("CONDA_PREFIX", Some(baseenv.as_os_str())),
959+
("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))),
960+
],
961+
|| {
962+
find_python_installation(
963+
&PythonRequest::Default,
964+
EnvironmentPreference::OnlyVirtual,
965+
PythonPreference::OnlySystem,
966+
&context.cache,
967+
)
968+
},
969+
)?;
970+
971+
assert!(
972+
matches!(result, Err(PythonNotFound { .. })),
973+
"We should not allow the non-virtual environment; got {result:?}"
974+
);
975+
976+
// Unless, system interpreters are included...
977+
let python = context.run_with_vars(
978+
&[
979+
("CONDA_PREFIX", Some(baseenv.as_os_str())),
980+
("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))),
981+
],
982+
|| {
983+
find_python_installation(
984+
&PythonRequest::Default,
985+
EnvironmentPreference::OnlySystem,
986+
PythonPreference::OnlySystem,
987+
&context.cache,
988+
)
989+
},
990+
)??;
991+
992+
assert_eq!(
993+
python.interpreter().python_full_version().to_string(),
994+
"3.12.1",
995+
"We should find the base conda environment"
996+
);
997+
998+
// If the environment name doesn't match the default, we should not treat it as system
999+
let python = context.run_with_vars(
1000+
&[
1001+
("CONDA_PREFIX", Some(condaenv.as_os_str())),
1002+
("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))),
1003+
],
1004+
|| {
1005+
find_python_installation(
1006+
&PythonRequest::Default,
1007+
EnvironmentPreference::OnlyVirtual,
1008+
PythonPreference::OnlySystem,
1009+
&context.cache,
1010+
)
1011+
},
1012+
)??;
1013+
1014+
assert_eq!(
1015+
python.interpreter().python_full_version().to_string(),
1016+
"3.12.0",
1017+
"We should find the conda environment"
1018+
);
1019+
9521020
Ok(())
9531021
}
9541022

crates/uv-python/src/virtualenv.rs

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,56 @@ pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
5757
None
5858
}
5959

60+
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
61+
pub(crate) enum CondaEnvironmentKind {
62+
/// The base Conda environment; treated like a system Python environment.
63+
Base,
64+
/// Any other Conda environment; treated like a virtual environment.
65+
Child,
66+
}
67+
68+
impl CondaEnvironmentKind {
69+
/// Whether the given `CONDA_PREFIX` path is the base Conda environment.
70+
///
71+
/// When the base environment is used, `CONDA_DEFAULT_ENV` will be set to a name, i.e., `base` or
72+
/// `root` which does not match the prefix, e.g. `/usr/local` instead of
73+
/// `/usr/local/conda/envs/<name>`.
74+
fn from_prefix_path(path: &Path) -> Self {
75+
// If we cannot read `CONDA_DEFAULT_ENV`, there's no way to know if the base environment
76+
let Ok(default_env) = env::var(EnvVars::CONDA_DEFAULT_ENV) else {
77+
return CondaEnvironmentKind::Child;
78+
};
79+
80+
// These are the expected names for the base environment
81+
if default_env != "base" && default_env != "root" {
82+
return CondaEnvironmentKind::Child;
83+
}
84+
85+
let Some(name) = path.file_name() else {
86+
return CondaEnvironmentKind::Child;
87+
};
88+
89+
if name.to_str().is_some_and(|name| name == default_env) {
90+
CondaEnvironmentKind::Base
91+
} else {
92+
CondaEnvironmentKind::Child
93+
}
94+
}
95+
}
96+
6097
/// Locate an active conda environment by inspecting environment variables.
6198
///
62-
/// Supports `CONDA_PREFIX`.
63-
pub(crate) fn conda_prefix_from_env() -> Option<PathBuf> {
64-
if let Some(dir) = env::var_os(EnvVars::CONDA_PREFIX).filter(|value| !value.is_empty()) {
65-
return Some(PathBuf::from(dir));
66-
}
99+
/// If `base` is true, the active environment must be the base environment or `None` is returned,
100+
/// and vice-versa.
101+
pub(crate) fn conda_environment_from_env(kind: CondaEnvironmentKind) -> Option<PathBuf> {
102+
let dir = env::var_os(EnvVars::CONDA_PREFIX).filter(|value| !value.is_empty())?;
103+
let path = PathBuf::from(dir);
67104

68-
None
105+
if kind != CondaEnvironmentKind::from_prefix_path(&path) {
106+
return None;
107+
};
108+
109+
Some(path)
69110
}
70111

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

crates/uv-static/src/env_vars.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@ impl EnvVars {
337337
/// Used to detect an activated Conda environment.
338338
pub const CONDA_PREFIX: &'static str = "CONDA_PREFIX";
339339

340+
/// Used to determine if an active Conda environment is the base environment or not.
341+
pub const CONDA_DEFAULT_ENV: &'static str = "CONDA_DEFAULT_ENV";
342+
340343
/// If set to `1` before a virtual environment is activated, then the
341344
/// virtual environment name will not be prepended to the terminal prompt.
342345
pub const VIRTUAL_ENV_DISABLE_PROMPT: &'static str = "VIRTUAL_ENV_DISABLE_PROMPT";

docs/configuration/environment.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ uv respects the following environment variables:
139139
See [`PycInvalidationMode`](https://docs.python.org/3/library/py_compile.html#py_compile.PycInvalidationMode).
140140
- <a id="VIRTUAL_ENV"></a> [`VIRTUAL_ENV`](#VIRTUAL_ENV): Used to detect an activated virtual environment.
141141
- <a id="CONDA_PREFIX"></a> [`CONDA_PREFIX`](#CONDA_PREFIX): Used to detect an activated Conda environment.
142+
- <a id="CONDA_DEFAULT_ENV"></a> [`CONDA_DEFAULT_ENV`](#CONDA_DEFAULT_ENV): Used to determine if an active Conda environment is the base environment or not.
142143
- <a id="VIRTUAL_ENV_DISABLE_PROMPT"></a> [`VIRTUAL_ENV_DISABLE_PROMPT`](#VIRTUAL_ENV_DISABLE_PROMPT): If set to `1` before a virtual environment is activated, then the
143144
virtual environment name will not be prepended to the terminal prompt.
144145
- <a id="PROMPT"></a> [`PROMPT`](#PROMPT): Used to detect the use of the Windows Command Prompt (as opposed to PowerShell).

0 commit comments

Comments
 (0)