Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions crates/ty/docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@ ty also reads the following externally defined environment variables:

### `CONDA_DEFAULT_ENV`

Used to determine if an active Conda environment is the base environment or not.
Used to determine the name of the active Conda environment.

### `CONDA_PREFIX`

Used to detect an activated Conda environment location.
Used to detect the path of an active Conda environment.
If both `VIRTUAL_ENV` and `CONDA_PREFIX` are present, `VIRTUAL_ENV` will be preferred.

### `_CONDA_ROOT`
Copy link
Contributor Author

@Danielkonge Danielkonge Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uv writes CONDA_ROOT instead of _CONDA_ROOT, but the section is "Externally-defined variables", and the actual environment variable is with the _, so I wrote it like this.


Used to determine the root install path of Conda.

### `PYTHONPATH`

Adds additional directories to ty's search paths.
Expand Down
177 changes: 137 additions & 40 deletions crates/ty/tests/cli/python_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -943,9 +943,10 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
/// The `site-packages` directory is used by ty for external import.
/// Ty does the following checks to discover the `site-packages` directory in the order:
/// 1) If `VIRTUAL_ENV` environment variable is set
/// 2) If `CONDA_PREFIX` environment variable is set (and .filename != `CONDA_DEFAULT_ENV`)
/// 2) If `CONDA_PREFIX` environment variable is set (and .filename == `CONDA_DEFAULT_ENV`)
/// 3) If a `.venv` directory exists at the project root
/// 4) If `CONDA_PREFIX` environment variable is set (and .filename == `CONDA_DEFAULT_ENV`)
/// 4) If `CONDA_PREFIX` environment variable is set (and .filename != `CONDA_DEFAULT_ENV`)
/// or if `_CONDA_ROOT` is set (and `_CONDA_ROOT` == `CONDA_PREFIX`)
///
/// This test (and the next one) is aiming at validating the logic around these cases.
///
Expand Down Expand Up @@ -986,15 +987,14 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
/// │ └── site-packages
/// │ └── package1
/// │ └── __init__.py
/// ├── conda-env
/// │ └── lib
/// │ └── python3.13
/// │ └── site-packages
/// │ └── package1
/// │ └── __init__.py
/// └── conda
/// ├── lib
/// │ └── python3.13
/// │ └── site-packages
/// │ └── package1
/// │ └── __init__.py
/// └── envs
/// └── base
/// └── conda-env
/// └── lib
/// └── python3.13
/// └── site-packages
Expand All @@ -1006,15 +1006,15 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
#[test]
fn check_venv_resolution_with_working_venv() -> anyhow::Result<()> {
let child_conda_package1_path = if cfg!(windows) {
"conda-env/Lib/site-packages/package1/__init__.py"
"conda/envs/conda-env/Lib/site-packages/package1/__init__.py"
} else {
"conda-env/lib/python3.13/site-packages/package1/__init__.py"
"conda/envs/conda-env/lib/python3.13/site-packages/package1/__init__.py"
};

let base_conda_package1_path = if cfg!(windows) {
"conda/envs/base/Lib/site-packages/package1/__init__.py"
"conda/Lib/site-packages/package1/__init__.py"
} else {
"conda/envs/base/lib/python3.13/site-packages/package1/__init__.py"
"conda//lib/python3.13/site-packages/package1/__init__.py"
};

let working_venv_package1_path = if cfg!(windows) {
Expand Down Expand Up @@ -1136,7 +1136,7 @@ home = ./
// run with CONDA_PREFIX set, should find the child conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env")), @r"
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env")), @r"
success: false
exit_code: 1
----- stdout -----
Expand All @@ -1157,21 +1157,21 @@ home = ./
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");

// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find child conda
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find working venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("CONDA_DEFAULT_ENV", "base"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
error[unresolved-import]: Module `package1` has no member `WorkingVenv`
--> test.py:4:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
| ^^^^^^^^^^^
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Expand All @@ -1186,7 +1186,7 @@ home = ./
// should find child active venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("CONDA_DEFAULT_ENV", "base")
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
success: false
Expand All @@ -1208,21 +1208,46 @@ home = ./
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");

// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find working venv
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find ChildConda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda/envs/base"))
.env("CONDA_DEFAULT_ENV", "base"), @r"
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env"))
.env("CONDA_DEFAULT_ENV", "conda-env"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `WorkingVenv`
--> test.py:4:22
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default

Found 1 diagnostic

----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");

// run with _CONDA_ROOT and CONDA_PREFIX (unequal!) set, should find ChildConda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env"))
.env("_CONDA_ROOT", "conda"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
| ^^^^^^^^^^^
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Expand All @@ -1233,6 +1258,30 @@ home = ./
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");

// run with _CONDA_ROOT and CONDA_PREFIX (equal!) set, should find BaseConda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("_CONDA_ROOT", "conda"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `BaseConda`
--> test.py:5:22
|
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
| ^^^^^^^^^
|
info: rule `unresolved-import` is enabled by default

Found 1 diagnostic

----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");

Ok(())
}

Expand All @@ -1242,15 +1291,15 @@ home = ./
#[test]
fn check_venv_resolution_without_working_venv() -> anyhow::Result<()> {
let child_conda_package1_path = if cfg!(windows) {
"conda-env/Lib/site-packages/package1/__init__.py"
"conda/envs/conda-env/Lib/site-packages/package1/__init__.py"
} else {
"conda-env/lib/python3.13/site-packages/package1/__init__.py"
"conda/envs/conda-env/lib/python3.13/site-packages/package1/__init__.py"
};

let base_conda_package1_path = if cfg!(windows) {
"conda/envs/base/Lib/site-packages/package1/__init__.py"
"conda/Lib/site-packages/package1/__init__.py"
} else {
"conda/envs/base/lib/python3.13/site-packages/package1/__init__.py"
"conda/lib/python3.13/site-packages/package1/__init__.py"
};

let active_venv_package1_path = if cfg!(windows) {
Expand Down Expand Up @@ -1398,7 +1447,7 @@ home = ./
// run with CONDA_PREFIX set, should find the child conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env")), @r"
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env")), @r"
success: false
exit_code: 1
----- stdout -----
Expand All @@ -1419,22 +1468,21 @@ home = ./
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");

// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find child conda
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find base conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("CONDA_DEFAULT_ENV", "base"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
error[unresolved-import]: Module `package1` has no member `BaseConda`
--> test.py:5:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
| ^^^^^^^^^
|
info: rule `unresolved-import` is enabled by default

Expand All @@ -1448,7 +1496,7 @@ home = ./
// should find child active venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("CONDA_DEFAULT_ENV", "base")
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
success: false
Expand All @@ -1470,10 +1518,10 @@ home = ./
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");

// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find base conda
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (unequal!) set, should find base conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda/envs/base"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("CONDA_DEFAULT_ENV", "base"), @r"
success: false
exit_code: 1
Expand All @@ -1494,6 +1542,55 @@ home = ./
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");

// run with _CONDA_ROOT and CONDA_PREFIX (unequal!) set, should find ChildConda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env"))
.env("_CONDA_ROOT", "conda"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default

Found 1 diagnostic

----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");

// run with _CONDA_ROOT and CONDA_PREFIX (equal!) set, should find BaseConda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("_CONDA_ROOT", "conda"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `BaseConda`
--> test.py:5:22
|
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
| ^^^^^^^^^
|
info: rule `unresolved-import` is enabled by default

Found 1 diagnostic

----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");

Ok(())
}

Expand Down
42 changes: 29 additions & 13 deletions crates/ty_python_semantic/src/site_packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -614,28 +614,44 @@ pub(crate) enum CondaEnvironmentKind {
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>`.
/// The base environment is typically stored in a location matching the `_CONDA_ROOT` path.
///
/// Additionally, when the base environment is active, `CONDA_DEFAULT_ENV` will be set to a
/// name, e.g., `base`, which does not match the `CONDA_PREFIX`, e.g., `/usr/local` instead of
/// `/usr/local/conda/envs/<name>`. Note that the name `CONDA_DEFAULT_ENV` is misleading, it's
/// the active environment name, not a constant base environment 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;
// If `_CONDA_ROOT` is set and matches `CONDA_PREFIX`, it's the base environment.
if let Ok(conda_root) = system.env_var(EnvVars::CONDA_ROOT) {
if path.as_str() == conda_root {
return Self::Base;
}
}

// Next, we'll use a heuristic based on `CONDA_DEFAULT_ENV`
let Ok(current_env) = system.env_var(EnvVars::CONDA_DEFAULT_ENV) else {
return Self::Child;
};

// These are the expected names for the base environment
if default_env != "base" && default_env != "root" {
return CondaEnvironmentKind::Child;
// 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" {
return Self::Base;
}

// For other environment names, use the path-based logic
let Some(name) = path.file_name() else {
return CondaEnvironmentKind::Child;
return Self::Child;
};

if name == default_env {
CondaEnvironmentKind::Base
// If the environment is in a directory matching the name of the environment, it's not
// usually a base environment.
if name == current_env {
Self::Child
} else {
CondaEnvironmentKind::Child
Self::Base
}
}
}
Expand Down
Loading
Loading