Skip to content

Commit ed4a0b3

Browse files
pilleyecarljm
andauthored
[red-knot] don't include Unknown in the type for a conditionally-defined import (#13563)
## Summary Fixes the bug described in #13514 where an unbound public type defaulted to the type or `Unknown`, whereas it should only be the type if unbound. ## Test Plan Added a new test case --------- Co-authored-by: Carl Meyer <[email protected]>
1 parent 2095ea8 commit ed4a0b3

File tree

10 files changed

+126
-91
lines changed

10 files changed

+126
-91
lines changed
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
# Unbound
22

3-
## Maybe unbound
4-
5-
```py
6-
if flag:
7-
y = 3
8-
x = y
9-
reveal_type(x) # revealed: Unbound | Literal[3]
10-
```
11-
123
## Unbound
134

145
```py
15-
x = foo; foo = 1
6+
x = foo
7+
foo = 1
168
reveal_type(x) # revealed: Unbound
179
```
1810

1911
## Unbound class variable
2012

21-
Class variables can reference global variables unless overridden within the class scope.
13+
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
2214

2315
```py
2416
x = 1
@@ -27,6 +19,6 @@ class C:
2719
if flag:
2820
x = 2
2921

30-
reveal_type(C.x) # revealed: Unbound | Literal[2]
22+
reveal_type(C.x) # revealed: Literal[2]
3123
reveal_type(C.y) # revealed: Literal[1]
3224
```

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ else:
2828
y = 5
2929
s = y
3030
x = y
31+
3132
reveal_type(x) # revealed: Literal[3, 4, 5]
3233
reveal_type(r) # revealed: Unbound | Literal[2]
3334
reveal_type(s) # revealed: Unbound | Literal[5]
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
# Except star
22

3-
TODO(Alex): Once we support `sys.version_info` branches, we can set `--target-version=py311` in these tests and the inferred type will just be `BaseExceptionGroup`
4-
53
## Except\* with BaseException
64

75
```py
86
try:
97
x
108
except* BaseException as e:
11-
reveal_type(e) # revealed: Unknown | BaseExceptionGroup
9+
reveal_type(e) # revealed: BaseExceptionGroup
1210
```
1311

1412
## Except\* with specific exception
@@ -18,7 +16,7 @@ try:
1816
x
1917
except* OSError as e:
2018
# TODO(Alex): more precise would be `ExceptionGroup[OSError]`
21-
reveal_type(e) # revealed: Unknown | BaseExceptionGroup
19+
reveal_type(e) # revealed: BaseExceptionGroup
2220
```
2321

2422
## Except\* with multiple exceptions
@@ -28,5 +26,5 @@ try:
2826
x
2927
except* (TypeError, AttributeError) as e:
3028
#TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
31-
reveal_type(e) # revealed: Unknown | BaseExceptionGroup
29+
reveal_type(e) # revealed: BaseExceptionGroup
3230
```

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,39 @@
11
# Conditional imports
22

3+
## Maybe unbound
4+
5+
```py path=maybe_unbound.py
6+
if flag:
7+
y = 3
8+
x = y
9+
reveal_type(x) # revealed: Unbound | Literal[3]
10+
reveal_type(y) # revealed: Unbound | Literal[3]
11+
```
12+
13+
```py
14+
from maybe_unbound import x, y
15+
reveal_type(x) # revealed: Literal[3]
16+
reveal_type(y) # revealed: Literal[3]
17+
```
18+
19+
## Maybe unbound annotated
20+
21+
```py path=maybe_unbound_annotated.py
22+
if flag:
23+
y: int = 3
24+
x = y
25+
reveal_type(x) # revealed: Unbound | Literal[3]
26+
reveal_type(y) # revealed: Unbound | Literal[3]
27+
```
28+
29+
Importing an annotated name prefers the declared type over the inferred type:
30+
31+
```py
32+
from maybe_unbound_annotated import x, y
33+
reveal_type(x) # revealed: Literal[3]
34+
reveal_type(y) # revealed: int
35+
```
36+
337
## Reimport
438

539
```py path=c.py
@@ -14,8 +48,7 @@ else:
1448
```
1549

1650
```py
17-
# TODO we should not emit this error
18-
from b import f # error: [invalid-assignment] "Object of type `Literal[f, f]` is not assignable to `Literal[f, f]`"
51+
from b import f
1952
# TODO: We should disambiguate in such cases, showing `Literal[b.f, c.f]`.
2053
reveal_type(f) # revealed: Literal[f, f]
2154
```

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
```py
66
import bar # error: "Cannot resolve import `bar`"
7+
reveal_type(bar) # revealed: Unknown
78
```
89

910
## Unresolved import from statement
1011

1112
```py
1213
from bar import baz # error: "Cannot resolve import `bar`"
14+
reveal_type(baz) # revealed: Unknown
1315
```
1416

