[ty] distinguish base conda from child conda (#19990)

This is a port of the logic in https://github.com/astral-sh/uv/pull/7691

The basic idea is we use CONDA_DEFAULT_ENV as a signal for whether
CONDA_PREFIX is just the ambient system conda install, or the user has
explicitly activated a custom one. If the former, then the conda is
treated like a system install (having lowest priority). If the latter,
the conda is treated like an activated venv (having priority over
everything but an Actual activated venv).

Fixes https://github.com/astral-sh/ty/issues/611
This commit is contained in:
Aria Desires 2025-08-20 09:07:42 -04:00 committed by GitHub
parent 276405b44e
commit 1d2128f918
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 567 additions and 34 deletions

View file

@ -139,6 +139,12 @@ pub enum PythonEnvironment {
}
impl PythonEnvironment {
/// Discover the python environment using the following priorities:
///
/// 1. activated virtual environment
/// 2. conda (child)
/// 3. working dir virtual environment
/// 4. conda (base)
pub fn discover(
project_root: &SystemPath,
system: &dyn System,
@ -161,13 +167,9 @@ impl PythonEnvironment {
.map(Some);
}
if let Ok(conda_env) = system.env_var(EnvVars::CONDA_PREFIX) {
return resolve_environment(
system,
SystemPath::new(&conda_env),
SysPrefixPathOrigin::CondaPrefixVar,
)
.map(Some);
if let Some(conda_env) = conda_environment_from_env(system, CondaEnvironmentKind::Child) {
return resolve_environment(system, &conda_env, SysPrefixPathOrigin::CondaPrefixVar)
.map(Some);
}
tracing::debug!("Discovering virtual environment in `{project_root}`");
@ -190,6 +192,11 @@ impl PythonEnvironment {
}
}
if let Some(conda_env) = conda_environment_from_env(system, CondaEnvironmentKind::Base) {
return resolve_environment(system, &conda_env, SysPrefixPathOrigin::CondaPrefixVar)
.map(Some);
}
Ok(None)
}
@ -589,6 +596,62 @@ System stdlib will not be used for module definitions.",
}
}
/// Different kinds of conda environment
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum CondaEnvironmentKind {
/// The base Conda environment; treated like a system Python environment.
Base,
/// Any other Conda environment; treated like a virtual environment.
Child,
}
impl CondaEnvironmentKind {
/// Compute the kind of `CONDA_PREFIX` we have.
///
/// When the base environment is used, `CONDA_DEFAULT_ENV` will be set to a name, i.e., `base` or
/// `root` which does not match the prefix, e.g. `/usr/local` instead of
/// `/usr/local/conda/envs/<name>`.
fn from_prefix_path(system: &dyn System, path: &SystemPath) -> Self {
// If we cannot read `CONDA_DEFAULT_ENV`, there's no way to know if the base environment
let Ok(default_env) = system.env_var(EnvVars::CONDA_DEFAULT_ENV) else {
return CondaEnvironmentKind::Child;
};
// These are the expected names for the base environment
if default_env != "base" && default_env != "root" {
return CondaEnvironmentKind::Child;
}
let Some(name) = path.file_name() else {
return CondaEnvironmentKind::Child;
};
if name == default_env {
CondaEnvironmentKind::Base
} else {
CondaEnvironmentKind::Child
}
}
}
/// Read `CONDA_PREFIX` and confirm that it has the expected kind
pub(crate) fn conda_environment_from_env(
system: &dyn System,
kind: CondaEnvironmentKind,
) -> Option<SystemPathBuf> {
let dir = system
.env_var(EnvVars::CONDA_PREFIX)
.ok()
.filter(|value| !value.is_empty())?;
let path = SystemPathBuf::from(dir);
if kind != CondaEnvironmentKind::from_prefix_path(system, &path) {
return None;
}
Some(path)
}
/// A parser for `pyvenv.cfg` files: metadata files for virtual environments.
///
/// Note that a `pyvenv.cfg` file *looks* like a `.ini` file, but actually isn't valid `.ini` syntax!