Skip to content

Commit 3410041

Browse files
authored
[ty] Improve diagnostics when a submodule is not available as an attribute on a module-literal type (#21561)
1 parent f2ce5e5 commit 3410041

File tree

7 files changed

+166
-41
lines changed

7 files changed

+166
-41
lines changed

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2683,6 +2683,39 @@ reveal_type(datetime.UTC) # revealed: Unknown
26832683
reveal_type(datetime.fakenotreal) # revealed: Unknown
26842684
```
26852685

2686+
## Unimported submodule incorrectly accessed as attribute
2687+
2688+
We give special diagnostics for this common case too:
2689+
2690+
<!-- snapshot-diagnostics -->
2691+
2692+
`foo/__init__.py`:
2693+
2694+
```py
2695+
```
2696+
2697+
`foo/bar.py`:
2698+
2699+
```py
2700+
```
2701+
2702+
`baz/bar.py`:
2703+
2704+
```py
2705+
```
2706+
2707+
`main.py`:
2708+
2709+
```py
2710+
import foo
2711+
import baz
2712+
2713+
# error: [unresolved-attribute]
2714+
reveal_type(foo.bar) # revealed: Unknown
2715+
# error: [unresolved-attribute]
2716+
reveal_type(baz.bar) # revealed: Unknown
2717+
```
2718+
26862719
## References
26872720

26882721
Some of the tests in the *Class and instance variables* section draw inspiration from

crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Y: int = 47
6060
import mypackage
6161

6262
reveal_type(mypackage.imported.X) # revealed: int
63-
# error: "has no member `fails`"
63+
# error: [unresolved-attribute] "Submodule `fails` may not be available"
6464
reveal_type(mypackage.fails.Y) # revealed: Unknown
6565
```
6666

@@ -90,7 +90,7 @@ Y: int = 47
9090
import mypackage
9191

9292
reveal_type(mypackage.imported.X) # revealed: int
93-
# error: "has no member `fails`"
93+
# error: [unresolved-attribute] "Submodule `fails` may not be available"
9494
reveal_type(mypackage.fails.Y) # revealed: Unknown
9595
```
9696

@@ -125,7 +125,7 @@ Y: int = 47
125125
import mypackage
126126

127127
reveal_type(mypackage.imported.X) # revealed: int
128-
# error: "has no member `fails`"
128+
# error: [unresolved-attribute] "Submodule `fails` may not be available"
129129
reveal_type(mypackage.fails.Y) # revealed: Unknown
130130
```
131131

@@ -155,7 +155,7 @@ Y: int = 47
155155
import mypackage
156156

157157
reveal_type(mypackage.imported.X) # revealed: int
158-
# error: "has no member `fails`"
158+
# error: [unresolved-attribute] "Submodule `fails` may not be available"
159159
reveal_type(mypackage.fails.Y) # revealed: Unknown
160160
```
161161

@@ -184,7 +184,7 @@ X: int = 42
184184
import mypackage
185185

186186
# TODO: this could work and would be nice to have?
187-
# error: "has no member `imported`"
187+
# error: [unresolved-attribute] "Submodule `imported` may not be available"
188188
reveal_type(mypackage.imported.X) # revealed: Unknown
189189
```
190190

@@ -208,7 +208,7 @@ X: int = 42
208208
import mypackage
209209

210210
# TODO: this could work and would be nice to have
211-
# error: "has no member `imported`"
211+
# error: [unresolved-attribute] "Submodule `imported` may not be available"
212212
reveal_type(mypackage.imported.X) # revealed: Unknown
213213
```
214214

@@ -242,13 +242,13 @@ X: int = 42
242242
import mypackage
243243

244244
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
245-
# error: "has no member `nested`"
245+
# error: [unresolved-attribute] "Submodule `nested` may not be available"
246246
reveal_type(mypackage.submodule.nested) # revealed: Unknown
247-
# error: "has no member `nested`"
247+
# error: [unresolved-attribute] "Submodule `nested` may not be available"
248248
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
249-
# error: "has no member `nested`"
249+
# error: [unresolved-attribute] "has no member `nested`"
250250
reveal_type(mypackage.nested) # revealed: Unknown
251-
# error: "has no member `nested`"
251+
# error: [unresolved-attribute] "has no member `nested`"
252252
reveal_type(mypackage.nested.X) # revealed: Unknown
253253
```
254254

@@ -280,9 +280,9 @@ import mypackage
280280

281281
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
282282
# TODO: this would be nice to support
283-
# error: "has no member `nested`"
283+
# error: [unresolved-attribute] "Submodule `nested` may not be available"
284284
reveal_type(mypackage.submodule.nested) # revealed: Unknown
285-
# error: "has no member `nested`"
285+
# error: [unresolved-attribute] "Submodule `nested` may not be available"
286286
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
287287
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
288288
reveal_type(mypackage.nested.X) # revealed: int
@@ -318,13 +318,13 @@ X: int = 42
318318
import mypackage
319319

320320
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
321-
# error: "has no member `nested`"
321+
# error: [unresolved-attribute] "Submodule `nested` may not be available"
322322
reveal_type(mypackage.submodule.nested) # revealed: Unknown
323-
# error: "has no member `nested`"
323+
# error: [unresolved-attribute] "Submodule `nested` may not be available"
324324
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
325-
# error: "has no member `nested`"
325+
# error: [unresolved-attribute] "has no member `nested`"
326326
reveal_type(mypackage.nested) # revealed: Unknown
327-
# error: "has no member `nested`"
327+
# error: [unresolved-attribute] "has no member `nested`"
328328
reveal_type(mypackage.nested.X) # revealed: Unknown
329329
```
330330

@@ -356,9 +356,9 @@ import mypackage
356356

357357
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
358358
# TODO: this would be nice to support
359-
# error: "has no member `nested`"
359+
# error: [unresolved-attribute] "Submodule `nested` may not be available"
360360
reveal_type(mypackage.submodule.nested) # revealed: Unknown
361-
# error: "has no member `nested`"
361+
# error: [unresolved-attribute] "Submodule `nested` may not be available"
362362
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
363363
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
364364
reveal_type(mypackage.nested.X) # revealed: int
@@ -393,11 +393,11 @@ X: int = 42
393393
```py
394394
import mypackage
395395

396-
# error: "has no member `submodule`"
396+
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
397397
reveal_type(mypackage.submodule) # revealed: Unknown
398-
# error: "has no member `submodule`"
398+
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
399399
reveal_type(mypackage.submodule.nested) # revealed: Unknown
400-
# error: "has no member `submodule`"
400+
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
401401
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
402402
```
403403

@@ -429,11 +429,11 @@ X: int = 42
429429
import mypackage
430430

431431
# TODO: this would be nice to support
432-
# error: "has no member `submodule`"
432+
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
433433
reveal_type(mypackage.submodule) # revealed: Unknown
434-
# error: "has no member `submodule`"
434+
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
435435
reveal_type(mypackage.submodule.nested) # revealed: Unknown
436-
# error: "has no member `submodule`"
436+
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
437437
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
438438
```
439439

@@ -460,9 +460,9 @@ X: int = 42
460460
```py
461461
import mypackage
462462

463-
# error: "has no member `imported`"
463+
# error: [unresolved-attribute] "Submodule `imported` may not be available"
464464
reveal_type(mypackage.imported.X) # revealed: Unknown
465-
# error: "has no member `imported_m`"
465+
# error: [unresolved-attribute] "has no member `imported_m`"
466466
reveal_type(mypackage.imported_m.X) # revealed: Unknown
467467
```
468468

@@ -486,7 +486,7 @@ X: int = 42
486486
import mypackage
487487

488488
# TODO: this would be nice to support, as it works at runtime
489-
# error: "has no member `imported`"
489+
# error: [unresolved-attribute] "Submodule `imported` may not be available"
490490
reveal_type(mypackage.imported.X) # revealed: Unknown
491491
reveal_type(mypackage.imported_m.X) # revealed: int
492492
```
@@ -566,7 +566,7 @@ X: int = 42
566566
from mypackage import *
567567

568568
# TODO: this would be nice to support
569-
# error: "`imported` used when not defined"
569+
# error: [unresolved-reference] "`imported` used when not defined"
570570
reveal_type(imported.X) # revealed: Unknown
571571
reveal_type(Z) # revealed: int
572572
```
@@ -669,10 +669,11 @@ X: int = 42
669669
import mypackage
670670
from mypackage import imported
671671

672+
reveal_type(imported.X) # revealed: int
673+
672674
# TODO: this would be nice to support, but it's dangerous with available_submodule_attributes
673675
# for details, see: https://github.com/astral-sh/ty/issues/1488
674-
reveal_type(imported.X) # revealed: int
675-
# error: "has no member `imported`"
676+
# error: [unresolved-attribute] "Submodule `imported` may not be available"
676677
reveal_type(mypackage.imported.X) # revealed: Unknown
677678
```
678679

@@ -695,9 +696,10 @@ X: int = 42
695696
import mypackage
696697
from mypackage import imported
697698

698-
# TODO: this would be nice to support, as it works at runtime
699699
reveal_type(imported.X) # revealed: int
700-
# error: "has no member `imported`"
700+
701+
# TODO: this would be nice to support, as it works at runtime
702+
# error: [unresolved-attribute] "Submodule `imported` may not be available"
701703
reveal_type(mypackage.imported.X) # revealed: Unknown
702704
```
703705

@@ -733,9 +735,9 @@ import mypackage
733735
from mypackage import imported
734736

735737
reveal_type(imported.X) # revealed: int
736-
# error: "has no member `fails`"
738+
# error: [unresolved-attribute] "has no member `fails`"
737739
reveal_type(imported.fails.Y) # revealed: Unknown
738-
# error: "has no member `fails`"
740+
# error: [unresolved-attribute] "Submodule `fails` may not be available"
739741
reveal_type(mypackage.fails.Y) # revealed: Unknown
740742
```
741743

@@ -768,7 +770,7 @@ from mypackage import imported
768770

769771
reveal_type(imported.X) # revealed: int
770772
reveal_type(imported.fails.Y) # revealed: int
771-
# error: "has no member `fails`"
773+
# error: [unresolved-attribute] "Submodule `fails`"
772774
reveal_type(mypackage.fails.Y) # revealed: Unknown
773775
```
774776

crates/ty_python_semantic/resources/mdtest/import/relative.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ X: int = 42
247247
from . import foo
248248
import package
249249

250-
# error: [unresolved-attribute] "Module `package` has no member `foo`"
250+
# error: [unresolved-attribute]
251251
reveal_type(package.foo.X) # revealed: Unknown
252252
```
253253

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: attributes.md - Attributes - Unimported submodule incorrectly accessed as attribute
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
8+
---
9+
10+
# Python source files
11+
12+
## foo/__init__.py
13+
14+
```
15+
```
16+
17+
## foo/bar.py
18+
19+
```
20+
```
21+
22+
## baz/bar.py
23+
24+
```
25+
```
26+
27+
## main.py
28+
29+
```
30+
1 | import foo
31+
2 | import baz
32+
3 |
33+
4 | # error: [unresolved-attribute]
34+
5 | reveal_type(foo.bar) # revealed: Unknown
35+
6 | # error: [unresolved-attribute]
36+
7 | reveal_type(baz.bar) # revealed: Unknown
37+
```
38+
39+
# Diagnostics
40+
41+
```
42+
error[unresolved-attribute]: Submodule `bar` may not be available as an attribute on module `foo`
43+
--> src/main.py:5:13
44+
|
45+
4 | # error: [unresolved-attribute]
46+
5 | reveal_type(foo.bar) # revealed: Unknown
47+
| ^^^^^^^
48+
6 | # error: [unresolved-attribute]
49+
7 | reveal_type(baz.bar) # revealed: Unknown
50+
|
51+
help: Consider explicitly importing `foo.bar`
52+
info: rule `unresolved-attribute` is enabled by default
53+
54+
```
55+
56+
```
57+
error[unresolved-attribute]: Submodule `bar` may not be available as an attribute on module `baz`
58+
--> src/main.py:7:13
59+
|
60+
5 | reveal_type(foo.bar) # revealed: Unknown
61+
6 | # error: [unresolved-attribute]
62+
7 | reveal_type(baz.bar) # revealed: Unknown
63+
| ^^^^^^^
64+
|
65+
help: Consider explicitly importing `baz.bar`
66+
info: rule `unresolved-attribute` is enabled by default
67+
68+
```

crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ import imported
628628
from module2 import imported as other_imported
629629
from ty_extensions import TypeOf, static_assert, is_equivalent_to
630630

631-
# error: [unresolved-attribute] "Module `imported` has no member `abc`"
631+
# error: [unresolved-attribute]
632632
reveal_type(imported.abc) # revealed: Unknown
633633

634634
reveal_type(other_imported.abc) # revealed: <module 'imported.abc'>

crates/ty_python_semantic/src/semantic_index/definition.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,10 +364,12 @@ pub(crate) struct ImportFromDefinitionNodeRef<'ast> {
364364
pub(crate) alias_index: usize,
365365
pub(crate) is_reexported: bool,
366366
}
367+
367368
#[derive(Copy, Clone, Debug)]
368369
pub(crate) struct ImportFromSubmoduleDefinitionNodeRef<'ast> {
369370
pub(crate) node: &'ast ast::StmtImportFrom,
370371
}
372+
371373
#[derive(Copy, Clone, Debug)]
372374
pub(crate) struct AssignmentDefinitionNodeRef<'ast, 'db> {
373375
pub(crate) unpack: Option<(UnpackPosition, Unpack<'db>)>,

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9082,10 +9082,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
90829082
}
90839083

90849084
let diagnostic = match value_type {
9085-
Type::ModuleLiteral(module) => builder.into_diagnostic(format_args!(
9086-
"Module `{}` has no member `{attr_name}`",
9087-
module.module(db).name(db),
9088-
)),
9085+
Type::ModuleLiteral(module) => {
9086+
let module = module.module(db);
9087+
let module_name = module.name(db);
9088+
if module.kind(db).is_package()
9089+
&& let Some(relative_submodule) = ModuleName::new(attr_name)
9090+
{
9091+
let mut maybe_submodule_name = module_name.clone();
9092+
maybe_submodule_name.extend(&relative_submodule);
9093+
if resolve_module(db, &maybe_submodule_name).is_some() {
9094+
let mut diag = builder.into_diagnostic(format_args!(
9095+
"Submodule `{attr_name}` may not be available as an attribute \
9096+
on module `{module_name}`"
9097+
));
9098+
diag.help(format_args!(
9099+
"Consider explicitly importing `{maybe_submodule_name}`"
9100+
));
9101+
return fallback();
9102+
}
9103+
}
9104+
9105+
builder.into_diagnostic(format_args!(
9106+
"Module `{module_name}` has no member `{attr_name}`",
9107+
))
9108+
}
90899109
Type::ClassLiteral(class) => builder.into_diagnostic(format_args!(
90909110
"Class `{}` has no attribute `{attr_name}`",
90919111
class.name(db),

0 commit comments

Comments
 (0)