Skip to content

Commit dce6885

Browse files
committed
[ty] Support finding dependencies in system Pythons that ty is installed into
Fixes an issue where ty couldn't resolve imports from packages installed in a system Python environment when ty itself was installed directly in that system Python (rather than in a virtual environment). Previously, `SysPrefixPathOrigin::SelfEnvironment` was treated as requiring a virtual environment (with `pyvenv.cfg`), which caused discovery to fail for system Python installations. This change allows ty to fall back to treating its own environment as a `SystemEnvironment` when no `pyvenv.cfg` is found. Additionally, this change implements correct priority ordering: - When ty is installed in a virtual environment (e.g., `uvx --with ...`), ty's venv takes priority over other discovered environments - When ty is installed in a system Python, discovered environments (like `.venv`) take priority over the system Python's site-packages Fixes astral-sh/ty#2068 https://claude.ai/code/session_01885t5j7zeT78vRZCtu8X9C
1 parent 77a1740 commit dce6885

File tree

3 files changed

+128
-26
lines changed

3 files changed

+128
-26
lines changed

crates/ty/tests/cli/python_environment.rs

Lines changed: 82 additions & 10 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
(
@@ -2211,20 +2211,92 @@ fn ty_environment_is_system_not_virtual() -> anyhow::Result<()> {
22112211
])?
22122212
.with_ty_at(ty_executable_path)?;
22132213

2214+
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 `.venv` should take priority over the system environment's site-packages.
2228+
/// This is the opposite of when ty is installed in a virtual environment (like `uvx --with ...`),
2229+
/// where ty's venv takes priority.
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+
let ty_conflicting_package = format!("{ty_system_site_packages}/shared_package/__init__.py");
2253+
let local_conflicting_package =
2254+
format!("{local_venv_site_packages}/shared_package/__init__.py");
2255+
2256+
let case = CliTest::with_files([
2257+
(ty_unique_package.as_str(), "class SystemEnvClass: ..."),
2258+
(local_unique_package.as_str(), "class LocalClass: ..."),
2259+
(ty_conflicting_package.as_str(), "class FromSystemEnv: ..."),
2260+
(
2261+
local_conflicting_package.as_str(),
2262+
"class FromLocalVenv: ...",
2263+
),
2264+
// Note: NO pyvenv.cfg for system-python - this is a system installation, not a venv
2265+
(
2266+
".venv/pyvenv.cfg",
2267+
r"
2268+
home = ./
2269+
version = 3.13
2270+
",
2271+
),
2272+
(
2273+
"test.py",
2274+
r"
2275+
# Should resolve from ty's system environment
2276+
from system_package import SystemEnvClass
2277+
# Should resolve from local .venv
2278+
from local_package import LocalClass
2279+
# Should resolve from .venv (takes precedence over system Python)
2280+
from shared_package import FromLocalVenv
2281+
# Should NOT resolve (shadowed by .venv version)
2282+
from shared_package import FromSystemEnv
2283+
",
2284+
),
2285+
])?
2286+
.with_ty_at(ty_executable_path)?;
2287+
22142288
assert_cmd_snapshot!(case.command(), @"
22152289
success: false
22162290
exit_code: 1
22172291
----- stdout -----
2218-
error[unresolved-import]: Cannot resolve imported module `system_package`
2219-
--> test.py:2:6
2292+
error[unresolved-import]: Module `shared_package` has no member `FromSystemEnv`
2293+
--> test.py:9:28
22202294
|
2221-
2 | from system_package import SystemClass
2222-
| ^^^^^^^^^^^^^^
2295+
7 | from shared_package import FromLocalVenv
2296+
8 | # Should NOT resolve (shadowed by .venv version)
2297+
9 | from shared_package import FromSystemEnv
2298+
| ^^^^^^^^^^^^^
22232299
|
2224-
info: Searched in the following paths during module resolution:
2225-
info: 1. <temp_dir>/ (first-party code)
2226-
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
2227-
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
22282300
info: rule `unresolved-import` is enabled by default
22292301
22302302
Found 1 diagnostic

crates/ty_project/src/metadata/options.rs

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

186-
let self_site_packages = self_environment_search_paths(
186+
let self_environment = self_environment_search_paths(
187187
python_environment
188188
.as_ref()
189189
.map(ty_python_semantic::PythonEnvironment::origin)
190190
.cloned(),
191191
system,
192-
)
193-
.unwrap_or_default();
192+
);
194193

195194
let site_packages_paths = if let Some(python_environment) = python_environment.as_ref() {
196195
let site_packages_paths = python_environment
@@ -209,10 +208,22 @@ impl Options {
209208
}
210209
}
211210
};
212-
self_site_packages.concatenate(site_packages_paths)
211+
match self_environment {
212+
// When ty is installed in a virtual environment (e.g., `uvx --with ...`),
213+
// the self-environment takes priority over the discovered environment.
214+
Some((self_site_packages, true)) => {
215+
self_site_packages.concatenate(site_packages_paths)
216+
}
217+
// When ty is installed in a system Python, the discovered environment
218+
// (e.g., `.venv`) takes priority over the self-environment.
219+
Some((self_site_packages, false)) => {
220+
site_packages_paths.concatenate(self_site_packages)
221+
}
222+
None => site_packages_paths,
223+
}
213224
} else {
214225
tracing::debug!("No virtual environment found");
215-
self_site_packages
226+
self_environment.map(|(paths, _)| paths).unwrap_or_default()
216227
};
217228

