[ty] Make auto-import ignore symbols in modules starting with a `_`

This applies recursively. So if *any* component of a module name starts
with a `_`, then symbols from that module are excluded from auto-import.

The exception is when it's a module within first party code. Then we
want to include it in auto-import.
This commit is contained in:
Andrew Gallant 2025-12-03 14:59:21 -05:00 committed by Andrew Gallant
parent 2a38395bc8
commit 32f400a457
7 changed files with 111 additions and 9 deletions

View File

@ -36,6 +36,20 @@ pub fn all_symbols<'db>(
let Some(file) = module.file(&*db) else { let Some(file) = module.file(&*db) else {
continue; 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 // TODO: also make it available in `TYPE_CHECKING` blocks
// (we'd need https://github.com/astral-sh/ty/issues/1553 to do this well) // (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 { if !is_typing_extensions_available && module.name(&*db) == &typing_extensions {

View File

@ -5979,6 +5979,94 @@ ZQ<CURSOR>
"); ");
} }
// 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<CURSOR>
"#,
)
.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 :: <no import required>
ZQZQ2 :: <no import required>
");
}
#[test]
fn auto_import_ignores_modules_with_leading_underscore() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
Quitter<CURSOR>
"#,
)
.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, @"<No completions found>");
}
#[test]
fn auto_import_includes_modules_with_leading_underscore_in_first_party() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
ZQ<CURSOR>
"#,
)
.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 /// A way to create a simple single-file (named `main.py`) completion test
/// builder. /// builder.
/// ///

View File

@ -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(_)) matches!(&*self.0, SearchPathInner::FirstParty(_))
} }

View File

@ -6,7 +6,7 @@ expression: completions
{ {
"label": "Literal (import typing)", "label": "Literal (import typing)",
"kind": 6, "kind": 6,
"sortText": " 58", "sortText": " 35",
"insertText": "Literal", "insertText": "Literal",
"additionalTextEdits": [ "additionalTextEdits": [
{ {
@ -27,7 +27,7 @@ expression: completions
{ {
"label": "LiteralString (import typing)", "label": "LiteralString (import typing)",
"kind": 6, "kind": 6,
"sortText": " 59", "sortText": " 36",
"insertText": "LiteralString", "insertText": "LiteralString",
"additionalTextEdits": [ "additionalTextEdits": [
{ {

View File

@ -6,7 +6,7 @@ expression: completions
{ {
"label": "Literal (import typing)", "label": "Literal (import typing)",
"kind": 6, "kind": 6,
"sortText": " 58", "sortText": " 35",
"insertText": "Literal", "insertText": "Literal",
"additionalTextEdits": [ "additionalTextEdits": [
{ {
@ -27,7 +27,7 @@ expression: completions
{ {
"label": "LiteralString (import typing)", "label": "LiteralString (import typing)",
"kind": 6, "kind": 6,
"sortText": " 59", "sortText": " 36",
"insertText": "LiteralString", "insertText": "LiteralString",
"additionalTextEdits": [ "additionalTextEdits": [
{ {

View File

@ -6,7 +6,7 @@ expression: completions
{ {
"label": "Literal (import typing)", "label": "Literal (import typing)",
"kind": 6, "kind": 6,
"sortText": " 58", "sortText": " 35",
"insertText": "Literal", "insertText": "Literal",
"additionalTextEdits": [ "additionalTextEdits": [
{ {
@ -27,7 +27,7 @@ expression: completions
{ {
"label": "LiteralString (import typing)", "label": "LiteralString (import typing)",
"kind": 6, "kind": 6,
"sortText": " 59", "sortText": " 36",
"insertText": "LiteralString", "insertText": "LiteralString",
"additionalTextEdits": [ "additionalTextEdits": [
{ {

View File

@ -6,7 +6,7 @@ expression: completions
{ {
"label": "Literal (import typing)", "label": "Literal (import typing)",
"kind": 6, "kind": 6,
"sortText": " 58", "sortText": " 35",
"insertText": "Literal", "insertText": "Literal",
"additionalTextEdits": [ "additionalTextEdits": [
{ {
@ -27,7 +27,7 @@ expression: completions
{ {
"label": "LiteralString (import typing)", "label": "LiteralString (import typing)",
"kind": 6, "kind": 6,
"sortText": " 59", "sortText": " 36",
"insertText": "LiteralString", "insertText": "LiteralString",
"additionalTextEdits": [ "additionalTextEdits": [
{ {