diff --git a/crates/ty_ide/src/all_symbols.rs b/crates/ty_ide/src/all_symbols.rs index c7282a5fc7..79767d36d1 100644 --- a/crates/ty_ide/src/all_symbols.rs +++ b/crates/ty_ide/src/all_symbols.rs @@ -36,6 +36,20 @@ pub fn all_symbols<'db>( let Some(file) = module.file(&*db) else { continue; }; + // By convention, modules starting with an underscore + // are generally considered unexported. However, we + // should consider first party modules fair game. + // + // Note that we apply this recursively. e.g., + // `numpy._core.multiarray` is considered private + // because it's a child of `_core`. + if module.name(&*db).components().any(|c| c.starts_with('_')) + && module + .search_path(&*db) + .is_none_or(|sp| !sp.is_first_party()) + { + continue; + } // TODO: also make it available in `TYPE_CHECKING` blocks // (we'd need https://github.com/astral-sh/ty/issues/1553 to do this well) if !is_typing_extensions_available && module.name(&*db) == &typing_extensions { diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index ba4c7e9e5b..d751611339 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -5979,6 +5979,94 @@ ZQ "); } + // This test confirms current behavior (as of 2025-12-04), but + // it's not consistent with auto-import. That is, it doesn't + // strictly respect `__all__` on `bar`, but perhaps it should. + // + // See: https://github.com/astral-sh/ty/issues/1757 + #[test] + fn object_attr_ignores_all() { + let snapshot = CursorTest::builder() + .source( + "main.py", + r#" +import bar +bar.ZQ +"#, + ) + .source( + "bar.py", + r#" + ZQZQ1 = 1 + ZQZQ2 = 1 + __all__ = ['ZQZQ1'] + "#, + ) + .completion_test_builder() + .auto_import() + .module_names() + .build() + .snapshot(); + // We specifically do not want `ZQZQ2` here, since + // it is not part of `__all__`. + assert_snapshot!(snapshot, @r" + ZQZQ1 :: + ZQZQ2 :: + "); + } + + #[test] + fn auto_import_ignores_modules_with_leading_underscore() { + let snapshot = CursorTest::builder() + .source( + "main.py", + r#" +Quitter +"#, + ) + .completion_test_builder() + .auto_import() + .module_names() + .build() + .snapshot(); + // There is a `Quitter` in `_sitebuiltins` in the standard + // library. But this is skipped by auto-import because it's + // 1) not first party and 2) starts with an `_`. + assert_snapshot!(snapshot, @""); + } + + #[test] + fn auto_import_includes_modules_with_leading_underscore_in_first_party() { + let snapshot = CursorTest::builder() + .source( + "main.py", + r#" +ZQ +"#, + ) + .source( + "bar.py", + r#" + ZQZQ1 = 1 + "#, + ) + .source( + "_foo.py", + r#" + ZQZQ1 = 1 + "#, + ) + .completion_test_builder() + .auto_import() + .module_names() + .build() + .snapshot(); + assert_snapshot!(snapshot, @r" + ZQZQ1 :: _foo + ZQZQ1 :: bar + "); + } + /// A way to create a simple single-file (named `main.py`) completion test /// builder. /// diff --git a/crates/ty_python_semantic/src/module_resolver/path.rs b/crates/ty_python_semantic/src/module_resolver/path.rs index 5200396dc1..638bbf819a 100644 --- a/crates/ty_python_semantic/src/module_resolver/path.rs +++ b/crates/ty_python_semantic/src/module_resolver/path.rs @@ -594,7 +594,7 @@ impl SearchPath { ) } - pub(crate) fn is_first_party(&self) -> bool { + pub fn is_first_party(&self) -> bool { matches!(&*self.0, SearchPathInner::FirstParty(_)) } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap index 2bdac4a17e..772f80a795 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap @@ -6,7 +6,7 @@ expression: completions { "label": "Literal (import typing)", "kind": 6, - "sortText": " 58", + "sortText": " 35", "insertText": "Literal", "additionalTextEdits": [ { @@ -27,7 +27,7 @@ expression: completions { "label": "LiteralString (import typing)", "kind": 6, - "sortText": " 59", + "sortText": " 36", "insertText": "LiteralString", "additionalTextEdits": [ { diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap index 2bdac4a17e..772f80a795 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap @@ -6,7 +6,7 @@ expression: completions { "label": "Literal (import typing)", "kind": 6, - "sortText": " 58", + "sortText": " 35", "insertText": "Literal", "additionalTextEdits": [ { @@ -27,7 +27,7 @@ expression: completions { "label": "LiteralString (import typing)", "kind": 6, - "sortText": " 59", + "sortText": " 36", "insertText": "LiteralString", "additionalTextEdits": [ { diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap index 2bdac4a17e..772f80a795 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap @@ -6,7 +6,7 @@ expression: completions { "label": "Literal (import typing)", "kind": 6, - "sortText": " 58", + "sortText": " 35", "insertText": "Literal", "additionalTextEdits": [ { @@ -27,7 +27,7 @@ expression: completions { "label": "LiteralString (import typing)", "kind": 6, - "sortText": " 59", + "sortText": " 36", "insertText": "LiteralString", "additionalTextEdits": [ { diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap index a0ff0b77b6..004f9f6823 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap @@ -6,7 +6,7 @@ expression: completions { "label": "Literal (import typing)", "kind": 6, - "sortText": " 58", + "sortText": " 35", "insertText": "Literal", "additionalTextEdits": [ { @@ -27,7 +27,7 @@ expression: completions { "label": "LiteralString (import typing)", "kind": 6, - "sortText": " 59", + "sortText": " 36", "insertText": "LiteralString", "additionalTextEdits": [ {