Skip to content

Commit 90b0e80

Browse files
committed
Support conditional re-exports
1 parent 9755445 commit 90b0e80

File tree

5 files changed

+259
-93
lines changed

5 files changed

+259
-93
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def coinflip() -> bool:
9595
return True
9696

9797
if coinflip():
98-
from c import f
98+
from c import f as f
9999
else:
100100
def f(): ...
101101
```
@@ -125,7 +125,7 @@ def coinflip() -> bool:
125125
return True
126126

127127
if coinflip():
128-
from c import x
128+
from c import x as x
129129
else:
130130
x = 1
131131
```

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

+113-5
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ from math import Iterable
4444
reveal_type(Iterable) # revealed: Unknown
4545
```
4646

47-
## Explicitly re-exported symbols in stub files
47+
## Re-exported symbols in stub files
4848

4949
When a symbol is re-exported, imporing it should not raise an error. This tests both `import ...`
5050
and `from ... import ...` forms.
@@ -248,10 +248,10 @@ from c import Foo
248248
class Foo: ...
249249
```
250250

251-
## Implicit re-exports in `__init__.py`
251+
## Non-exports in `__init__.py`
252252

253253
Red knot does not special case `__init__.py` files, so if a symbol is imported in `__init__.py`
254-
without an explicit re-export, it should raise an error.
254+
which is not re-exported, it should raise an error.
255255

256256
TODO: When we support rule selection, the `implicit-reexport` rule should be disabled by default and
257257
this test case should be updated to explicitly enable it.
@@ -289,9 +289,9 @@ class Foo: ...
289289
```py
290290
```
291291

292-
## Explicit re-exports in `__init__.py`
292+
## Re-exports in `__init__.py`
293293

294-
But, if they're explicitly re-exported, it should not raise an error.
294+
But, if they're re-exported, it should not raise an error.
295295

296296
TODO: When we support rule selection, the `implicit-reexport` rule should be disabled by default and
297297
this test case should be updated to explicitly enable it.
@@ -364,3 +364,111 @@ class Foo: ...
364364

365365
```pyi
366366
```
367+
368+
## Conditional re-export in stub file
369+
370+
The following scenarios are when a re-export happens conditionally in a stub file.
371+
372+
### Global import
373+
374+
```py
375+
# error: [possibly-unbound-import] "Member `Foo` of module `a` is possibly unbound"
376+
from a import Foo
377+
378+
reveal_type(Foo) # revealed: Literal[Foo] | str
379+
```
380+
381+
`a.pyi`:
382+
383+
```pyi
384+
from b import Foo
385+
386+
def coinflip() -> bool:
387+
return True
388+
389+
if coinflip():
390+
Foo: str = "foo"
391+
```
392+
393+
`b.pyi`:
394+
395+
```pyi
396+
class Foo: ...
397+
```
398+
399+
### Both branch is an import
400+
401+
Here, both the branches of the condition are import statements where one of them re-exports while
402+
the other does not.
403+
404+
```py
405+
# error: "Member `Foo` of module `a` is possibly unbound"
406+
from a import Foo
407+
408+
reveal_type(Foo) # revealed: Literal[Foo]
409+
```
410+
411+
`a.pyi`:
412+
413+
```pyi
414+
def coinflip() -> bool: ...
415+
416+
if coinflip():
417+
from b import Foo
418+
else:
419+
from b import Foo as Foo
420+
```
421+
422+
`b.pyi`:
423+
424+
```pyi
425+
class Foo: ...
426+
```
427+
428+
### Re-export in one branch
429+
430+
```py
431+
# error: "Member `Foo` of module `a` is possibly unbound"
432+
from a import Foo
433+
434+
reveal_type(Foo) # revealed: Literal[Foo]
435+
```
436+
437+
`a.pyi`:
438+
439+
```pyi
440+
def coinflip() -> bool: ...
441+
442+
if coinflip():
443+
from b import Foo as Foo
444+
```
445+
446+
`b.pyi`:
447+
448+
```pyi
449+
class Foo: ...
450+
```
451+
452+
### Non-export in one branch
453+
454+
```py
455+
# error: "Module `a` has no member `Foo`"
456+
from a import Foo
457+
458+
reveal_type(Foo) # revealed: Unknown
459+
```
460+
461+
`a.pyi`:
462+
463+
```pyi
464+
def coinflip() -> bool: ...
465+
466+
if coinflip():
467+
from b import Foo
468+
```
469+
470+
`b.pyi`:
471+
472+
```pyi
473+
class Foo: ...
474+
```

crates/red_knot_python_semantic/src/symbol.rs

+73-30
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,66 @@ impl Boundness {
1818
}
1919
}
2020