1517
## Unresolved import from resolved module
@@ -19,22 +21,25 @@ from bar import baz # error: "Cannot resolve import `bar`"
1921

2022
```py
2123
from a import thing # error: "Module `a` has no member `thing`"
24+
reveal_type(thing) # revealed: Unknown
2225
```
2326

2427
## Resolved import of symbol from unresolved import
2528

2629
```py path=a.py
2730
import foo as foo # error: "Cannot resolve import `foo`"
31+
reveal_type(foo) # revealed: Unknown
2832
```
2933

3034
Importing the unresolved import into a second file should not trigger an additional "unresolved
3135
import" violation:
3236

3337
```py
3438
from a import foo
39+
reveal_type(foo) # revealed: Unknown
3540
```
3641

37-
## No implicit shadowing error
42+
## No implicit shadowing
3843

3944
```py path=b.py
4045
x: int
@@ -43,5 +48,5 @@ x: int
4348
```py
4449
from b import x
4550

46-
x = 'foo' # error: "Object of type `Literal["foo"]"
51+
x = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]"
4752
```

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ reveal_type(y) # revealed: Unknown
126126
```
127127

128128
```py path=package/bar.py
129-
# TODO: submodule imports possibly not supported right now?
129+
# TODO: support submodule imports
130130
from . import foo # error: [unresolved-import]
131131

132132
reveal_type(foo) # revealed: Unknown
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Builtin scope
2+
3+
## Conditionally global or builtin
4+
5+
If a builtin name is conditionally defined as a global, a name lookup should union the builtin type
6+
with the conditionally-defined type:
7+
8+
```py
9+
def returns_bool() -> bool:
10+
return True
11+
12+
if returns_bool():
13+
copyright = 1
14+
15+
def f():
16+
reveal_type(copyright) # revealed: Literal[copyright] | Literal[1]
17+
```
18+
19+
## Conditionally global or builtin, with annotation
20+
21+
Same is true if the name is annotated:
22+
23+
```py
24+
def returns_bool() -> bool:
25+
return True
26+
27+
if returns_bool():
28+
copyright: int = 1
29+
30+
def f():
31+
reveal_type(copyright) # revealed: Literal[copyright] | int
32+
```

crates/red_knot_python_semantic/src/stdlib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ fn core_module_symbol_ty<'db>(
3737
) -> Type<'db> {
3838
resolve_module(db, &core_module.name())
3939
.map(|module| global_symbol_ty(db, module.file(), symbol))
40+
.map(|ty| {
41+
if ty.is_unbound() {
42+
ty
43+
} else {
44+
ty.replace_unbound_with(db, Type::Never)
45+
}
46+
})
4047
.unwrap_or(Type::Unbound)
4148
}
4249

crates/red_knot_python_semantic/src/types.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ fn symbol_ty_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymb
6060
use_def.public_bindings(symbol),
6161
use_def
6262
.public_may_be_unbound(symbol)
63-
.then_some(Type::Unknown),
63+
.then_some(Type::Unbound),
6464
))
6565
} else {
6666
None
@@ -79,7 +79,7 @@ fn symbol_ty_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymb
7979
}
8080
}
8181

82-
/// Shorthand for `symbol_ty` that takes a symbol name instead of an ID.
82+
/// Shorthand for `symbol_ty_by_id` that takes a symbol name instead of an ID.
8383
fn symbol_ty<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Type<'db> {
8484
let table = symbol_table(db, scope);
8585
table
@@ -381,7 +381,7 @@ impl<'db> Type<'db> {
381381
Type::Union(union) => {
382382
union.map(db, |element| element.replace_unbound_with(db, replacement))
383383
}
384-
ty => *ty,
384+
_ => *self,
385385
}
386386
}
387387

@@ -444,6 +444,9 @@ impl<'db> Type<'db> {
444444
///
445445
/// [assignable to]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
446446
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
447+
if self.is_equivalent_to(db, target) {
448+
return true;
449+
}
447450
match (self, target) {
448451
(Type::Unknown | Type::Any | Type::Todo, _) => true,
449452
(_, Type::Unknown | Type::Any | Type::Todo) => true,
@@ -1426,7 +1429,8 @@ impl<'db> ClassType<'db> {
14261429
pub fn class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
14271430
let member = self.own_class_member(db, name);
14281431
if !member.is_unbound() {
1429-
return member;
1432+
// TODO diagnostic if maybe unbound?
1433+
return member.replace_unbound_with(db, Type::Never);
14301434
}
14311435

14321436
self.inherited_class_member(db, name)
@@ -1625,6 +1629,7 @@ mod tests {
16251629
#[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("bytes"))]
16261630
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
16271631
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::Unknown, Ty::BuiltinInstance("str")]))]
1632+
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))]
16281633
fn is_assignable_to(from: Ty, to: Ty) {
16291634
let db = setup_db();
16301635
assert!(from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));

0 commit comments

Comments
 (0)