Skip to content

Commit 91c9168

Browse files
authored
Handle nested imports correctly in from ... import (#15026)
#14946 fixed our handling of nested imports with the `import` statement, but didn't touch `from...import` statements. cf #14826 (comment)
1 parent 80577a4 commit 91c9168

File tree

4 files changed

+144
-69
lines changed

4 files changed

+144
-69
lines changed

crates/red_knot_python_semantic/resources/mdtest/import/invalid_syntax.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,25 @@ from import bar # error: [invalid-syntax]
77

88
reveal_type(bar) # revealed: Unknown
99
```
10+
11+
## Invalid nested module import
12+
13+
TODO: This is correctly flagged as an error, but we could clean up the diagnostics that we report.
14+
15+
```py
16+
# TODO: No second diagnostic
17+
# error: [invalid-syntax] "Expected ',', found '.'"
18+
# error: [unresolved-import] "Module `a` has no member `c`"
19+
from a import b.c
20+
21+
# TODO: Should these be inferred as Unknown?
22+
reveal_type(b) # revealed: <module 'a.b'>
23+
reveal_type(b.c) # revealed: Literal[1]
24+
```
25+
26+
```py path=a/__init__.py
27+
```
28+
29+
```py path=a/b.py
30+
c = 1
31+
```

crates/red_knot_python_semantic/resources/mdtest/import/relative.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,23 +121,44 @@ X = 42
121121
```
122122

123123
```py path=package/bar.py
124-
# TODO: support submodule imports
125-
from . import foo # error: [unresolved-import]
126-
127-
y = foo.X
124+
from . import foo
128125

129-
# TODO: should be `Literal[42]`
130-
reveal_type(y) # revealed: Unknown
126+
reveal_type(foo.X) # revealed: Literal[42]
131127
```
132128

133129
## Non-existent + bare to module
134130

131+
This test verifies that we emit an error when we try to import a symbol that is neither a submodule
132+
nor an attribute of `package`.
133+
135134
```py path=package/__init__.py
136135
```
137136

138137
```py path=package/bar.py
139-
# TODO: support submodule imports
140138
from . import foo # error: [unresolved-import]
141139

142140
reveal_type(foo) # revealed: Unknown
143141
```
142+
143+
## Import submodule from self
144+
145+
We don't currently consider `from...import` statements when building up the `imported_modules` set
146+
in the semantic index. When accessing an attribute of a module, we only consider it a potential
147+
submodule when that submodule name appears in the `imported_modules` set. That means that submodules
148+
that are imported via `from...import` are not visible to our type inference if you also access that
149+
submodule via the attribute on its parent package.
150+
151+
```py path=package/__init__.py
152+
```
153+
154+
```py path=package/foo.py
155+
X = 42
156+
```
157+
158+
```py path=package/bar.py
159+
from . import foo
160+
import package
161+
162+
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
163+
reveal_type(package.foo.X) # revealed: Unknown
164+
```

crates/red_knot_python_semantic/src/semantic_index.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<Sym
6262
}
6363

