Skip to content

Commit 6c953b4

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 7da7ae0 commit 6c953b4

3 files changed

Lines changed: 128 additions & 26 deletions

File tree

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
@@ -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,22 @@ 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, the discovered environment
219+
// (e.g., `.venv`) takes priority over the self-environment.
220+
Some((self_site_packages, false)) => {
221+
site_packages_paths.concatenate(self_site_packages)
222+
}
223+
None => site_packages_paths,
224+
}
214225
} else {
215226
tracing::debug!("No virtual environment found");
216-
self_site_packages
227+
self_environment.map(|(paths, _)| paths).unwrap_or_default()
217228
};
218229

219230
let real_stdlib_path = python_environment.as_ref().and_then(|python_environment| {
@@ -518,10 +529,14 @@ impl Options {
518529
///
519530
/// Since ty may be executed from an arbitrary non-Python location, errors during discovery of ty's
520531
/// environment are not raised, instead [`None`] is returned.
532+
///
533+
/// Returns a tuple of (`site_packages`, `is_virtual_env`). When the self-environment is a virtual
534+
/// environment (e.g., `uvx --with ...`), it should take priority over other environments.
535+
/// When it's a system Python, other environments (like `.venv`) should take priority.
521536
fn self_environment_search_paths(
522537
existing_origin: Option<SysPrefixPathOrigin>,
523538
system: &dyn System,
524-
) -> Option<SitePackagesPaths> {
539+
) -> Option<(SitePackagesPaths, bool)> {
525540
if existing_origin.is_some_and(|origin| !origin.allows_concatenation_with_self_environment()) {
526541
return None;
527542
}
@@ -535,15 +550,17 @@ fn self_environment_search_paths(
535550
.inspect_err(|err| tracing::debug!("Failed to discover ty's environment: {err}"))
536551
.ok()?;
537552

553+
let is_virtual_env = environment.is_virtual();
554+
538555
let search_paths = environment
539556
.site_packages_paths(system)
540557
.inspect_err(|err| {
541558
tracing::debug!("Failed to discover site-packages in ty's environment: {err}");
542559
})
543-
.ok();
560+
.ok()?;
544561

545562
tracing::debug!("Using site-packages from ty's environment");
546-
search_paths
563+
Some((search_paths, is_virtual_env))
547564
}
548565

549566
#[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)