Skip to content

Commit 6a1e91c

Browse files
authored
[ty] Check typeshed VERSIONS for parent modules when reporting failed stdlib imports (#20908)
This is a drive-by improvement that I stumbled backwards into while looking into * astral-sh/ty#296 I was writing some simple tests for "thing not in old version of stdlib" diagnostics and checked what was added in 3.14, and saw `compression.zstd` and to my surprise discovered that `import compression.zstd` and `from compression import zstd` had completely different quality diagnostics. This is because `compression` and `compression.zstd` were *both* introduced in 3.14, and so per VERSIONS policy only an entry for `compression` was added, and so we don't actually have any definite info on `compression.zstd` and give up on producing a diagnostic. However the `from compression import zstd` form fails on looking up `compression` and we *do* have an exact match for that, so it gets a better diagnostic! (aside: I have now learned about the VERSIONS format and I *really* wish they would just enumerate all the submodules but, oh well!) The fix is, when handling an import failure, if we fail to find an exact match *we requery with the parent module*. In cases like `compression.zstd` this lets us at least identify that, hey, not even `compression` exists, and luckily that fixes the whole issue. In cases where the parent module and submodule were introduced at different times then we may discover that the parent module is in-range and that's fine, we don't produce the richer stdlib diagnostic.
1 parent 3db5d59 commit 6a1e91c

File tree

3 files changed

+123
-15
lines changed

3 files changed

+123
-15
lines changed

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,26 @@ from string.templatelib import Template # error: [unresolved-import]
192192
from importlib.resources import abc # error: [unresolved-import]
193193
```
194194

195+
## Attempting to import a stdlib submodule when both parts haven't yet been added
196+
197+
`compression` and `compression.zstd` were both added in 3.14 so there is a typeshed `VERSIONS` entry
198+
for `compression` but not `compression.zstd`. We can't be confident `compression.zstd` exists but we
199+
do know `compression` does and can still give good diagnostics about it.
200+
201+
<!-- snapshot-diagnostics -->
202+
203+
```toml
204+
[environment]
205+
python-version = "3.10"
206+
```
207+
208+
```py
209+
import compression.zstd # error: [unresolved-import]
210+
from compression import zstd # error: [unresolved-import]
211+
import compression.fakebutwhocansay # error: [unresolved-import]
212+
from compression import fakebutwhocansay # error: [unresolved-import]
213+
```
214+
195215
## Attempting to import a stdlib module that was previously removed
196216

197217
<!-- snapshot-diagnostics -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: basic.md - Structures - Attempting to import a stdlib submodule when both parts haven't yet been added
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | import compression.zstd # error: [unresolved-import]
16+
2 | from compression import zstd # error: [unresolved-import]
17+
3 | import compression.fakebutwhocansay # error: [unresolved-import]
18+
4 | from compression import fakebutwhocansay # error: [unresolved-import]
19+
```
20+
21+
# Diagnostics
22+
23+
```
24+
error[unresolved-import]: Cannot resolve imported module `compression.zstd`
25+
--> src/mdtest_snippet.py:1:8
26+
|
27+
1 | import compression.zstd # error: [unresolved-import]
28+
| ^^^^^^^^^^^^^^^^
29+
2 | from compression import zstd # error: [unresolved-import]
30+
3 | import compression.fakebutwhocansay # error: [unresolved-import]
31+
|
32+
info: The stdlib module `compression` is only available on Python 3.14+
33+
info: Python 3.10 was assumed when resolving modules because it was specified on the command line
34+
info: rule `unresolved-import` is enabled by default
35+
36+
```
37+
38+
```
39+
error[unresolved-import]: Cannot resolve imported module `compression`
40+
--> src/mdtest_snippet.py:2:6
41+
|
42+
1 | import compression.zstd # error: [unresolved-import]
43+
2 | from compression import zstd # error: [unresolved-import]
44+
| ^^^^^^^^^^^
45+
3 | import compression.fakebutwhocansay # error: [unresolved-import]
46+
4 | from compression import fakebutwhocansay # error: [unresolved-import]
47+
|
48+
info: The stdlib module `compression` is only available on Python 3.14+
49+
info: Python 3.10 was assumed when resolving modules because it was specified on the command line
50+
info: rule `unresolved-import` is enabled by default
51+
52+
```
53+
54+
```
55+
error[unresolved-import]: Cannot resolve imported module `compression.fakebutwhocansay`
56+
--> src/mdtest_snippet.py:3:8
57+
|
58+
1 | import compression.zstd # error: [unresolved-import]
59+
2 | from compression import zstd # error: [unresolved-import]
60+
3 | import compression.fakebutwhocansay # error: [unresolved-import]
61+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62+
4 | from compression import fakebutwhocansay # error: [unresolved-import]
63+
|
64+
info: The stdlib module `compression` is only available on Python 3.14+
65+
info: Python 3.10 was assumed when resolving modules because it was specified on the command line
66+
info: rule `unresolved-import` is enabled by default
67+
68+
```
69+
70+
```
71+
error[unresolved-import]: Cannot resolve imported module `compression`
72+
--> src/mdtest_snippet.py:4:6
73+
|
74+
2 | from compression import zstd # error: [unresolved-import]
75+
3 | import compression.fakebutwhocansay # error: [unresolved-import]
76+
4 | from compression import fakebutwhocansay # error: [unresolved-import]
77+
| ^^^^^^^^^^^
78+
|
79+
info: The stdlib module `compression` is only available on Python 3.14+
80+
info: Python 3.10 was assumed when resolving modules because it was specified on the command line
81+
info: rule `unresolved-import` is enabled by default
82+
83+
```

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

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4790,21 +4790,26 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
47904790
let program = Program::get(self.db());
47914791
let typeshed_versions = program.search_paths(self.db()).typeshed_versions();
47924792

4793-
if let Some(version_range) = typeshed_versions.exact(&module_name) {
4794-
// We know it is a stdlib module on *some* Python versions...
4795-
let python_version = program.python_version(self.db());
4796-
if !version_range.contains(python_version) {
4797-
// ...But not on *this* Python version.
4798-
diagnostic.info(format_args!(
4799-
"The stdlib module `{module_name}` is only available on Python {version_range}",
4800-
version_range = version_range.diagnostic_display(),
4801-
));
4802-
add_inferred_python_version_hint_to_diagnostic(
4803-
self.db(),
4804-
&mut diagnostic,
4805-
"resolving modules",
4806-
);
4807-
return;
4793+
// Loop over ancestors in case we have info on the parent module but not submodule
4794+
for module_name in module_name.ancestors() {
4795+
if let Some(version_range) = typeshed_versions.exact(&module_name) {
4796+
// We know it is a stdlib module on *some* Python versions...
4797+
let python_version = program.python_version(self.db());
4798+
if !version_range.contains(python_version) {
4799+
// ...But not on *this* Python version.
4800+
diagnostic.info(format_args!(
4801+
"The stdlib module `{module_name}` is only available on Python {version_range}",
4802+
version_range = version_range.diagnostic_display(),
4803+
));
4804+
add_inferred_python_version_hint_to_diagnostic(
4805+
self.db(),
4806+
&mut diagnostic,
4807+
"resolving modules",
4808+
);
4809+
return;
4810+
}
4811+
// We found the most precise answer we could, stop searching
4812+
break;
48084813
}
48094814
}
48104815
}

0 commit comments

Comments
 (0)