Skip to content

Commit afd662c

Browse files
committed
Fix TypedDict
1 parent 265a718 commit afd662c

2 files changed

Lines changed: 106 additions & 2 deletions

File tree

crates/ty_python_semantic/resources/mdtest/typed_dict.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,87 @@ takes_formatter({"format": "%(message)s"})
7171
takes_formatter({"factory": object(), "facility": "local0"})
7272
```
7373

74+
Large `dict[str, TypedDict]` literals should still preserve inner `TypedDict` keys after crossing
75+
the large-collection promotion threshold:
76+
77+
```py
78+
from typing import TypedDict
79+
80+
class Entry(TypedDict):
81+
a: str
82+
b: bool
83+
84+
entries: dict[str, Entry] = {
85+
"k0": {"a": "v", "b": False},
86+
"k1": {"a": "v", "b": False},
87+
"k2": {"a": "v", "b": False},
88+
"k3": {"a": "v", "b": False},
89+
"k4": {"a": "v", "b": False},
90+
"k5": {"a": "v", "b": False},
91+
"k6": {"a": "v", "b": False},
92+
"k7": {"a": "v", "b": False},
93+
"k8": {"a": "v", "b": False},
94+
"k9": {"a": "v", "b": False},
95+
"k10": {"a": "v", "b": False},
96+
"k11": {"a": "v", "b": False},
97+
"k12": {"a": "v", "b": False},
98+
"k13": {"a": "v", "b": False},
99+
"k14": {"a": "v", "b": False},
100+
"k15": {"a": "v", "b": False},
101+
"k16": {"a": "v", "b": False},
102+
"k17": {"a": "v", "b": False},
103+
"k18": {"a": "v", "b": False},
104+
"k19": {"a": "v", "b": False},
105+
"k20": {"a": "v", "b": False},
106+
"k21": {"a": "v", "b": False},
107+
"k22": {"a": "v", "b": False},
108+
"k23": {"a": "v", "b": False},
109+
"k24": {"a": "v", "b": False},
110+
"k25": {"a": "v", "b": False},
111+
"k26": {"a": "v", "b": False},
112+
"k27": {"a": "v", "b": False},
113+
"k28": {"a": "v", "b": False},
114+
"k29": {"a": "v", "b": False},
115+
"k30": {"a": "v", "b": False},
116+
"k31": {"a": "v", "b": False},
117+
"k32": {"a": "v", "b": False},
118+
"k33": {"a": "v", "b": False},
119+
"k34": {"a": "v", "b": False},
120+
"k35": {"a": "v", "b": False},
121+
"k36": {"a": "v", "b": False},
122+
"k37": {"a": "v", "b": False},
123+
"k38": {"a": "v", "b": False},
124+
"k39": {"a": "v", "b": False},
125+
"k40": {"a": "v", "b": False},
126+
"k41": {"a": "v", "b": False},
127+
"k42": {"a": "v", "b": False},
128+
"k43": {"a": "v", "b": False},
129+
"k44": {"a": "v", "b": False},
130+
"k45": {"a": "v", "b": False},
131+
"k46": {"a": "v", "b": False},
132+
"k47": {"a": "v", "b": False},
133+
"k48": {"a": "v", "b": False},
134+
"k49": {"a": "v", "b": False},
135+
"k50": {"a": "v", "b": False},
136+
"k51": {"a": "v", "b": False},
137+
"k52": {"a": "v", "b": False},
138+
"k53": {"a": "v", "b": False},
139+
"k54": {"a": "v", "b": False},
140+
"k55": {"a": "v", "b": False},
141+
"k56": {"a": "v", "b": False},
142+
"k57": {"a": "v", "b": False},
143+
"k58": {"a": "v", "b": False},
144+
"k59": {"a": "v", "b": False},
145+
"k60": {"a": "v", "b": False},
146+
"k61": {"a": "v", "b": False},
147+
"k62": {"a": "v", "b": False},
148+
"k63": {"a": "v", "b": False},
149+
"k64": {"a": "v", "b": False},
150+
}
151+
152+
reveal_type(entries["k0"]) # revealed: Entry
153+
```
154+
74155
Methods that are available on `dict`s are also available on `TypedDict`s:
75156

76157
```py

crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::types::diagnostic::{
1111
INVALID_ARGUMENT_TYPE, INVALID_TYPE_FORM, MISSING_ARGUMENT, TOO_MANY_POSITIONAL_ARGUMENTS,
1212
UNKNOWN_ARGUMENT, report_mismatched_type_name,
1313
};
14+
use crate::types::infer::InferenceFlags;
1415
use crate::types::infer::builder::DeferredExpressionState;
1516
use crate::types::special_form::TypeQualifier;
1617
use crate::types::typed_dict::{
@@ -61,6 +62,25 @@ impl<'expr> TypedDictConstructorForm<'expr> {
6162
}
6263

6364
impl<'db> TypeInferenceBuilder<'db, '_> {
65+
/// Preserve string literal keys while inferring `TypedDict` fields, even when the enclosing
66+
/// collection enables large-literal promotion.
67+
fn infer_typed_dict_key_expression(&mut self, key: &ast::Expr) -> Type<'db> {
68+
if let Some(key_ty) = self.try_expression_type(key) {
69+
return key_ty.as_string_literal().map_or_else(
70+
|| {
71+
key.as_string_literal_expr().map_or(key_ty, |literal| {
72+
Type::string_literal(self.db(), literal.value.to_str())
73+
})
74+
},
75+
|_| key_ty,
76+
);
77+
}
78+
79+
self.with_inference_flag(InferenceFlags::PROMOTE_LITERALS, false, |builder| {
80+
builder.infer_expression(key, TypeContext::default())
81+
})
82+
}
83+
6484
/// Infer a `TypedDict(name, fields)` call expression.
6585
///
6686
/// This method *does not* call `infer_expression` on the object being called;
@@ -309,7 +329,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
309329
let typed_dict_items = typed_dict.items(self.db());
310330

311331
for item in items {
312-
let key_ty = self.infer_optional_expression(item.key.as_ref(), TypeContext::default());
332+
let key_ty = item
333+
.key
334+
.as_ref()
335+
.map(|key| self.infer_typed_dict_key_expression(key));
313336
if let Some((key, key_ty)) = item.key.as_ref().zip(key_ty) {
314337
item_types.insert(key.node_index().load(), key_ty);
315338
}
@@ -436,7 +459,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
436459
let value_tcx = item
437460
.key
438461
.as_ref()
439-
.map(|key| self.get_or_infer_expression(key, TypeContext::default()))
462+
.map(|key| self.infer_typed_dict_key_expression(key))
440463
.and_then(Type::as_string_literal)
441464
.and_then(|key| items.get(key.value(self.db())))
442465
.map(|field| TypeContext::new(Some(field.declared_ty)))

0 commit comments

Comments
 (0)