diff --git a/crates/ty/docs/environment.md b/crates/ty/docs/environment.md index 9b9061518f..12cdf45ea8 100644 --- a/crates/ty/docs/environment.md +++ b/crates/ty/docs/environment.md @@ -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` + +Used to determine the root install path of Conda. + ### `PYTHONPATH` Adds additional directories to ty's search paths. diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs index 483379368a..3a96129a65 100644 --- a/crates/ty/tests/cli/python_environment.rs +++ b/crates/ty/tests/cli/python_environment.rs @@ -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. /// @@ -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 @@ -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) { @@ -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 ----- @@ -1157,61 +1157,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 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_DEFAULT_ENV", "base"), @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_PREFIX and CONDA_DEFAULT_ENV (unequal) and VIRTUAL_ENV set, - // 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_DEFAULT_ENV", "base") - .env("VIRTUAL_ENV", case.root().join("myvenv")), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-import]: Module `package1` has no member `ActiveVenv` - --> test.py:2:22 - | - 2 | from package1 import ActiveVenv - | ^^^^^^^^^^ - 3 | from package1 import ChildConda - 4 | from package1 import WorkingVenv - | - 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_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find working venv - 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 @@ -1233,6 +1182,106 @@ 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 (unequal) and VIRTUAL_ENV set, + // should find child active venv + assert_cmd_snapshot!(case.command() + .current_dir(case.root().join("project")) + .env("CONDA_PREFIX", case.root().join("conda")) + .env("CONDA_DEFAULT_ENV", "base") + .env("VIRTUAL_ENV", case.root().join("myvenv")), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Module `package1` has no member `ActiveVenv` + --> test.py:2:22 + | + 2 | from package1 import ActiveVenv + | ^^^^^^^^^^ + 3 | from package1 import ChildConda + 4 | from package1 import WorkingVenv + | + 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_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/conda-env")) + .env("CONDA_DEFAULT_ENV", "conda-env"), @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 (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(()) } @@ -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) { @@ -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 ----- @@ -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 @@ -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 @@ -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 @@ -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(()) } diff --git a/crates/ty_python_semantic/src/site_packages.rs b/crates/ty_python_semantic/src/site_packages.rs index f7f84c8d7a..af810950c6 100644 --- a/crates/ty_python_semantic/src/site_packages.rs +++ b/crates/ty_python_semantic/src/site_packages.rs @@ -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/`. + /// 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/`. 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; - }; - - // These are the expected names for the base environment - if default_env != "base" && default_env != "root" { - 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; + } } - let Some(name) = path.file_name() else { - return CondaEnvironmentKind::Child; + // 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; }; - if name == default_env { - CondaEnvironmentKind::Base + // 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 Self::Child; + }; + + // 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 } } } diff --git a/crates/ty_static/src/env_vars.rs b/crates/ty_static/src/env_vars.rs index bd7c05a3c0..b15edffd7e 100644 --- a/crates/ty_static/src/env_vars.rs +++ b/crates/ty_static/src/env_vars.rs @@ -48,13 +48,16 @@ impl EnvVars { /// (e.g. colons on Unix or semicolons on Windows). pub const PYTHONPATH: &'static str = "PYTHONPATH"; - /// Used to determine if an active Conda environment is the base environment or not. + /// Used to determine the name of the active Conda environment. pub const CONDA_DEFAULT_ENV: &'static str = "CONDA_DEFAULT_ENV"; - /// 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. pub const CONDA_PREFIX: &'static str = "CONDA_PREFIX"; + /// Used to determine the root install path of Conda. + pub const CONDA_ROOT: &'static str = "_CONDA_ROOT"; + /// Filter which tests to run in mdtest. /// /// Only tests whose names contain this filter string will be executed.