Skip to content

Commit 673bf60

Browse files
committed
[ty] Improve diagnostics when a submodule is not available as an attribute on a module-literal type
1 parent 762c445 commit 673bf60

File tree

6 files changed

+164
-42
lines changed

6 files changed

+164
-42
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 & 36 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]
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]
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]
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]
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]
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]
212212
reveal_type(mypackage.imported.X) # revealed: Unknown
213213
```
214214

@@ -241,15 +241,15 @@ X: int = 42
241241
```py
242242
import mypackage
243243

244-
# error: "has no member `submodule`"
244+
# error: [unresolved-attribute]
245245
reveal_type(mypackage.submodule) # revealed: Unknown
246-
# error: "has no member `submodule`"
246+
# error: [unresolved-attribute]
247247
reveal_type(mypackage.submodule.nested) # revealed: Unknown
248-
# error: "has no member `submodule`"
248+
# error: [unresolved-attribute]
249249
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
250-
# error: "has no member `nested`"
250+
# error: [unresolved-attribute]
251251
reveal_type(mypackage.nested) # revealed: Unknown
252-
# error: "has no member `nested`"
252+
# error: [unresolved-attribute]
253253
reveal_type(mypackage.nested.X) # revealed: Unknown
254254
```
255255

@@ -281,9 +281,9 @@ import mypackage
281281

282282
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
283283
# TODO: this would be nice to support
284-
# error: "has no member `nested`"
284+
# error: [unresolved-attribute]
285285
reveal_type(mypackage.submodule.nested) # revealed: Unknown
286-
# error: "has no member `nested`"
286+
# error: [unresolved-attribute]
287287
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
288288
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
289289
reveal_type(mypackage.nested.X) # revealed: int
@@ -319,15 +319,15 @@ X: int = 42
319319
import mypackage
320320

321321
# TODO: this could work and would be nice to have
322-
# error: "has no member `submodule`"
322+
# error: [unresolved-attribute]
323323
reveal_type(mypackage.submodule) # revealed: Unknown
324-
# error: "has no member `submodule`"
324+
# error: [unresolved-attribute]
325325
reveal_type(mypackage.submodule.nested) # revealed: Unknown
326-
# error: "has no member `submodule`"
326+
# error: [unresolved-attribute]
327327
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
328-
# error: "has no member `nested`"
328+
# error: [unresolved-attribute]
329329
reveal_type(mypackage.nested) # revealed: Unknown
330-
# error: "has no member `nested`"
330+
# error: [unresolved-attribute]
331331
reveal_type(mypackage.nested.X) # revealed: Unknown
332332
```
333333

@@ -359,9 +359,9 @@ import mypackage
359359

360360
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
361361
# TODO: this would be nice to support
362-
# error: "has no member `nested`"
362+
# error: [unresolved-attribute]
363363
reveal_type(mypackage.submodule.nested) # revealed: Unknown
364-
# error: "has no member `nested`"
364+
# error: [unresolved-attribute]
365365
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
366366
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
367367
reveal_type(mypackage.nested.X) # revealed: int
@@ -396,11 +396,11 @@ X: int = 42
396396
```py
397397
import mypackage
398398

399-
# error: "has no member `submodule`"
399+
# error: [unresolved-attribute]
400400
reveal_type(mypackage.submodule) # revealed: Unknown
401-
# error: "has no member `submodule`"
401+
# error: [unresolved-attribute]
402402
reveal_type(mypackage.submodule.nested) # revealed: Unknown
403-
# error: "has no member `submodule`"
403+
# error: [unresolved-attribute]
404404
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
405405
```
406406

@@ -432,11 +432,11 @@ X: int = 42
432432
import mypackage
433433

434434
# TODO: this would be nice to support
435-
# error: "has no member `submodule`"
435+
# error: [unresolved-attribute]
436436
reveal_type(mypackage.submodule) # revealed: Unknown
437-
# error: "has no member `submodule`"
437+
# error: [unresolved-attribute]
438438
reveal_type(mypackage.submodule.nested) # revealed: Unknown
439-
# error: "has no member `submodule`"
439+
# error: [unresolved-attribute]
440440
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
441441
```
442442

@@ -463,9 +463,9 @@ X: int = 42
463463
```py
464464
import mypackage
465465

466-
# error: "has no member `imported`"
466+
# error: [unresolved-attribute]
467467
reveal_type(mypackage.imported.X) # revealed: Unknown
468-
# error: "has no member `imported_m`"
468+
# error: [unresolved-attribute]
469469
reveal_type(mypackage.imported_m.X) # revealed: Unknown
470470
```
471471

@@ -489,7 +489,7 @@ X: int = 42
489489
import mypackage
490490

491491
# TODO: this would be nice to support, as it works at runtime
492-
# error: "has no member `imported`"
492+
# error: [unresolved-attribute]
493493
reveal_type(mypackage.imported.X) # revealed: Unknown
494494
reveal_type(mypackage.imported_m.X) # revealed: int
495495
```
@@ -623,7 +623,7 @@ X: int = 42
623623
```py
624624
import mypackage
625625

626-
# error: "no member `imported`"
626+
# error: [unresolved-attribute]
627627
reveal_type(mypackage.imported.X) # revealed: Unknown
628628
```
629629

@@ -676,7 +676,7 @@ from mypackage import imported
676676
# TODO: this would be nice to support, but it's dangerous with available_submodule_attributes
677677
# for details, see: https://github.com/astral-sh/ty/issues/1488
678678
reveal_type(imported.X) # revealed: int
679-
# error: "has no member `imported`"
679+
# error: [unresolved-attribute]
680680
reveal_type(mypackage.imported.X) # revealed: Unknown
681681
```
682682

@@ -699,9 +699,10 @@ X: int = 42
699699
import mypackage
700700
from mypackage import imported
701701

702-
# TODO: this would be nice to support, as it works at runtime
703702
reveal_type(imported.X) # revealed: int
704-
# error: "has no member `imported`"
703+
704+
# TODO: this would be nice to support, as it works at runtime
705+
# error: [unresolved-attribute]
705706
reveal_type(mypackage.imported.X) # revealed: Unknown
706707
```
707708

@@ -737,9 +738,9 @@ import mypackage
737738
from mypackage import imported
738739

739740
reveal_type(imported.X) # revealed: int
740-
# error: "has no member `fails`"
741+
# error: [unresolved-attribute]
741742
reveal_type(imported.fails.Y) # revealed: Unknown
742-
# error: "has no member `fails`"
743+
# error: [unresolved-attribute]
743744
reveal_type(mypackage.fails.Y) # revealed: Unknown
744745
```
745746

@@ -772,7 +773,7 @@ from mypackage import imported
772773

773774
reveal_type(imported.X) # revealed: int
774775
reveal_type(imported.fails.Y) # revealed: int
775-
# error: "has no member `fails`"
776+
# error: [unresolved-attribute]
776777
reveal_type(mypackage.fails.Y) # revealed: Unknown
777778
```
778779

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/types/infer/builder.rs

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

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

0 commit comments

Comments
 (0)