Skip to content

Commit bea92c8

Browse files
authored
[ty] More precise type inference for dictionary literals (#20523)
## Summary Extends #20360 to dictionary literals. This also improves our `TypeDict` support by passing through nested type context.
1 parent f2cc2f6 commit bea92c8

File tree

8 files changed

+240
-95
lines changed

8 files changed

+240
-95
lines changed

crates/ty_python_semantic/resources/mdtest/assignment/annotations.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ reveal_type(n) # revealed: list[Literal[1, 2, 3]]
139139
# error: [invalid-assignment] "Object of type `list[Unknown | str]` is not assignable to `list[LiteralString]`"
140140
o: list[typing.LiteralString] = ["a", "b", "c"]
141141
reveal_type(o) # revealed: list[LiteralString]
142+
143+
p: dict[int, int] = {}
144+
reveal_type(p) # revealed: dict[int, int]
145+
146+
q: dict[int | str, int] = {1: 1, 2: 2, 3: 3}
147+
reveal_type(q) # revealed: dict[int | str, int]
148+
149+
r: dict[int | str, int | str] = {1: 1, 2: 2, 3: 3}
150+
reveal_type(r) # revealed: dict[int | str, int | str]
142151
```
143152

144153
## Incorrect collection literal assignments are complained aobut

crates/ty_python_semantic/resources/mdtest/call/builtins.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ type("Foo", Base, {})
5757
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
5858
type("Foo", (1, 2), {})
5959

60-
# TODO: this should be an error
60+
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[Unknown | bytes, Unknown | int]`"
6161
type("Foo", (Base,), {b"attr": 1})
6262
```
6363

crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,49 @@
33
## Empty dictionary
44

55
```py
6-
reveal_type({}) # revealed: dict[@Todo(dict literal key type), @Todo(dict literal value type)]
6+
reveal_type({}) # revealed: dict[Unknown, Unknown]
7+
```
8+
9+
## Basic dict
10+
11+
```py
12+
reveal_type({1: 1, 2: 1}) # revealed: dict[Unknown | int, Unknown | int]
13+
```
14+
15+
## Dict of tuples
16+
17+
```py
18+
reveal_type({1: (1, 2), 2: (3, 4)}) # revealed: dict[Unknown | int, Unknown | tuple[int, int]]
19+
```
20+
21+
## Unpacked dict
22+
23+
```py
24+
a = {"a": 1, "b": 2}
25+
b = {"c": 3, "d": 4}
26+
27+
d = {**a, **b}
28+
reveal_type(d) # revealed: dict[Unknown | str, Unknown | int]
29+
```
30+
31+
## Dict of functions
32+
33+
```py
34+
def a(_: int) -> int:
35+
return 0
36+
37+
def b(_: int) -> int:
38+
return 1
39+
40+
x = {1: a, 2: b}
41+
reveal_type(x) # revealed: dict[Unknown | int, Unknown | ((_: int) -> int)]
42+
```
43+
44+
## Mixed dict
45+
46+
```py
47+
# revealed: dict[Unknown | str, Unknown | int | tuple[int, int] | tuple[int, int, int]]
48+
reveal_type({"a": 1, "b": (1, 2), "c": (1, 2, 3)})
749
```
850

951
## Dict comprehensions

crates/ty_python_semantic/resources/mdtest/narrow/assignment.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,7 @@ dd: defaultdict[int, int] = defaultdict(int)
206206
dd[0] = 0
207207
cm: ChainMap[int, int] = ChainMap({1: 1}, {0: 0})
208208
cm[0] = 0
209-
# TODO: should be ChainMap[int, int]
210-
reveal_type(cm) # revealed: ChainMap[@Todo(dict literal key type), @Todo(dict literal value type)]
209+
reveal_type(cm) # revealed: ChainMap[Unknown | int, Unknown | int]
211210

212211
reveal_type(l[0]) # revealed: Literal[0]
213212
reveal_type(d[0]) # revealed: Literal[0]

crates/ty_python_semantic/resources/mdtest/typed_dict.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,34 @@ alice["extra"] = True
8585
bob["extra"] = True
8686
```
8787

88+
## Nested `TypedDict`
89+
90+
Nested `TypedDict` fields are also supported.
91+
92+
```py
93+
from typing import TypedDict
94+
95+
class Inner(TypedDict):
96+
name: str
97+
age: int | None
98+
99+
class Person(TypedDict):
100+
inner: Inner
101+
```
102+
103+
```py
104+
alice: Person = {"inner": {"name": "Alice", "age": 30}}
105+
106+
reveal_type(alice["inner"]["name"]) # revealed: str
107+
reveal_type(alice["inner"]["age"]) # revealed: int | None
108+
109+
# error: [invalid-key] "Invalid key access on TypedDict `Inner`: Unknown key "non_existing""
110+
reveal_type(alice["inner"]["non_existing"]) # revealed: Unknown
111+
112+
# error: [invalid-key] "Invalid key access on TypedDict `Inner`: Unknown key "extra""
113+
alice: Person = {"inner": {"name": "Alice", "age": 30, "extra": 1}}
114+
```
115+
88116
## Validation of `TypedDict` construction
89117

90118
```py

crates/ty_python_semantic/src/types.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,28 @@ impl<'db> Type<'db> {
849849
matches!(self, Type::Dynamic(_))
850850
}
851851

852+
// If the type is a specialized instance of the given `KnownClass`, returns the specialization.
853+
pub(crate) fn known_specialization(
854+
self,
855+
known_class: KnownClass,
856+
db: &'db dyn Db,
857+
) -> Option<Specialization<'db>> {
858+
let class_type = match self {
859+
Type::NominalInstance(instance) => instance,
860+
Type::TypeAlias(alias) => alias.value_type(db).into_nominal_instance()?,
861+
_ => return None,
862+
}
863+
.class(db);
864+
865+
if !class_type.is_known(db, known_class) {
866+
return None;
867+
}
868+
869+
class_type
870+
.into_generic_alias()
871+
.map(|generic_alias| generic_alias.specialization(db))
872+
}
873+
852874
/// Returns the top materialization (or upper bound materialization) of this type, which is the
853875
/// most general form of the type that is fully static.
854876
#[must_use]

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -386,20 +386,8 @@ impl<'db> TypeContext<'db> {
386386
known_class: KnownClass,
387387
db: &'db dyn Db,
388388
) -> Option<Specialization<'db>> {
389-
let class_type = match self.annotation? {
390-
Type::NominalInstance(instance) => instance,
391-
Type::TypeAlias(alias) => alias.value_type(db).into_nominal_instance()?,
392-
_ => return None,
393-
}
394-
.class(db);
395-
396-
if !class_type.is_known(db, known_class) {
397-
return None;
398-
}
399-
400-
class_type
401-
.into_generic_alias()
402-
.map(|generic_alias| generic_alias.specialization(db))
389+
self.annotation
390+
.and_then(|ty| ty.known_specialization(known_class, db))
403391
}
404392
}
405393

0 commit comments

Comments
 (0)