218229
let real_stdlib_path = python_environment.as_ref().and_then(|python_environment| {
@@ -521,10 +532,14 @@ impl Options {
521532
///
522533
/// Since ty may be executed from an arbitrary non-Python location, errors during discovery of ty's
523534
/// environment are not raised, instead [`None`] is returned.
535+
///
536+
/// Returns a tuple of (`site_packages`, `is_virtual_env`). When the self-environment is a virtual
537+
/// environment (e.g., `uvx --with ...`), it should take priority over other environments.
538+
/// When it's a system Python, other environments (like `.venv`) should take priority.
524539
fn self_environment_search_paths(
525540
existing_origin: Option<SysPrefixPathOrigin>,
526541
system: &dyn System,
527-
) -> Option<SitePackagesPaths> {
542+
) -> Option<(SitePackagesPaths, bool)> {
528543
if existing_origin.is_some_and(|origin| !origin.allows_concatenation_with_self_environment()) {
529544
return None;
530545
}
@@ -538,15 +553,17 @@ fn self_environment_search_paths(
538553
.inspect_err(|err| tracing::debug!("Failed to discover ty's environment: {err}"))
539554
.ok()?;
540555

556+
let is_virtual_env = environment.is_virtual();
557+
541558
let search_paths = environment
542559
.site_packages_paths(system)
543560
.inspect_err(|err| {
544561
tracing::debug!("Failed to discover site-packages in ty's environment: {err}");
545562
})
546-
.ok();
563+
.ok()?;
547564

548565
tracing::debug!("Using site-packages from ty's environment");
549-
search_paths
566+
Some((search_paths, is_virtual_env))
550567
}
551568

552569
#[derive(

crates/ty_site_packages/src/lib.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ impl PythonEnvironment {
270270
Self::System(env) => &env.root_path.origin,
271271
}
272272
}
273+
274+
/// Returns `true` if this is a virtual environment (has a `pyvenv.cfg` file).
275+
pub fn is_virtual(&self) -> bool {
276+
matches!(self, Self::Virtual(_))
277+
}
273278
}
274279

275280
/// Enumeration of the subdirectories of `sys.prefix` that could contain a
@@ -1618,14 +1623,8 @@ impl SysPrefixPathOrigin {
16181623
| Self::PythonCliFlag
16191624
| Self::Editor
16201625
| Self::DerivedFromPyvenvCfg
1621-
| Self::CondaPrefixVar => false,
1622-
// It's not strictly true that the self environment must be virtual, e.g., ty could be
1623-
// installed in a system Python environment and users may expect us to respect
1624-
// dependencies installed alongside it. However, we're intentionally excluding support
1625-
// for this to start. Note a change here has downstream implications, i.e., we probably
1626-
// don't want the packages in a system environment to take precedence over those in a
1627-
// virtual environment and would need to reverse the ordering in that case.
1628-
Self::SelfEnvironment => true,
1626+
| Self::CondaPrefixVar
1627+
| Self::SelfEnvironment => false,
16291628
}
16301629
}
16311630

@@ -2095,6 +2094,20 @@ mod tests {
20952094
);
20962095
}
20972096

2097+
#[test]
2098+
fn can_find_site_packages_directory_no_virtual_env_at_origin_self_environment() {
2099+
// Test that ty can discover dependencies in a system Python environment
2100+
// that it's installed into (issue #2068).
2101+
let test = PythonEnvironmentTestCase {
2102+
system: TestSystem::default(),
2103+
minor_version: 13,
2104+
free_threaded: false,
2105+
origin: SysPrefixPathOrigin::SelfEnvironment,
2106+
virtual_env: None,
2107+
};
2108+
test.run();
2109+
}
2110+
20982111
#[test]
20992112
fn can_find_site_packages_directory_venv_style_version_field_in_pyvenv_cfg() {
21002113
// Shouldn't be converted to an mdtest because we want to assert

0 commit comments

Comments
 (0)