Skip to content

Commit 8a04266

Browse files
carljmclaude
andauthored
[ty] Allow discovering dependencies in system Python environments (#22994)
## Summary Fixes astral-sh/ty#2068. This change allows ty to discover and use dependencies installed in system Python environments where ty itself is installed. The subtlety here is in the case where we discover a local `.venv` AND ty is installed in a different Python env. In this case, we prioritize the local `.venv`, to avoid false-negatives due to accidental reliance on globally installed packages. ## Test Plan Added test; existing tests continue to pass. Co-authored-by: Claude <noreply@anthropic.com>
1 parent 55d06c8 commit 8a04266

3 files changed

Lines changed: 115 additions & 20 deletions

File tree

crates/ty/tests/cli/python_environment.rs

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2181,7 +2181,7 @@ fn ty_environment_and_active_environment() -> anyhow::Result<()> {
21812181
}
21822182

21832183
/// When ty is installed in a system environment rather than a virtual environment, it should
2184-
/// not include the environment's site-packages in its search path.
2184+
/// include the environment's site-packages in its search path.
21852185
#[test]
21862186
fn ty_environment_is_system_not_virtual() -> anyhow::Result<()> {
21872187
let ty_system_site_packages = if cfg!(windows) {
@@ -2199,7 +2199,7 @@ fn ty_environment_is_system_not_virtual() -> anyhow::Result<()> {
21992199
let ty_package_path = format!("{ty_system_site_packages}/system_package/__init__.py");
22002200

22012201
let case = CliTest::with_files([
2202-
// Package in system Python installation (should NOT be discovered)
2202+
// Package in system Python installation (should be discovered)
22032203
(ty_package_path.as_str(), "class SystemClass: ..."),
22042204
// Note: NO pyvenv.cfg - this is a system installation, not a venv
22052205
(
@@ -2212,18 +2212,85 @@ fn ty_environment_is_system_not_virtual() -> anyhow::Result<()> {
22122212
.with_ty_at(ty_executable_path)?;
22132213

22142214
assert_cmd_snapshot!(case.command(), @"
2215+
success: true
2216+
exit_code: 0
2217+
----- stdout -----
2218+
All checks passed!
2219+
2220+
----- stderr -----
2221+
");
2222+
2223+
Ok(())
2224+
}
2225+
2226+
/// When ty is installed in a system environment and there's also a local `.venv`,
2227+
/// the system environment's site-packages should not be included at all.
2228+
/// This is the opposite of when ty is installed in a virtual environment (like `uvx --with ...`),
2229+
/// where ty's venv takes priority but both are included.
2230+
#[test]
2231+
fn ty_system_environment_and_local_venv() -> anyhow::Result<()> {
2232+
let ty_system_site_packages = if cfg!(windows) {
2233+
"system-python/Lib/site-packages"
2234+
} else {
2235+
"system-python/lib/python3.13/site-packages"
2236+
};
2237+
2238+
let ty_executable_path = if cfg!(windows) {
2239+
"system-python/Scripts/ty.exe"
2240+
} else {
2241+
"system-python/bin/ty"
2242+
};
2243+
2244+
let local_venv_site_packages = if cfg!(windows) {
2245+
".venv/Lib/site-packages"
2246+
} else {
2247+
".venv/lib/python3.13/site-packages"
2248+
};
2249+
2250+
let ty_unique_package = format!("{ty_system_site_packages}/system_package/__init__.py");
2251+
let local_unique_package = format!("{local_venv_site_packages}/local_package/__init__.py");
2252+
2253+
let case = CliTest::with_files([
2254+
(ty_unique_package.as_str(), "class SystemEnvClass: ..."),
2255+
(local_unique_package.as_str(), "class LocalClass: ..."),
2256+
// Note: NO pyvenv.cfg for system-python - this is a system installation, not a venv
2257+
(
2258+
".venv/pyvenv.cfg",
2259+
r"
2260+
home = ./
2261+
version = 3.13
2262+
",
2263+
),
2264+
(
2265+
"test.py",
2266+
r"
2267+
# Should NOT resolve (system Python site-packages excluded when .venv exists)
2268+
from system_package import SystemEnvClass
2269+
# Should resolve from local .venv
2270+
from local_package import LocalClass
2271+
",
2272+
),
2273+
])?
2274+
.with_ty_at(ty_executable_path)?
2275+
.with_filter(&site_packages_filter("3.13"), "<site-packages>");
2276+
2277+
assert_cmd_snapshot!(case.command().env_remove("VIRTUAL_ENV"), @"
22152278
success: false
22162279
exit_code: 1
22172280
----- stdout -----
22182281
error[unresolved-import]: Cannot resolve imported module `system_package`
2219-
--> test.py:2:6
2282+
--> test.py:3:6
22202283
|
2221-
2 | from system_package import SystemClass
2284+
2 | # Should NOT resolve (system Python site-packages excluded when .venv exists)
2285+
3 | from system_package import SystemEnvClass
22222286
| ^^^^^^^^^^^^^^
2287+
4 | # Should resolve from local .venv
2288+
5 | from local_package import LocalClass
22232289
|
22242290
info: Searched in the following paths during module resolution:
22252291
info: 1. <temp_dir>/ (first-party code)
22262292
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
2293+
info: 3. <temp_dir>/.venv/<site-packages> (site-packages)
22272294
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
22282295
info: rule `unresolved-import` is enabled by default
22292296

crates/ty_project/src/metadata/options.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,14 +184,13 @@ impl Options {
184184
}
185185
};
186186

187-
let self_site_packages = self_environment_search_paths(
187+
let self_environment = self_environment_search_paths(
188188
python_environment
189189
.as_ref()
190190
.map(ty_python_semantic::PythonEnvironment::origin)
191191
.cloned(),
192192
system,
193-
)
194-
.unwrap_or_default();
193+
);
195194

196195
let site_packages_paths = if let Some(python_environment) = python_environment.as_ref() {
197196
let site_packages_paths = python_environment
@@ -210,10 +209,19 @@ impl Options {
210209
}
211210
}
212211
};
213-
self_site_packages.concatenate(site_packages_paths)
212+
match self_environment {
213+
// When ty is installed in a virtual environment (e.g., `uvx --with ...`),
214+
// the self-environment takes priority over the discovered environment.
215+
Some((self_site_packages, true)) => {
216+
self_site_packages.concatenate(site_packages_paths)
217+
}
218+
// When ty is installed in a system Python, do not include the system
219+
// Python's site-packages if there's a discovered project environment.
220+
Some((_, false)) | None => site_packages_paths,
221+
}
214222
} else {
215223
tracing::debug!("No virtual environment found");
216-
self_site_packages
224+
self_environment.map(|(paths, _)| paths).unwrap_or_default()
217225
};
218226

219227
let real_stdlib_path = python_environment.as_ref().and_then(|python_environment| {
@@ -518,10 +526,15 @@ impl Options {
518526
///
519527
/// Since ty may be executed from an arbitrary non-Python location, errors during discovery of ty's
520528
/// environment are not raised, instead [`None`] is returned.
529+
///
530+
/// Returns a tuple of (`site_packages`, `is_virtual_env`). When the self-environment is a virtual
531+
/// environment (e.g., `uvx --with ...`), it takes priority over other environments.
532+
/// When it's a system Python and there's a project environment (like `.venv`), the system
533+
/// Python's site-packages are excluded entirely.
521534
fn self_environment_search_paths(
522535
existing_origin: Option<SysPrefixPathOrigin>,
523536
system: &dyn System,
524-
) -> Option<SitePackagesPaths> {
537+
) -> Option<(SitePackagesPaths, bool)> {
525538
if existing_origin.is_some_and(|origin| !origin.allows_concatenation_with_self_environment()) {
526539
return None;
527540
}
@@ -535,15 +548,17 @@ fn self_environment_search_paths(
535548
.inspect_err(|err| tracing::debug!("Failed to discover ty's environment: {err}"))
536549
.ok()?;
537550

551+
let is_virtual_env = environment.is_virtual();
552+
538553
let search_paths = environment
539554
.site_packages_paths(system)
540555
.inspect_err(|err| {
541556
tracing::debug!("Failed to discover site-packages in ty's environment: {err}");
542557
})
543-
.ok();
558+
.ok()?;
544559

545560
tracing::debug!("Using site-packages from ty's environment");
546-
search_paths
561+
Some((search_paths, is_virtual_env))
547562
}
548563

549564
#[derive(

crates/ty_site_packages/src/lib.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,11 @@ impl PythonEnvironment {
276276
Self::System(env) => &env.root_path.origin,
277277
}
278278
}
279+
280+
/// Returns `true` if this is a virtual environment (has a `pyvenv.cfg` file).
281+
pub fn is_virtual(&self) -> bool {
282+
matches!(self, Self::Virtual(_))
283+
}
279284
}
280285

281286
/// Enumeration of the subdirectories of `sys.prefix` that could contain a
@@ -1709,14 +1714,8 @@ impl SysPrefixPathOrigin {
17091714
| Self::Editor
17101715
| Self::DerivedFromPyvenvCfg
17111716
| Self::CondaPrefixVar
1712-
| Self::PythonBinary => false,
1713-
// It's not strictly true that the self environment must be virtual, e.g., ty could be
1714-
// installed in a system Python environment and users may expect us to respect
1715-
// dependencies installed alongside it. However, we're intentionally excluding support
1716-
// for this to start. Note a change here has downstream implications, i.e., we probably
1717-
// don't want the packages in a system environment to take precedence over those in a
1718-
// virtual environment and would need to reverse the ordering in that case.
1719-
Self::SelfEnvironment => true,
1717+
| Self::PythonBinary
1718+
| Self::SelfEnvironment => false,
17201719
}
17211720
}
17221721

@@ -2189,6 +2188,20 @@ mod tests {
21892188
);
21902189
}
21912190

2191+
#[test]
2192+
fn can_find_site_packages_directory_no_virtual_env_at_origin_self_environment() {
2193+
// Test that ty can discover dependencies in a system Python environment
2194+
// that it's installed into (issue #2068).
2195+
let test = PythonEnvironmentTestCase {
2196+
system: TestSystem::default(),
2197+
minor_version: 13,
2198+
free_threaded: false,
2199+
origin: SysPrefixPathOrigin::SelfEnvironment,
2200+
virtual_env: None,
2201+
};
2202+
test.run();
2203+
}
2204+
21922205
#[test]
21932206
fn can_find_site_packages_directory_venv_style_version_field_in_pyvenv_cfg() {
21942207
// Shouldn't be converted to an mdtest because we want to assert

0 commit comments

Comments
 (0)