Skip to content

Commit e8e461d

Browse files
authored
Prioritize attribute in from/import statement (#15041)
This tweaks the new semantics from #15026 a bit when a symbol could be interpreted both as an attribute and a submodule of a package. For `from...import`, we should actually prioritize the attribute, because of how the statement itself is implemented [1]. > 1. check if the imported module has an attribute by that name > 2. if not, attempt to import a submodule with that name and then check the imported module again for that attribute [1] https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
1 parent 91c9168 commit e8e461d

File tree

2 files changed

+101
-25
lines changed

2 files changed

+101
-25
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Conflicting attributes and submodules
2+
3+
## Via import
4+
5+
```py
6+
import a.b
7+
8+
reveal_type(a.b) # revealed: <module 'a.b'>
9+
```
10+
11+
```py path=a/__init__.py
12+
b = 42
13+
```
14+
15+
```py path=a/b.py
16+
```
17+
18+
## Via from/import
19+
20+
```py
21+
from a import b
22+
23+
reveal_type(b) # revealed: Literal[42]
24+
```
25+
26+
```py path=a/__init__.py
27+
b = 42
28+
```
29+
30+
```py path=a/b.py
31+
```
32+
33+
## Via both
34+
35+
```py
36+
import a.b
37+
from a import b
38+
39+
reveal_type(b) # revealed: <module 'a.b'>
40+
reveal_type(a.b) # revealed: <module 'a.b'>
41+
```
42+
43+
```py path=a/__init__.py
44+
b = 42
45+
```
46+
47+
```py path=a/b.py
48+
```
49+
50+
## Via both (backwards)
51+
52+
In this test, we infer a different type for `b` than the runtime behavior of the Python interpreter.
53+
The interpreter will not load the submodule `a.b` during the `from a import b` statement, since `a`
54+
contains a non-module attribute named `b`. (See the [definition][from-import] of a `from...import`
55+
statement for details.) However, because our import tracking is flow-insensitive, we will see that
56+
`a.b` is imported somewhere in the file, and therefore assume that the `from...import` statement
57+
sees the submodule as the value of `b` instead of the integer.
58+
59+
```py
60+
from a import b
61+
import a.b
62+
63+
# Python would say `Literal[42]` for `b`
64+
reveal_type(b) # revealed: <module 'a.b'>
65+
reveal_type(a.b) # revealed: <module 'a.b'>
66+
```
67+
68+
```py path=a/__init__.py
69+
b = 42
70+
```
71+
72+
```py path=a/b.py
73+
```
74+
75+
[from-import]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2307,11 +2307,26 @@ impl<'db> TypeInferenceBuilder<'db> {
23072307
asname: _,
23082308
} = alias;
23092309

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:
2310+
// First try loading the requested attribute from the module.
2311+
if let Symbol::Type(ty, boundness) = module_ty.member(self.db, name) {
2312+
if boundness == Boundness::PossiblyUnbound {
2313+
// TODO: Consider loading _both_ the attribute and any submodule and unioning them
2314+
// together if the attribute exists but is possibly-unbound.
2315+
self.diagnostics.add_lint(
2316+
&POSSIBLY_UNBOUND_IMPORT,
2317+
AnyNodeRef::Alias(alias),
2318+
format_args!("Member `{name}` of module `{module_name}` is possibly unbound",),
2319+
);
2320+
}
2321+
self.add_declaration_with_binding(alias.into(), definition, ty, ty);
2322+
return;
2323+
};
2324+
2325+
// If the module doesn't bind the symbol, check if it's a submodule. This won't get
2326+
// handled by the `Type::member` call because it relies on the semantic index's
2327+
// `imported_modules` set. The semantic index does not include information about
2328+
// `from...import` statements because there are two things it cannot determine while only
2329+
// inspecting the content of the current file:
23152330
//
23162331
// - whether the imported symbol is an attribute or submodule
23172332
// - whether the containing file is in a module or a package (needed to correctly resolve
@@ -2336,26 +2351,12 @@ impl<'db> TypeInferenceBuilder<'db> {
23362351
}
23372352
}
23382353

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;
2348-
};
2349-
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-
2358-
self.add_declaration_with_binding(alias.into(), definition, ty, ty);
2354+
self.diagnostics.add_lint(
2355+
&UNRESOLVED_IMPORT,
2356+
AnyNodeRef::Alias(alias),
2357+
format_args!("Module `{module_name}` has no member `{name}`",),
2358+
);
2359+
self.add_unknown_declaration_with_binding(alias.into(), definition);
23592360
}
23602361

23612362
fn infer_return_statement(&mut self, ret: &ast::StmtReturn) {

0 commit comments

Comments
 (0)