Skip to content

Commit 733d338

Browse files
committed
do from imports too
1 parent 5c801e7 commit 733d338

File tree

5 files changed

+121
-30
lines changed

5 files changed

+121
-30
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ python-version = "3.10"
189189
```py
190190
import tomllib # error: [unresolved-import]
191191
from string.templatelib import Template # error: [unresolved-import]
192+
from importlib.resources import abc # error: [unresolved-import]
192193
```
193194

194195
## Attempting to import a stdlib module that was previously removed

crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import…_(2fcfcf567587a056).snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md
1414
```
1515
1 | import tomllib # error: [unresolved-import]
1616
2 | from string.templatelib import Template # error: [unresolved-import]
17+
3 | from importlib.resources import abc # error: [unresolved-import]
1718
```
1819

1920
# Diagnostics
@@ -25,6 +26,7 @@ error[unresolved-import]: Cannot resolve imported module `tomllib`
2526
1 | import tomllib # error: [unresolved-import]
2627
| ^^^^^^^
2728
2 | from string.templatelib import Template # error: [unresolved-import]
29+
3 | from importlib.resources import abc # error: [unresolved-import]
2830
|
2931
info: The stdlib module `tomllib` is only available on Python 3.11+
3032
info: Python 3.10 was assumed when resolving modules because it was specified on the command line
@@ -39,9 +41,25 @@ error[unresolved-import]: Cannot resolve imported module `string.templatelib`
3941
1 | import tomllib # error: [unresolved-import]
4042
2 | from string.templatelib import Template # error: [unresolved-import]
4143
| ^^^^^^^^^^^^^^^^^^
44+
3 | from importlib.resources import abc # error: [unresolved-import]
4245
|
4346
info: The stdlib module `string.templatelib` is only available on Python 3.14+
4447
info: Python 3.10 was assumed when resolving modules because it was specified on the command line
4548
info: rule `unresolved-import` is enabled by default
4649
4750
```
51+
52+
```
53+
error[unresolved-import]: Module `importlib.resources` has no member `abc`
54+
--> src/mdtest_snippet.py:3:33
55+
|
56+
1 | import tomllib # error: [unresolved-import]
57+
2 | from string.templatelib import Template # error: [unresolved-import]
58+
3 | from importlib.resources import abc # error: [unresolved-import]
59+
| ^^^
60+
|
61+
info: The stdlib module `importlib.resources` only has a `abc` submodule on Python 3.11+
62+
info: Python 3.10 was assumed when resolving modules because it was specified on the command line
63+
info: rule `unresolved-import` is enabled by default
64+
65+
```

crates/ty_python_semantic/src/types.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,7 @@ impl<'db> Type<'db> {
843843
matches!(self, Type::PropertyInstance(..))
844844
}
845845

846-
pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: Module) -> Self {
846+
pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: &Module) -> Self {
847847
Self::ModuleLiteral(ModuleLiteralType::new(db, importing_file, submodule))
848848
}
849849

@@ -8194,7 +8194,7 @@ impl<'db> ModuleLiteralType<'db> {
81948194
full_submodule_name.extend(&submodule_name);
81958195
if imported_submodules.contains(&full_submodule_name) {
81968196
if let Some(submodule) = resolve_module(db, &full_submodule_name) {
8197-
return Symbol::bound(Type::module_literal(db, importing_file, submodule));
8197+
return Symbol::bound(Type::module_literal(db, importing_file, &submodule));
81988198
}
81998199
}
82008200
}

crates/ty_python_semantic/src/types/diagnostic.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use super::{
55
CallArgumentTypes, CallDunderError, ClassBase, ClassLiteral, KnownClass,
66
add_inferred_python_version_hint_to_diagnostic,
77
};
8-
use crate::declare_lint;
98
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
109
use crate::suppression::FileSuppressionId;
1110
use crate::types::LintDiagnosticGuard;
@@ -15,6 +14,7 @@ use crate::types::string_annotation::{
1514
RAW_STRING_TYPE_ANNOTATION,
1615
};
1716
use crate::types::{KnownFunction, SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
17+
use crate::{Db, Module, ModuleName, Program, declare_lint};
1818
use itertools::Itertools;
1919
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};
2020
use ruff_python_ast::{self as ast, AnyNodeRef};
@@ -2139,3 +2139,51 @@ fn report_invalid_base<'ctx, 'db>(
21392139
));
21402140
Some(diagnostic)
21412141
}
2142+
2143+
/// This function receives an unresolved `from foo import bar` import,
2144+
/// where `foo` can be resolved to a module but that module does not
2145+
/// have a `bar` member or submdoule.
2146+
///
2147+
/// If the `foo` module originates from the standard library and `foo.bar`
2148+
/// *does* exist as a submodule in the standard library on *other* Python
2149+
/// versions, we add a hint to the diagnostic that the user may have
2150+
/// misconfigured their Python version.
2151+
pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
2152+
db: &dyn Db,
2153+
mut diagnostic: LintDiagnosticGuard,
2154+
full_submodule_name: &ModuleName,
2155+
parent_module: &Module,
2156+
) {
2157+
let Some(search_path) = parent_module.search_path() else {
2158+
return;
2159+
};
2160+
2161+
if !search_path.is_standard_library() {
2162+
return;
2163+
}
2164+
2165+
let program = Program::get(db);
2166+
let typeshed_versions = program.search_paths(db).typeshed_versions();
2167+
2168+
let Some(version_range) = typeshed_versions.exact(full_submodule_name) else {
2169+
return;
2170+
};
2171+
2172+
let python_version = program.python_version(db);
2173+
if version_range.contains(python_version) {
2174+
return;
2175+
}
2176+
2177+
diagnostic.info(format_args!(
2178+
"The stdlib module `{module_name}` only has a `{name}` \
2179+
submodule on Python {version_range}",
2180+
module_name = parent_module.name(),
2181+
name = full_submodule_name
2182+
.components()
2183+
.next_back()
2184+
.expect("A `ModuleName` always has at least one component"),
2185+
version_range = version_range.diagnostic_display(),
2186+
));
2187+
2188+
add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, "resolving modules");
2189+
}

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ use crate::{Db, FxOrderSet, Program};
102102
use super::context::{InNoTypeCheck, InferContext};
103103
use super::diagnostic::{
104104
INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL, REDUNDANT_CAST, STATIC_ASSERT_ERROR,
105-
SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, report_attempted_protocol_instantiation,
105+
SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
106+
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
106107
report_bad_argument_to_get_protocol_members, report_duplicate_bases,
107108
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
108109
report_invalid_exception_raised, report_invalid_or_unsupported_base,
@@ -4122,11 +4123,13 @@ impl<'db> TypeInferenceBuilder<'db> {
41224123
return;
41234124
};
41244125

4125-
let Some(module_ty) = self.module_type_from_name(&module_name) else {
4126+
let Some(module) = resolve_module(self.db(), &module_name) else {
41264127
self.add_unknown_declaration_with_binding(alias.into(), definition);
41274128
return;
41284129
};
41294130

4131+
let module_ty = Type::module_literal(self.db(), self.file(), &module);
4132+
41304133
// The indirection of having `star_import_info` as a separate variable
41314134
// is required in order to make the borrow checker happy.
41324135
let star_import_info = definition
@@ -4175,6 +4178,15 @@ impl<'db> TypeInferenceBuilder<'db> {
41754178
}
41764179
}
41774180

4181+
// Evaluate whether `X.Y` would constitute a valid submodule name,
4182+
// given a `from X import Y` statement. If it is valid, this will be `Some()`;
4183+
// else, it will be `None`.
4184+
let full_submodule_name = ModuleName::new(name).map(|final_part| {
4185+
let mut ret = module_name.clone();
4186+
ret.extend(&final_part);
4187+
ret
4188+
});
4189+
41784190
// If the module doesn't bind the symbol, check if it's a submodule. This won't get
41794191
// handled by the `Type::member` call because it relies on the semantic index's
41804192
// `imported_modules` set. The semantic index does not include information about
@@ -4190,35 +4202,47 @@ impl<'db> TypeInferenceBuilder<'db> {
41904202
//
41914203
// Regardless, for now, we sidestep all of that by repeating the submodule-or-attribute
41924204
// check here when inferring types for a `from...import` statement.
4193-
if let Some(submodule_name) = ModuleName::new(name) {
4194-
let mut full_submodule_name = module_name.clone();
4195-
full_submodule_name.extend(&submodule_name);
4196-
if let Some(submodule_ty) = self.module_type_from_name(&full_submodule_name) {
4197-
self.add_declaration_with_binding(
4198-
alias.into(),
4199-
definition,
4200-
&DeclaredAndInferredType::AreTheSame(submodule_ty),
4201-
);
4202-
return;
4203-
}
4205+
if let Some(submodule_type) = full_submodule_name
4206+
.as_ref()
4207+
.and_then(|submodule_name| self.module_type_from_name(submodule_name))
4208+
{
4209+
self.add_declaration_with_binding(
4210+
alias.into(),
4211+
definition,
4212+
&DeclaredAndInferredType::AreTheSame(submodule_type),
4213+
);
4214+
return;
42044215
}
42054216

4206-
if &alias.name != "*" {
4207-
let is_import_reachable = self.is_reachable(import_from);
4217+
self.add_unknown_declaration_with_binding(alias.into(), definition);
42084218

4209-
if is_import_reachable {
4210-
if let Some(builder) = self
4211-
.context
4212-
.report_lint(&UNRESOLVED_IMPORT, AnyNodeRef::Alias(alias))
4213-
{
4214-
builder.into_diagnostic(format_args!(
4215-
"Module `{module_name}` has no member `{name}`"
4216-
));
4217-
}
4218-
}
4219+
if &alias.name == "*" {
4220+
return;
42194221
}
42204222

4221-
self.add_unknown_declaration_with_binding(alias.into(), definition);
4223+
if !self.is_reachable(import_from) {
4224+
return;
4225+
}
4226+
4227+
let Some(builder) = self
4228+
.context
4229+
.report_lint(&UNRESOLVED_IMPORT, AnyNodeRef::Alias(alias))
4230+
else {
4231+
return;
4232+
};
4233+
4234+
let diagnostic = builder.into_diagnostic(format_args!(
4235+
"Module `{module_name}` has no member `{name}`"
4236+
));
4237+
4238+
if let Some(full_submodule_name) = full_submodule_name {
4239+
hint_if_stdlib_submodule_exists_on_other_versions(
4240+
self.db(),
4241+
diagnostic,
4242+
&full_submodule_name,
4243+
&module,
4244+
);
4245+
}
42224246
}
42234247

42244248
fn infer_return_statement(&mut self, ret: &ast::StmtReturn) {
@@ -4242,7 +4266,7 @@ impl<'db> TypeInferenceBuilder<'db> {
42424266

42434267
fn module_type_from_name(&self, module_name: &ModuleName) -> Option<Type<'db>> {
42444268
resolve_module(self.db(), module_name)
4245-
.map(|module| Type::module_literal(self.db(), self.file(), module))
4269+
.map(|module| Type::module_literal(self.db(), self.file(), &module))
42464270
}
42474271

42484272
fn infer_decorator(&mut self, decorator: &ast::Decorator) -> Type<'db> {

0 commit comments

Comments
 (0)