Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions crates/ty/tests/cli/python_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,231 @@ fn python_version_inferred_from_system_installation() -> anyhow::Result<()> {
Ok(())
}

/// This attempts to simulate the tangled web of symlinks that a homebrew install has
/// which can easily confuse us if we're ever told to use it.
///
/// The main thing this is regression-testing is a panic in one *extremely* specific case
/// that you have to try really hard to hit (but vscode, hilariously, did hit).
#[cfg(unix)]
#[test]
fn python_argument_trapped_in_a_symlink_factory() -> anyhow::Result<()> {
let case = CliTest::with_files([
// This is the real python binary.
(
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3.13",
"",
),
// There's a real site-packages here (although it's basically empty).
(
"opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages/foo.py",
"",
),
// There's also a real site-packages here (although it's basically empty).
("opt/homebrew/lib/python3.13/site-packages/bar.py", ""),
// This has the real stdlib, but the site-packages in this dir is a symlink.
(
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/abc.py",
"",
),
// It's important that this our faux-homebrew not be in the same dir as our working directory
// to reproduce the crash, don't ask me why.
(
"project/test.py",
"\
import foo
import bar
import colorama
",
),
])?;

// many python symlinks pointing to a single real python (the longest path)
case.write_symlink(
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3.13",
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3",
)?;
case.write_symlink(
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3",
"opt/homebrew/Cellar/python@3.13/3.13.5/bin/python3",
)?;
case.write_symlink(
"opt/homebrew/Cellar/python@3.13/3.13.5/bin/python3",
"opt/homebrew/bin/python3",
)?;
// the "real" python's site-packages is a symlink to a different dir
case.write_symlink(
"opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages",
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages",
)?;

// Try all 4 pythons with absolute paths to our fauxbrew install
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.arg("--python").arg(case.root().join("opt/homebrew/bin/python3")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `foo`
--> test.py:1:8
|
1 | import foo
| ^^^
2 | import bar
3 | import colorama
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/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

error[unresolved-import]: Cannot resolve imported module `colorama`
--> test.py:3:8
|
1 | import foo
2 | import bar
3 | import colorama
| ^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/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 2 diagnostics

----- stderr -----
");

assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.arg("--python").arg(case.root().join("opt/homebrew/Cellar/python@3.13/3.13.5/bin/python3")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `bar`
--> test.py:2:8
|
1 | import foo
2 | import bar
| ^^^
3 | import colorama
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/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

error[unresolved-import]: Cannot resolve imported module `colorama`
--> test.py:3:8
|
1 | import foo
2 | import bar
3 | import colorama
| ^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/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 2 diagnostics

----- stderr -----
");

assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.arg("--python").arg(case.root().join("opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `bar`
--> test.py:2:8
|
1 | import foo
2 | import bar
| ^^^
3 | import colorama
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/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

error[unresolved-import]: Cannot resolve imported module `colorama`
--> test.py:3:8
|
1 | import foo
2 | import bar
3 | import colorama
| ^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/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 2 diagnostics

----- stderr -----
");

assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.arg("--python").arg(case.root().join("opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3.13")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `bar`
--> test.py:2:8
|
1 | import foo
2 | import bar
| ^^^
3 | import colorama
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/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

error[unresolved-import]: Cannot resolve imported module `colorama`
--> test.py:3:8
|
1 | import foo
2 | import bar
3 | import colorama
| ^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/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 2 diagnostics

----- stderr -----
");

Ok(())
}

/// On Unix systems, it's common for a Python installation at `.venv/bin/python` to only be a symlink
/// to a system Python installation. We must be careful not to resolve the symlink too soon!
/// If we do, we will incorrectly add the system installation's `site-packages` as a search path,
Expand Down
9 changes: 3 additions & 6 deletions crates/ty_python_semantic/src/module_resolver/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,15 +452,12 @@ pub(crate) fn dynamic_resolution_paths<'db>(
let site_packages_dir = site_packages_search_path
.as_system_path()
.expect("Expected site package path to be a system path");
let site_packages_dir = system
.canonicalize_path(site_packages_dir)
.unwrap_or_else(|_| site_packages_dir.to_path_buf());

if !existing_paths.insert(Cow::Owned(site_packages_dir.clone())) {
if !existing_paths.insert(Cow::Borrowed(site_packages_dir)) {
continue;
}

let site_packages_root = files.expect_root(db, &site_packages_dir);
let site_packages_root = files.expect_root(db, site_packages_dir);

// This query needs to be re-executed each time a `.pth` file
// is added, modified or removed from the `site-packages` directory.
Expand All @@ -477,7 +474,7 @@ pub(crate) fn dynamic_resolution_paths<'db>(
// containing a (relative or absolute) path.
// Each of these paths may point to an editable install of a package,
// so should be considered an additional search path.
let pth_file_iterator = match PthFileIterator::new(db, &site_packages_dir) {
let pth_file_iterator = match PthFileIterator::new(db, site_packages_dir) {
Ok(iterator) => iterator,
Err(error) => {
tracing::warn!(
Expand Down
Loading