diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index 27221733d..9104f0dc6 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -35,6 +35,7 @@ pub enum PreviewFeature { MetadataJson = 1 << 20, GcsEndpoint = 1 << 21, AdjustUlimit = 1 << 22, + SpecialCondaEnvNames = 1 << 23, } impl PreviewFeature { @@ -64,6 +65,7 @@ impl PreviewFeature { Self::MetadataJson => "metadata-json", Self::GcsEndpoint => "gcs-endpoint", Self::AdjustUlimit => "adjust-ulimit", + Self::SpecialCondaEnvNames => "special-conda-env-names", } } } @@ -106,6 +108,7 @@ impl FromStr for PreviewFeature { "target-workspace-discovery" => Self::TargetWorkspaceDiscovery, "metadata-json" => Self::MetadataJson, "adjust-ulimit" => Self::AdjustUlimit, + "special-conda-env-names" => Self::SpecialCondaEnvNames, _ => return Err(PreviewFeatureParseError), }) } @@ -329,5 +332,9 @@ mod tests { assert_eq!(PreviewFeature::MetadataJson.as_str(), "metadata-json"); assert_eq!(PreviewFeature::GcsEndpoint.as_str(), "gcs-endpoint"); assert_eq!(PreviewFeature::AdjustUlimit.as_str(), "adjust-ulimit"); + assert_eq!( + PreviewFeature::SpecialCondaEnvNames.as_str(), + "special-conda-env-names" + ); } } diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index f7328aa10..94b52bbdb 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -285,8 +285,9 @@ pub enum Error { /// - Discovered virtual environment (e.g. `.venv` in a parent directory) /// /// Notably, "system" environments are excluded. See [`python_executables_from_installed`]. -fn python_executables_from_virtual_environments<'a>() --> impl Iterator> + 'a { +fn python_executables_from_virtual_environments<'a>( + preview: Preview, +) -> impl Iterator> + 'a { let from_active_environment = iter::once_with(|| { virtualenv_from_env() .into_iter() @@ -296,8 +297,8 @@ fn python_executables_from_virtual_environments<'a>() .flatten(); // N.B. we prefer the conda environment over discovered virtual environments - let from_conda_environment = iter::once_with(|| { - conda_environment_from_env(CondaEnvironmentKind::Child) + let from_conda_environment = iter::once_with(move || { + conda_environment_from_env(CondaEnvironmentKind::Child, preview) .into_iter() .map(virtualenv_python_executable) .map(|path| Ok((PythonSource::CondaPrefix, path))) @@ -524,15 +525,15 @@ fn python_executables<'a>( .flatten(); // Check if the base conda environment is active - let from_base_conda_environment = iter::once_with(|| { - conda_environment_from_env(CondaEnvironmentKind::Base) + let from_base_conda_environment = iter::once_with(move || { + conda_environment_from_env(CondaEnvironmentKind::Base, preview) .into_iter() .map(virtualenv_python_executable) .map(|path| Ok((PythonSource::BaseCondaPrefix, path))) }) .flatten(); - let from_virtual_environments = python_executables_from_virtual_environments(); + let from_virtual_environments = python_executables_from_virtual_environments(preview); let from_installed = python_executables_from_installed(version, implementation, platform, preference, preview); diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index e71aac0c1..e2776063a 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -136,7 +136,7 @@ mod tests { use temp_env::with_vars; use test_log::test; use uv_client::BaseClientBuilder; - use uv_preview::Preview; + use uv_preview::{Preview, PreviewFeature}; use uv_static::EnvVars; use uv_cache::Cache; @@ -1335,6 +1335,36 @@ mod tests { "We should not allow the base environment when looking for virtual environments" ); + // With the `special-conda-env-names` preview feature, "base" is not special-cased + // and uses path-based heuristics instead. When the directory name matches the env name, + // it should be treated as a child environment. + let base_dir = context.tempdir.child("base"); + TestContext::mock_conda_prefix(&base_dir, "3.12.6")?; + let python = context + .run_with_vars( + &[ + (EnvVars::CONDA_PREFIX, Some(base_dir.as_os_str())), + (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("base"))), + (EnvVars::CONDA_ROOT, None), + ], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlyVirtual, + PythonPreference::OnlySystem, + &context.cache, + Preview::new(&[PreviewFeature::SpecialCondaEnvNames]), + ) + }, + )? + .unwrap(); + + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.6", + "With special-conda-env-names preview, 'base' named env in matching dir should be treated as child" + ); + // When environment name matches directory name, it should be treated as a child environment let myenv_dir = context.tempdir.child("myenv"); TestContext::mock_conda_prefix(&myenv_dir, "3.12.5")?; diff --git a/crates/uv-python/src/virtualenv.rs b/crates/uv-python/src/virtualenv.rs index b21b3f77c..d9c6c4ec9 100644 --- a/crates/uv-python/src/virtualenv.rs +++ b/crates/uv-python/src/virtualenv.rs @@ -8,6 +8,7 @@ use std::{ use fs_err as fs; use thiserror::Error; +use uv_preview::{Preview, PreviewFeature}; use uv_pypi_types::Scheme; use uv_static::EnvVars; @@ -85,7 +86,7 @@ impl CondaEnvironmentKind { /// name, e.g., `base`, which does not match the `CONDA_PREFIX`, e.g., `/usr/local` instead of /// `/usr/local/conda/envs/`. Note the name `CONDA_DEFAULT_ENV` is misleading, it's the /// active environment name, not a constant base environment name. - fn from_prefix_path(path: &Path) -> Self { + fn from_prefix_path(path: &Path, preview: Preview) -> Self { // Pixi never creates true "base" envs and names project envs "default", confusing our // heuristics, so treat Pixi prefixes as child envs outright. if is_pixi_environment(path) { @@ -113,8 +114,10 @@ impl CondaEnvironmentKind { // If the environment name is "base" or "root", treat it as a base environment // // These are the expected names for the base environment; and is retained for backwards - // compatibility, but in a future breaking release we should remove this special-casing. - if current_env == "base" || current_env == "root" { + // compatibility, but can be removed with the `special-conda-env-names` preview feature. + if !preview.is_enabled(PreviewFeature::SpecialCondaEnvNames) + && (current_env == "base" || current_env == "root") + { return Self::Base; } @@ -142,11 +145,14 @@ fn is_pixi_environment(path: &Path) -> bool { /// /// If `base` is true, the active environment must be the base environment or `None` is returned, /// and vice-versa. -pub(crate) fn conda_environment_from_env(kind: CondaEnvironmentKind) -> Option { +pub(crate) fn conda_environment_from_env( + kind: CondaEnvironmentKind, + preview: Preview, +) -> Option { let dir = env::var_os(EnvVars::CONDA_PREFIX).filter(|value| !value.is_empty())?; let path = PathBuf::from(dir); - if kind != CondaEnvironmentKind::from_prefix_path(&path) { + if kind != CondaEnvironmentKind::from_prefix_path(&path, preview) { return None; } @@ -353,7 +359,7 @@ mod tests { with_vars(vars, || { assert_eq!( - CondaEnvironmentKind::from_prefix_path(prefix), + CondaEnvironmentKind::from_prefix_path(prefix, Preview::default()), CondaEnvironmentKind::Child ); }); diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 12efa9893..b244a174b 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -7930,6 +7930,7 @@ fn preview_features() { MetadataJson, GcsEndpoint, AdjustUlimit, + SpecialCondaEnvNames, ], }, python_preference: Managed, @@ -8188,6 +8189,7 @@ fn preview_features() { MetadataJson, GcsEndpoint, AdjustUlimit, + SpecialCondaEnvNames, ], }, python_preference: Managed,