From 6a1e91ce9767a2752fbda77eeacc0516dbf7b087 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Thu, 16 Oct 2025 09:25:08 -0400 Subject: [PATCH] [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. --- .../resources/mdtest/import/basic.md | 20 +++++ ...empting_to_import…_(dba22bd97137ee38).snap | 83 +++++++++++++++++++ .../src/types/infer/builder.rs | 35 ++++---- 3 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import…_(dba22bd97137ee38).snap diff --git a/crates/ty_python_semantic/resources/mdtest/import/basic.md b/crates/ty_python_semantic/resources/mdtest/import/basic.md index 45abcf5018..1220ddc770 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/import/basic.md @@ -192,6 +192,26 @@ from string.templatelib import Template # error: [unresolved-import] from importlib.resources import abc # error: [unresolved-import] ``` +## Attempting to import a stdlib submodule when both parts haven't yet been added + +`compression` and `compression.zstd` were both added in 3.14 so there is a typeshed `VERSIONS` entry +for `compression` but not `compression.zstd`. We can't be confident `compression.zstd` exists but we +do know `compression` does and can still give good diagnostics about it. + + + +```toml +[environment] +python-version = "3.10" +``` + +```py +import compression.zstd # error: [unresolved-import] +from compression import zstd # error: [unresolved-import] +import compression.fakebutwhocansay # error: [unresolved-import] +from compression import fakebutwhocansay # error: [unresolved-import] +``` + ## Attempting to import a stdlib module that was previously removed diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import…_(dba22bd97137ee38).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import…_(dba22bd97137ee38).snap new file mode 100644 index 0000000000..cc712bb06e --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import…_(dba22bd97137ee38).snap @@ -0,0 +1,83 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: basic.md - Structures - Attempting to import a stdlib submodule when both parts haven't yet been added +mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | import compression.zstd # error: [unresolved-import] +2 | from compression import zstd # error: [unresolved-import] +3 | import compression.fakebutwhocansay # error: [unresolved-import] +4 | from compression import fakebutwhocansay # error: [unresolved-import] +``` + +# Diagnostics + +``` +error[unresolved-import]: Cannot resolve imported module `compression.zstd` + --> src/mdtest_snippet.py:1:8 + | +1 | import compression.zstd # error: [unresolved-import] + | ^^^^^^^^^^^^^^^^ +2 | from compression import zstd # error: [unresolved-import] +3 | import compression.fakebutwhocansay # error: [unresolved-import] + | +info: The stdlib module `compression` is only available on Python 3.14+ +info: Python 3.10 was assumed when resolving modules because it was specified on the command line +info: rule `unresolved-import` is enabled by default + +``` + +``` +error[unresolved-import]: Cannot resolve imported module `compression` + --> src/mdtest_snippet.py:2:6 + | +1 | import compression.zstd # error: [unresolved-import] +2 | from compression import zstd # error: [unresolved-import] + | ^^^^^^^^^^^ +3 | import compression.fakebutwhocansay # error: [unresolved-import] +4 | from compression import fakebutwhocansay # error: [unresolved-import] + | +info: The stdlib module `compression` is only available on Python 3.14+ +info: Python 3.10 was assumed when resolving modules because it was specified on the command line +info: rule `unresolved-import` is enabled by default + +``` + +``` +error[unresolved-import]: Cannot resolve imported module `compression.fakebutwhocansay` + --> src/mdtest_snippet.py:3:8 + | +1 | import compression.zstd # error: [unresolved-import] +2 | from compression import zstd # error: [unresolved-import] +3 | import compression.fakebutwhocansay # error: [unresolved-import] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4 | from compression import fakebutwhocansay # error: [unresolved-import] + | +info: The stdlib module `compression` is only available on Python 3.14+ +info: Python 3.10 was assumed when resolving modules because it was specified on the command line +info: rule `unresolved-import` is enabled by default + +``` + +``` +error[unresolved-import]: Cannot resolve imported module `compression` + --> src/mdtest_snippet.py:4:6 + | +2 | from compression import zstd # error: [unresolved-import] +3 | import compression.fakebutwhocansay # error: [unresolved-import] +4 | from compression import fakebutwhocansay # error: [unresolved-import] + | ^^^^^^^^^^^ + | +info: The stdlib module `compression` is only available on Python 3.14+ +info: Python 3.10 was assumed when resolving modules because it was specified on the command line +info: rule `unresolved-import` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 7155e1f7a1..4b2c7cdc44 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -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; } } }