Skip to content

Commit 0fbe7db

Browse files
committed
[red-knot] Add support for import conventions
1 parent 6a2f1a8 commit 0fbe7db

File tree

6 files changed

+323
-89
lines changed

6 files changed

+323
-89
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Import conventions
2+
3+
Tests that validates the import conventions.
4+
5+
Reference:
6+
* <https://typing.readthedocs.io/en/latest/spec/distributing.html#import-conventions>
7+
8+
## Builtins scope
9+
10+
When looking up for a name, red knot will fallback to using the builtins scope
11+
if the name is not found in the current scope. The `builtins.pyi` file, that will
12+
be used to resolve any symbol in the builtins scope, contains multiple symbols
13+
from other modules (e.g., `typing`) but those are not being re-exported.
14+
15+
```py
16+
# These symbols are being imported in `builtins.pyi` but shouldn't be considered as being
17+
# available in the builtins scope.
18+
19+
# error: "Name `Literal` used when not defined"
20+
a: Literal[1] = 1
21+
22+
# error: "Name `sys` used when not defined"
23+
reveal_type(sys.executable) # revealed: str
24+
```
25+
26+
## Builtins import
27+
28+
Try to import the symbols from the module which aren't explicitly exporting them.
29+
30+
```py
31+
# error: "Module `builtins` does not explicitly export attribute `Literal`"
32+
# error: "Module `builtins` does not explicitly export attribute `sys`"
33+
from builtins import Literal, sys
34+
35+
# TODO: This should be an error but we don't understand `*` imports yet and
36+
# the `collections.abc` uses `from _collections_abc import *`.
37+
from math import Iterable
38+
```
39+
40+
## Explicitly re-exported symbols
41+
42+
When explicitly re-exporting a symbol or a module, it should not raise an error
43+
when importing it. This tests both `import ...` and `from ... import ...` forms.
44+
45+
Note: Submodule imports in `import ...` form doesn't work because it's a syntax
46+
error. For example, in `import os.path as os.path` the `os.path` is not a valid
47+
identifier.
48+
49+
```py
50+
from b import Any, Literal, ast
51+
52+
reveal_type(Any) # revealed: typing.Any
53+
reveal_type(Literal) # revealed: typing.Literal
54+
reveal_type(ast) # revealed: <module 'ast'>
55+
```
56+
57+
```pyi path=b.pyi
58+
import ast as ast
59+
from typing import Any as Any, Literal as Literal
60+
```
61+
62+
## Implicitly re-exported symbols
63+
64+
Here, none of the symbols are being re-exported in the stub file. Regardless,
65+
we should still infer the types correctly.
66+
67+
```py
68+
# error: 15 [implicit-reexport] "Module `b` does not explicitly export attribute `ast`"
69+
# error: 20 [implicit-reexport] "Module `b` does not explicitly export attribute `Any`"
70+
# error: 25 [implicit-reexport] "Module `b` does not explicitly export attribute `Literal`"
71+
from b import ast, Any, Literal
72+
73+
reveal_type(Any) # revealed: typing.Any
74+
reveal_type(Literal) # revealed: typing.Literal
75+
reveal_type(ast) # revealed: <module 'ast'>
76+
```
77+
78+
```pyi path=b.pyi
79+
import ast
80+
from typing import Any, Literal
81+
```
82+
83+
TODO:
84+
* Conditional imports
85+
* `__init__` files even though we don't special case it
86+
* Submodule imports

crates/red_knot_python_semantic/src/symbol.rs

+68-28
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,30 @@ impl Boundness {
1818
}
1919
}
2020

