[ty] Discover site-packages from the environment that ty is installed in (#21286)
<!-- Thank you for contributing to Ruff/ty! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? (Please prefix with `[ty]` for ty pull requests.) - Does this pull request include references to any relevant issues? --> ## Summary <!-- What's the purpose of the change? What does it do, and why? --> Closes https://github.com/astral-sh/ty/issues/989 There are various situations where users expect the Python packages installed in the same environment as ty itself to be considered during type checking. A minimal example would look like: ``` uv venv my-env uv pip install my-env ty httpx echo "import httpx" > foo.py ./my-env/bin/ty check foo.py ``` or ``` uv tool install ty --with httpx echo "import httpx" > foo.py ty check foo.py ``` While these are a bit contrived, there are real-world situations where a user would expect a similar behavior to work. Notably, all of the other type checkers consider their own environment when determining search paths (though I'll admit that I have not verified when they choose not to do this). One common situation where users are encountering this today is with `uvx --with-requirements script.py ty check script.py` — which is currently our "best" recommendation for type checking a PEP 723 script, but it doesn't work. Of the options discussed in https://github.com/astral-sh/ty/issues/989#issuecomment-3307417985, I've chosen (2) as our criteria for including ty's environment in the search paths. - If no virtual environment is discovered, we will always include ty's environment. - If a `.venv` is discovered in the working directory, we will _prepend_ ty's environment to the search paths. The dependencies in ty's environment (e.g., from `uvx --with`) will take precedence. - If a virtual environment is active, e.g., `VIRTUAL_ENV` (i.e., including conda prefixes) is set, we will not include ty's environment. The reason we need to special case the `.venv` case is that we both 1. Recommend `uvx ty` today as a way to check your project 2. Want to enable `uvx --with <...> ty` And I don't want (2) to break when you _happen_ to be in a project (i.e., if we only included ty's environment when _no_ environment is found) and don't want to remove support for (1). I think long-term, I want to make `uvx <cmd>` layer the environment on _top_ of the project environment (in uv), which would obviate the need for this change when you're using uv. However, that change is breaking and I think users will expect this behavior in contexts where they're not using uv, so I think we should handle it in ty regardless. I've opted not to include the environment if it's non-virtual (i.e., a system environment) for now. It seems better to start by being more restrictive. I left a comment in the code. ## Test Plan I did some manual testing with the initial commit, then subsequently added some unit tests. ``` ❯ echo "import httpx" > example.py ❯ uvx --with httpx ty check example.py Installed 8 packages in 19ms error[unresolved-import]: Cannot resolve imported module `httpx` --> foo/example.py:1:8 | 1 | import httpx | ^^^^^ | info: Searched in the following paths during module resolution: info: 1. /Users/zb/workspace/ty/python (first-party code) info: 2. /Users/zb/workspace/ty (first-party code) info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty) info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment info: rule `unresolved-import` is enabled by default Found 1 diagnostic ❯ uvx --from . --with httpx ty check example.py All checks passed! ``` ``` ❯ uv init --script foo.py Initialized script at `foo.py` ❯ uv add --script foo.py httpx warning: The Python request from `.python-version` resolved to Python 3.13.8, which is incompatible with the script's Python requirement: `>=3.14` Updated `foo.py` ❯ echo "import httpx" >> foo.py ❯ uvx --with-requirements foo.py ty check foo.py error[unresolved-import]: Cannot resolve imported module `httpx` --> foo.py:15:8 | 13 | if __name__ == "__main__": 14 | main() 15 | import httpx | ^^^^^ | info: Searched in the following paths during module resolution: info: 1. /Users/zb/workspace/ty/python (first-party code) info: 2. /Users/zb/workspace/ty (first-party code) info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty) info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment info: rule `unresolved-import` is enabled by default Found 1 diagnostic ❯ uvx --from . --with-requirements foo.py ty check foo.py All checks passed! ``` Notice we do not include ty's environment if `VIRTUAL_ENV` is set ``` ❯ VIRTUAL_ENV=.venv uvx --with httpx ty check foo/example.py error[unresolved-import]: Cannot resolve imported module `httpx` --> foo/example.py:1:8 | 1 | import httpx | ^^^^^ | info: Searched in the following paths during module resolution: info: 1. /Users/zb/workspace/ty/python (first-party code) info: 2. /Users/zb/workspace/ty (first-party code) info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty) info: 4. /Users/zb/workspace/ty/.venv/lib/python3.13/site-packages (site-packages) info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment info: rule `unresolved-import` is enabled by default Found 1 diagnostic ```
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::CliTest;
|
||||
use crate::{CliTest, site_packages_filter};
|
||||
|
||||
/// Specifying an option on the CLI should take precedence over the same setting in the
|
||||
/// project's configuration. Here, this is tested for the Python version.
|
||||
@@ -1654,6 +1654,278 @@ home = ./
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ty should include site packages from its own environment when no other environment is found.
|
||||
#[test]
|
||||
fn ty_environment_is_only_environment() -> anyhow::Result<()> {
|
||||
let ty_venv_site_packages = if cfg!(windows) {
|
||||
"ty-venv/Lib/site-packages"
|
||||
} else {
|
||||
"ty-venv/lib/python3.13/site-packages"
|
||||
};
|
||||
|
||||
let ty_executable_path = if cfg!(windows) {
|
||||
"ty-venv/Scripts/ty.exe"
|
||||
} else {
|
||||
"ty-venv/bin/ty"
|
||||
};
|
||||
|
||||
let ty_package_path = format!("{ty_venv_site_packages}/ty_package/__init__.py");
|
||||
|
||||
let case = CliTest::with_files([
|
||||
(ty_package_path.as_str(), "class TyEnvClass: ..."),
|
||||
(
|
||||
"ty-venv/pyvenv.cfg",
|
||||
r"
|
||||
home = ./
|
||||
version = 3.13
|
||||
",
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r"
|
||||
from ty_package import TyEnvClass
|
||||
",
|
||||
),
|
||||
])?;
|
||||
|
||||
let case = case.with_ty_at(ty_executable_path)?;
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ty should include site packages from both its own environment and a local `.venv`. The packages
|
||||
/// from ty's environment should take precedence.
|
||||
#[test]
|
||||
fn ty_environment_and_discovered_venv() -> anyhow::Result<()> {
|
||||
let ty_venv_site_packages = if cfg!(windows) {
|
||||
"ty-venv/Lib/site-packages"
|
||||
} else {
|
||||
"ty-venv/lib/python3.13/site-packages"
|
||||
};
|
||||
|
||||
let ty_executable_path = if cfg!(windows) {
|
||||
"ty-venv/Scripts/ty.exe"
|
||||
} else {
|
||||
"ty-venv/bin/ty"
|
||||
};
|
||||
|
||||
let local_venv_site_packages = if cfg!(windows) {
|
||||
".venv/Lib/site-packages"
|
||||
} else {
|
||||
".venv/lib/python3.13/site-packages"
|
||||
};
|
||||
|
||||
let ty_unique_package = format!("{ty_venv_site_packages}/ty_package/__init__.py");
|
||||
let local_unique_package = format!("{local_venv_site_packages}/local_package/__init__.py");
|
||||
let ty_conflicting_package = format!("{ty_venv_site_packages}/shared_package/__init__.py");
|
||||
let local_conflicting_package =
|
||||
format!("{local_venv_site_packages}/shared_package/__init__.py");
|
||||
|
||||
let case = CliTest::with_files([
|
||||
(ty_unique_package.as_str(), "class TyEnvClass: ..."),
|
||||
(local_unique_package.as_str(), "class LocalClass: ..."),
|
||||
(ty_conflicting_package.as_str(), "class FromTyEnv: ..."),
|
||||
(
|
||||
local_conflicting_package.as_str(),
|
||||
"class FromLocalVenv: ...",
|
||||
),
|
||||
(
|
||||
"ty-venv/pyvenv.cfg",
|
||||
r"
|
||||
home = ./
|
||||
version = 3.13
|
||||
",
|
||||
),
|
||||
(
|
||||
".venv/pyvenv.cfg",
|
||||
r"
|
||||
home = ./
|
||||
version = 3.13
|
||||
",
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r"
|
||||
# Should resolve from ty's environment
|
||||
from ty_package import TyEnvClass
|
||||
# Should resolve from local .venv
|
||||
from local_package import LocalClass
|
||||
# Should resolve from ty's environment (takes precedence)
|
||||
from shared_package import FromTyEnv
|
||||
# Should NOT resolve (shadowed by ty's environment version)
|
||||
from shared_package import FromLocalVenv
|
||||
",
|
||||
),
|
||||
])?
|
||||
.with_ty_at(ty_executable_path)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `shared_package` has no member `FromLocalVenv`
|
||||
--> test.py:9:28
|
||||
|
|
||||
7 | from shared_package import FromTyEnv
|
||||
8 | # Should NOT resolve (shadowed by ty's environment version)
|
||||
9 | from shared_package import FromLocalVenv
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// When `VIRTUAL_ENV` is set, ty should *not* discover its own environment's site-packages.
|
||||
#[test]
|
||||
fn ty_environment_and_active_environment() -> anyhow::Result<()> {
|
||||
let ty_venv_site_packages = if cfg!(windows) {
|
||||
"ty-venv/Lib/site-packages"
|
||||
} else {
|
||||
"ty-venv/lib/python3.13/site-packages"
|
||||
};
|
||||
|
||||
let ty_executable_path = if cfg!(windows) {
|
||||
"ty-venv/Scripts/ty.exe"
|
||||
} else {
|
||||
"ty-venv/bin/ty"
|
||||
};
|
||||
|
||||
let active_venv_site_packages = if cfg!(windows) {
|
||||
"active-venv/Lib/site-packages"
|
||||
} else {
|
||||
"active-venv/lib/python3.13/site-packages"
|
||||
};
|
||||
|
||||
let ty_package_path = format!("{ty_venv_site_packages}/ty_package/__init__.py");
|
||||
let active_package_path = format!("{active_venv_site_packages}/active_package/__init__.py");
|
||||
|
||||
let case = CliTest::with_files([
|
||||
(ty_package_path.as_str(), "class TyEnvClass: ..."),
|
||||
(
|
||||
"ty-venv/pyvenv.cfg",
|
||||
r"
|
||||
home = ./
|
||||
version = 3.13
|
||||
",
|
||||
),
|
||||
(active_package_path.as_str(), "class ActiveClass: ..."),
|
||||
(
|
||||
"active-venv/pyvenv.cfg",
|
||||
r"
|
||||
home = ./
|
||||
version = 3.13
|
||||
",
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r"
|
||||
from ty_package import TyEnvClass
|
||||
from active_package import ActiveClass
|
||||
",
|
||||
),
|
||||
])?
|
||||
.with_ty_at(ty_executable_path)?
|
||||
.with_filter(&site_packages_filter("3.13"), "<site-packages>");
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command()
|
||||
.env("VIRTUAL_ENV", case.root().join("active-venv")),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `ty_package`
|
||||
--> test.py:2:6
|
||||
|
|
||||
2 | from ty_package import TyEnvClass
|
||||
| ^^^^^^^^^^
|
||||
3 | from active_package import ActiveClass
|
||||
|
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/ (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/active-venv/<site-packages> (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// When ty is installed in a system environment rather than a virtual environment, it should
|
||||
/// not include the environment's site-packages in its search path.
|
||||
#[test]
|
||||
fn ty_environment_is_system_not_virtual() -> anyhow::Result<()> {
|
||||
let ty_system_site_packages = if cfg!(windows) {
|
||||
"system-python/Lib/site-packages"
|
||||
} else {
|
||||
"system-python/lib/python3.13/site-packages"
|
||||
};
|
||||
|
||||
let ty_executable_path = if cfg!(windows) {
|
||||
"system-python/Scripts/ty.exe"
|
||||
} else {
|
||||
"system-python/bin/ty"
|
||||
};
|
||||
|
||||
let ty_package_path = format!("{ty_system_site_packages}/system_package/__init__.py");
|
||||
|
||||
let case = CliTest::with_files([
|
||||
// Package in system Python installation (should NOT be discovered)
|
||||
(ty_package_path.as_str(), "class SystemClass: ..."),
|
||||
// Note: NO pyvenv.cfg - this is a system installation, not a venv
|
||||
(
|
||||
"test.py",
|
||||
r"
|
||||
from system_package import SystemClass
|
||||
",
|
||||
),
|
||||
])?
|
||||
.with_ty_at(ty_executable_path)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `system_package`
|
||||
--> test.py:2:6
|
||||
|
|
||||
2 | from system_package import SystemClass
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/ (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn src_root_deprecation_warning() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
|
||||
Reference in New Issue
Block a user