21+
/// Indicates whether a symbol is re-exported using the [import conventions].
22+
///
23+
/// [import conventions]: https://typing.readthedocs.io/en/latest/spec/distributing.html#import-conventions
2124
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2225
pub(crate) enum ReExport {
23-
Implicit,
24-
Explicit,
25-
None,
26+
/// Symbol is either defined by an import statement and is re-exported, or it could be defined
27+
/// by any other statement or expression.
28+
///
29+
/// For example, in the following code:
30+
/// ```py
31+
/// import foo as foo
32+
/// from foo import Bar as Bar
33+
///
34+
/// baz = 1
35+
/// ```
36+
///
37+
/// All the symbols (`foo`, `Bar`, and `baz`) are re-exported.
38+
Yes,
39+
40+
/// Symbol is defined by an import statement and is not re-exported.
41+
///
42+
/// For example, in the following code:
43+
/// ```py
44+
/// import foo
45+
/// from foo import Bar
46+
/// ```
47+
///
48+
/// Both `foo` (module) and `Bar` are not re-exported.
49+
No,
50+
51+
/// Symbol is maybe re-exported.
52+
///
53+
/// For example, in the following code:
54+
/// ```py
55+
/// if flag:
56+
/// import foo
57+
/// else:
58+
/// import foo as foo
59+
/// ```
60+
///
61+
/// The `foo` symbol is maybe re-exported, depending on the value of `flag`.
62+
///
63+
/// Or, in the following code:
64+
/// ```py
65+
/// import foo
66+
///
67+
/// if flag:
68+
/// foo: int = 1
69+
/// ```
70+
///
71+
/// The `foo` symbol is maybe re-exported if the truthiness of `flag` is ambiguous.
72+
Maybe,
2673
}
2774

2875
impl ReExport {
29-
pub(crate) const fn is_implicit(self) -> bool {
30-
matches!(self, ReExport::Implicit)
31-
}
32-
3376
pub(crate) fn or(self, other: ReExport) -> ReExport {
3477
match (self, other) {
35-
(ReExport::Implicit, _) | (_, ReExport::Implicit) => ReExport::Implicit,
36-
(ReExport::Explicit, ReExport::Explicit) => ReExport::Explicit,
37-
(non_none, ReExport::None) | (ReExport::None, non_none) => non_none,
78+
(ReExport::Yes, ReExport::Yes) => ReExport::Yes,
79+
(ReExport::No, ReExport::No) => ReExport::No,
80+
_ => ReExport::Maybe,
3881
}
3982
}
4083
}
@@ -139,53 +182,53 @@ mod tests {
139182
);
140183
assert_eq!(
141184
Symbol::Unbound
142-
.or_fall_back_to(&db, &Symbol::Type(ty1, ReExport::None, PossiblyUnbound)),
143-
Symbol::Type(ty1, ReExport::None, PossiblyUnbound)
185+
.or_fall_back_to(&db, &Symbol::Type(ty1, ReExport::Yes, PossiblyUnbound)),
186+
Symbol::Type(ty1, ReExport::Yes, PossiblyUnbound)
144187
);
145188
assert_eq!(
146-
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, ReExport::None, Bound)),
147-
Symbol::Type(ty1, ReExport::None, Bound)
189+
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, ReExport::Yes, Bound)),
190+
Symbol::Type(ty1, ReExport::Yes, Bound)
148191
);
149192

150193
// Start from a possibly unbound symbol
151194
assert_eq!(
152-
Symbol::Type(ty1, ReExport::None, PossiblyUnbound)
195+
Symbol::Type(ty1, ReExport::Yes, PossiblyUnbound)
153196
.or_fall_back_to(&db, &Symbol::Unbound),
154-
Symbol::Type(ty1, ReExport::None, PossiblyUnbound)
197+
Symbol::Type(ty1, ReExport::Yes, PossiblyUnbound)
155198
);
156199
assert_eq!(
157-
Symbol::Type(ty1, ReExport::None, PossiblyUnbound)
158-
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::None, PossiblyUnbound)),
200+
Symbol::Type(ty1, ReExport::Yes, PossiblyUnbound)
201+
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::Yes, PossiblyUnbound)),
159202
Symbol::Type(
160203
UnionType::from_elements(&db, [ty2, ty1]),
161-
ReExport::None,
204+
ReExport::Yes,
162205
PossiblyUnbound
163206
)
164207
);
165208
assert_eq!(
166-
Symbol::Type(ty1, ReExport::None, PossiblyUnbound)
167-
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::None, Bound)),
209+
Symbol::Type(ty1, ReExport::Yes, PossiblyUnbound)
210+
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::Yes, Bound)),
168211
Symbol::Type(
169212
UnionType::from_elements(&db, [ty2, ty1]),
170-
ReExport::None,
213+
ReExport::Yes,
171214
Bound
172215
)
173216
);
174217

175218
// Start from a definitely bound symbol
176219
assert_eq!(
177-
Symbol::Type(ty1, ReExport::None, Bound).or_fall_back_to(&db, &Symbol::Unbound),
178-
Symbol::Type(ty1, ReExport::None, Bound)
220+
Symbol::Type(ty1, ReExport::Yes, Bound).or_fall_back_to(&db, &Symbol::Unbound),
221+
Symbol::Type(ty1, ReExport::Yes, Bound)
179222
);
180223
assert_eq!(
181-
Symbol::Type(ty1, ReExport::None, Bound)
182-
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::None, PossiblyUnbound)),
183-
Symbol::Type(ty1, ReExport::None, Bound)
224+
Symbol::Type(ty1, ReExport::Yes, Bound)
225+
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::Yes, PossiblyUnbound)),
226+
Symbol::Type(ty1, ReExport::Yes, Bound)
184227
);
185228
assert_eq!(
186-
Symbol::Type(ty1, ReExport::None, Bound)
187-
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::None, Bound)),
188-
Symbol::Type(ty1, ReExport::None, Bound)
229+
Symbol::Type(ty1, ReExport::Yes, Bound)
230+
.or_fall_back_to(&db, &Symbol::Type(ty2, ReExport::Yes, Bound)),
231+
Symbol::Type(ty1, ReExport::Yes, Bound)
189232
);
190233
}
191234
}

0 commit comments

Comments
 (0)