6464
/// Returns the set of modules that are imported anywhere in `file`.
65+
///
66+
/// This set only considers `import` statements, not `from...import` statements, because:
67+
///
68+
/// - In `from foo import bar`, we cannot determine whether `foo.bar` is a submodule (and is
69+
/// therefore imported) without looking outside the content of this file. (We could turn this
70+
/// into a _potentially_ imported modules set, but that would change how it's used in our type
71+
/// inference logic.)
72+
///
73+
/// - We cannot resolve relative imports (which aren't allowed in `import` statements) without
74+
/// knowing the name of the current module, and whether it's a package.
6575
#[salsa::tracked]
6676
pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc<FxHashSet<ModuleName>> {
6777
semantic_index(db, file).imported_modules.clone()

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 84 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,14 @@ impl<'db> TypeInferenceBuilder<'db> {
865865
self.types.bindings.insert(definition, inferred_ty);
866866
}
867867

868+
fn add_unknown_declaration_with_binding(
869+
&mut self,
870+
node: AnyNodeRef,
871+
definition: Definition<'db>,
872+
) {
873+
self.add_declaration_with_binding(node, definition, Type::Unknown, Type::Unknown);
874+
}
875+
868876
fn infer_module(&mut self, module: &ast::ModModule) {
869877
self.infer_body(&module.body);
870878
}
@@ -2121,24 +2129,14 @@ impl<'db> TypeInferenceBuilder<'db> {
21212129
// The name of the module being imported
21222130
let Some(full_module_name) = ModuleName::new(name) else {
21232131
tracing::debug!("Failed to resolve import due to invalid syntax");
2124-
self.add_declaration_with_binding(
2125-
alias.into(),
2126-
definition,
2127-
Type::Unknown,
2128-
Type::Unknown,
2129-
);
2132+
self.add_unknown_declaration_with_binding(alias.into(), definition);
21302133
return;
21312134
};
21322135

21332136
// Resolve the module being imported.
21342137
let Some(full_module_ty) = self.module_ty_from_name(&full_module_name) else {
21352138
self.diagnostics.add_unresolved_module(alias, 0, Some(name));
2136-
self.add_declaration_with_binding(
2137-
alias.into(),
2138-
definition,
2139-
Type::Unknown,
2140-
Type::Unknown,
2141-
);
2139+
self.add_unknown_declaration_with_binding(alias.into(), definition);
21422140
return;
21432141
};
21442142

@@ -2152,11 +2150,11 @@ impl<'db> TypeInferenceBuilder<'db> {
21522150
// parent package of that module.
21532151
let topmost_parent_name =
21542152
ModuleName::new(full_module_name.components().next().unwrap()).unwrap();
2155-
if let Some(topmost_parent_ty) = self.module_ty_from_name(&topmost_parent_name) {
2156-
topmost_parent_ty
2157-
} else {
2158-
Type::Unknown
2159-
}
2153+
let Some(topmost_parent_ty) = self.module_ty_from_name(&topmost_parent_name) else {
2154+
self.add_unknown_declaration_with_binding(alias.into(), definition);
2155+
return;
2156+
};
2157+
topmost_parent_ty
21602158
} else {
21612159
// If there's no `as` clause and the imported module isn't nested, then the imported
21622160
// module _is_ what we bind into the current scope.
@@ -2243,12 +2241,6 @@ impl<'db> TypeInferenceBuilder<'db> {
22432241
// TODO:
22442242
// - Absolute `*` imports (`from collections import *`)
22452243
// - Relative `*` imports (`from ...foo import *`)
2246-
// - Submodule imports (`from collections import abc`,
2247-
// where `abc` is a submodule of the `collections` package)
2248-
//
2249-
// For the last item, see the currently skipped tests
2250-
// `follow_relative_import_bare_to_module()` and
2251-
// `follow_nonexistent_import_bare_to_module()`.
22522244
let ast::StmtImportFrom { module, level, .. } = import_from;
22532245
let module = module.as_deref();
22542246

@@ -2271,46 +2263,13 @@ impl<'db> TypeInferenceBuilder<'db> {
22712263
.ok_or(ModuleNameResolutionError::InvalidSyntax)
22722264
};
22732265

2274-
let ty = match module_name {
2275-
Ok(module_name) => {
2276-
if let Some(module_ty) = self.module_ty_from_name(&module_name) {
2277-
let ast::Alias {
2278-
range: _,
2279-
name,
2280-
asname: _,
2281-
} = alias;
2282-
2283-
match module_ty.member(self.db, &ast::name::Name::new(&name.id)) {
2284-
Symbol::Type(ty, boundness) => {
2285-
if boundness == Boundness::PossiblyUnbound {
2286-
self.diagnostics.add_lint(
2287-
&POSSIBLY_UNBOUND_IMPORT,
2288-
AnyNodeRef::Alias(alias),
2289-
format_args!("Member `{name}` of module `{module_name}` is possibly unbound", ),
2290-
);
2291-
}
2292-
2293-
ty
2294-
}
2295-
Symbol::Unbound => {
2296-
self.diagnostics.add_lint(
2297-
&UNRESOLVED_IMPORT,
2298-
AnyNodeRef::Alias(alias),
2299-
format_args!("Module `{module_name}` has no member `{name}`",),
2300-
);
2301-
Type::Unknown
2302-
}
2303-
}
2304-
} else {
2305-
self.diagnostics
2306-
.add_unresolved_module(import_from, *level, module);
2307-
Type::Unknown
2308-
}
2309-
}
2266+
let module_name = match module_name {
2267+
Ok(module_name) => module_name,
23102268
Err(ModuleNameResolutionError::InvalidSyntax) => {
23112269
tracing::debug!("Failed to resolve import due to invalid syntax");
23122270
// Invalid syntax diagnostics are emitted elsewhere.
2313-
Type::Unknown
2271+
self.add_unknown_declaration_with_binding(alias.into(), definition);
2272+
return;
23142273
}
23152274
Err(ModuleNameResolutionError::TooManyDots) => {
23162275
tracing::debug!(
@@ -2319,7 +2278,8 @@ impl<'db> TypeInferenceBuilder<'db> {
23192278
);
23202279
self.diagnostics
23212280
.add_unresolved_module(import_from, *level, module);
2322-
Type::Unknown
2281+
self.add_unknown_declaration_with_binding(alias.into(), definition);
2282+
return;
23232283
}
23242284
Err(ModuleNameResolutionError::UnknownCurrentModule) => {
23252285
tracing::debug!(
@@ -2329,10 +2289,72 @@ impl<'db> TypeInferenceBuilder<'db> {
23292289
);
23302290
self.diagnostics
23312291
.add_unresolved_module(import_from, *level, module);
2332-
Type::Unknown
2292+
self.add_unknown_declaration_with_binding(alias.into(), definition);
2293+
return;
2294+
}
2295+
};
2296+
2297+
let Some(module_ty) = self.module_ty_from_name(&module_name) else {
2298+
self.diagnostics
2299+
.add_unresolved_module(import_from, *level, module);
2300+
self.add_unknown_declaration_with_binding(alias.into(), definition);
2301+
return;
2302+
};
2303+
2304+
let ast::Alias {
2305+
range: _,
2306+
name,
2307+
asname: _,
2308+
} = alias;
2309+
2310+
// Check if the symbol being imported is a submodule. This won't get handled by the
2311+
// `Type::member` call below because it relies on the semantic index's `imported_modules`
2312+
// set. The semantic index does not include information about `from...import` statements
2313+
// because there are two things it cannot determine while only inspecting the content of
2314+
// the current file:
2315+
//
2316+
// - whether the imported symbol is an attribute or submodule
2317+
// - whether the containing file is in a module or a package (needed to correctly resolve
2318+
// relative imports)
2319+
//
2320+
// The first would be solvable by making it a _potentially_ imported modules set. The
2321+
// second is not.
2322+
//
2323+
// Regardless, for now, we sidestep all of that by repeating the submodule-or-attribute
2324+
// check here when inferring types for a `from...import` statement.
2325+
if let Some(submodule_name) = ModuleName::new(name) {
2326+
let mut full_submodule_name = module_name.clone();
2327+
full_submodule_name.extend(&submodule_name);
2328+
if let Some(submodule_ty) = self.module_ty_from_name(&full_submodule_name) {
2329+
self.add_declaration_with_binding(
2330+
alias.into(),
2331+
definition,
2332+
submodule_ty,
2333+
submodule_ty,
2334+
);
2335+
return;
23332336
}
2337+
}
2338+
2339+
// Otherwise load the requested attribute from the module.
2340+
let Symbol::Type(ty, boundness) = module_ty.member(self.db, name) else {
2341+
self.diagnostics.add_lint(
2342+
&UNRESOLVED_IMPORT,
2343+
AnyNodeRef::Alias(alias),
2344+
format_args!("Module `{module_name}` has no member `{name}`",),
2345+
);
2346+
self.add_unknown_declaration_with_binding(alias.into(), definition);
2347+
return;
23342348
};
23352349

2350+
if boundness == Boundness::PossiblyUnbound {
2351+
self.diagnostics.add_lint(
2352+
&POSSIBLY_UNBOUND_IMPORT,
2353+
AnyNodeRef::Alias(alias),
2354+
format_args!("Member `{name}` of module `{module_name}` is possibly unbound",),
2355+
);
2356+
}
2357+
23362358
self.add_declaration_with_binding(alias.into(), definition, ty, ty);
23372359
}
23382360

0 commit comments

Comments
 (0)