diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs index de6d99aa9a..638fc6c4ca 100644 --- a/crates/ty/tests/cli/python_environment.rs +++ b/crates/ty/tests/cli/python_environment.rs @@ -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. /project (first-party code) + info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: 3. /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. /project (first-party code) + info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: 3. /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. /project (first-party code) + info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: 3. /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. /project (first-party code) + info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: 3. /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. /project (first-party code) + info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: 3. /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. /project (first-party code) + info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: 3. /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. /project (first-party code) + info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: 3. /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. /project (first-party code) + info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: 3. /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, diff --git a/crates/ty_python_semantic/src/module_resolver/resolver.rs b/crates/ty_python_semantic/src/module_resolver/resolver.rs index 0787859049..349d685862 100644 --- a/crates/ty_python_semantic/src/module_resolver/resolver.rs +++ b/crates/ty_python_semantic/src/module_resolver/resolver.rs @@ -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. @@ -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!(