[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

* https://github.com/astral-sh/ty/issues/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.
This commit is contained in:
Aria Desires 2025-10-16 09:25:08 -04:00 committed by GitHub
parent 3db5d5906e
commit 6a1e91ce97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 123 additions and 15 deletions

View file

@ -4790,21 +4790,26 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let program = Program::get(self.db());
let typeshed_versions = program.search_paths(self.db()).typeshed_versions();
if let Some(version_range) = typeshed_versions.exact(&module_name) {
// We know it is a stdlib module on *some* Python versions...
let python_version = program.python_version(self.db());
if !version_range.contains(python_version) {
// ...But not on *this* Python version.
diagnostic.info(format_args!(
"The stdlib module `{module_name}` is only available on Python {version_range}",
version_range = version_range.diagnostic_display(),
));
add_inferred_python_version_hint_to_diagnostic(
self.db(),
&mut diagnostic,
"resolving modules",
);
return;
// Loop over ancestors in case we have info on the parent module but not submodule
for module_name in module_name.ancestors() {
if let Some(version_range) = typeshed_versions.exact(&module_name) {
// We know it is a stdlib module on *some* Python versions...
let python_version = program.python_version(self.db());
if !version_range.contains(python_version) {
// ...But not on *this* Python version.
diagnostic.info(format_args!(
"The stdlib module `{module_name}` is only available on Python {version_range}",
version_range = version_range.diagnostic_display(),
));
add_inferred_python_version_hint_to_diagnostic(
self.db(),
&mut diagnostic,
"resolving modules",
);
return;
}
// We found the most precise answer we could, stop searching
break;
}
}
}