Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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