21+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22+
pub(crate) enum ReExport {
23+
Implicit,
24+
Explicit,
25+
None,
26+
}
27+
28+
impl ReExport {
29+
pub(crate) const fn is_implicit(self) -> bool {
30+
matches!(self, ReExport::Implicit)
31+
}
32+
33+
pub(crate) fn or(self, other: ReExport) -> ReExport {
34+
match (self, other) {
35+
(ReExport::Implicit, ReExport::Explicit) | (ReExport::Explicit, ReExport::Implicit) => {
36+
ReExport::Explicit
37+
}
38+
(ReExport::Implicit, ReExport::Implicit) => ReExport::Implicit,
39+
(ReExport::Explicit, ReExport::Explicit) => ReExport::Explicit,
40+
(non_none, ReExport::None) | (ReExport::None, non_none) => non_none,
41+
}
42+
}
43+
}
44+
2145
/// The result of a symbol lookup, which can either be a (possibly unbound) type
2246
/// or a completely unbound symbol.
2347
///
@@ -37,7 +61,7 @@ impl Boundness {
3761
/// ```
3862
#[derive(Debug, Clone, PartialEq, Eq)]
3963
pub(crate) enum Symbol<'db> {
40-
Type(Type<'db>, Boundness),
64+
Type(Type<'db>, ReExport, Boundness),
4165
Unbound,
4266
}
4367

@@ -48,8 +72,8 @@ impl<'db> Symbol<'db> {
4872

4973
pub(crate) fn possibly_unbound(&self) -> bool {
5074
match self {
51-
Symbol::Type(_, Boundness::PossiblyUnbound) | Symbol::Unbound => true,
52-
Symbol::Type(_, Boundness::Bound) => false,
75+
Symbol::Type(_, _, Boundness::PossiblyUnbound) | Symbol::Unbound => true,
76+
Symbol::Type(_, _, Boundness::Bound) => false,
5377
}
5478
}
5579

@@ -59,7 +83,7 @@ impl<'db> Symbol<'db> {
5983
/// if there is at least one control-flow path where the symbol is bound, return the type.
6084
pub(crate) fn ignore_possibly_unbound(&self) -> Option<Type<'db>> {
6185
match self {
62-
Symbol::Type(ty, _) => Some(*ty),
86+
Symbol::Type(ty, _, _) => Some(*ty),
6387
Symbol::Unbound => None,
6488
}
6589
}
@@ -74,12 +98,15 @@ impl<'db> Symbol<'db> {
7498
#[must_use]
7599
pub(crate) fn or_fall_back_to(self, db: &'db dyn Db, fallback: &Symbol<'db>) -> Symbol<'db> {
76100
match fallback {
77-
Symbol::Type(fallback_ty, fallback_boundness) => match self {
78-
Symbol::Type(_, Boundness::Bound) => self,
79-
Symbol::Type(ty, boundness @ Boundness::PossiblyUnbound) => Symbol::Type(
80-
UnionType::from_elements(db, [*fallback_ty, ty]),
81-
fallback_boundness.or(boundness),
82-
),
101+
Symbol::Type(fallback_ty, fallback_re_export, fallback_boundness) => match self {
102+
Symbol::Type(_, _, Boundness::Bound) => self,
103+
Symbol::Type(ty, re_export, boundness @ Boundness::PossiblyUnbound) => {
104+
Symbol::Type(
105+
UnionType::from_elements(db, [*fallback_ty, ty]),
106+
fallback_re_export.or(re_export),
107+
fallback_boundness.or(boundness),
108+
)
109+
}
83110
Symbol::Unbound => fallback.clone(),
84111
},
85112
Symbol::Unbound => self,
@@ -89,7 +116,7 @@ impl<'db> Symbol<'db> {
89116
#[must_use]
90117
pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Symbol<'db> {
91118
match self {
92-
Symbol::Type(ty, boundness) => Symbol::Type(f(ty), boundness),
119+
Symbol::Type(ty, re_export, boundness) => Symbol::Type(f(ty), re_export, boundness),
93120
Symbol::Unbound => Symbol::Unbound,
94121
}
95122
}
@@ -114,41 +141,54 @@ mod tests {
114141
Symbol::Unbound
115142
);
116143
assert_eq!(
117-
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, PossiblyUnbound)),
118-
Symbol::Type(ty1, PossiblyUnbound)
144+
Symbol::Unbound
145+
.or_fall_back_to(&db, &Symbol::Type(ty1, ReExport::None, PossiblyUnbound)),
146+
Symbol::Type(ty1, ReExport::None, PossiblyUnbound)
119147
);
120148
assert_eq!(
121-
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, Bound)),
122-
Symbol::Type(ty1, Bound)
149+
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, ReExport::None, Bound)),
150+
Symbol::Type(ty1, ReExport::None, Bound)
123151
);
124152

125153
// Start from a possibly unbound symbol
126154
assert_eq!(
127-
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Unbound),
128-
Symbol::Type(ty1, PossiblyUnbound)
155+
Symbol::Type(ty1, ReExport::None, PossiblyUnbound)
156+
.or_fall_back_to(&db, &Symbol::Unbound),
157+
Symbol::Type(ty1, ReExport::None, PossiblyUnbound)
129158
);
130159
assert_eq!(
131-
Symbol::Type(ty1, PossiblyUnbound)
132-
.or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
133-
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), PossiblyUnbound)
160+
Symbol::Type(ty1, ReExport::None, PossiblyUnbound)
161+
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::None, PossiblyUnbound)),
162+
Symbol::Type(
163+
UnionType::from_elements(&db, [ty2, ty1]),
164+
ReExport::None,
165+
PossiblyUnbound
166+
)
134167
);
135168
assert_eq!(
136-
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
137-
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), Bound)
169+
Symbol::Type(ty1, ReExport::None, PossiblyUnbound)
170+
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::None, Bound)),
171+
Symbol::Type(
172+
UnionType::from_elements(&db, [ty2, ty1]),
173+
ReExport::None,
174+
Bound
175+
)
138176
);
139177

140178
// Start from a definitely bound symbol
141179
assert_eq!(
142-
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Unbound),
143-
Symbol::Type(ty1, Bound)
180+
Symbol::Type(ty1, ReExport::None, Bound).or_fall_back_to(&db, &Symbol::Unbound),
181+
Symbol::Type(ty1, ReExport::None, Bound)
144182
);
145183
assert_eq!(
146-
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
147-
Symbol::Type(ty1, Bound)
184+
Symbol::Type(ty1, ReExport::None, Bound)
185+
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::None, PossiblyUnbound)),
186+
Symbol::Type(ty1, ReExport::None, Bound)
148187
);
149188
assert_eq!(
150-
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
151-
Symbol::Type(ty1, Bound)
189+
Symbol::Type(ty1, ReExport::None, Bound)
190+
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::None, Bound)),
191+
Symbol::Type(ty1, ReExport::None, Bound)
152192
);
153193
}
154194
}

0 commit comments

Comments
 (0)