Skip to content

Commit e334d71

Browse files
committed
[ty] Infer dict(**TypedDict) in TypedDict context
1 parent 10f4c9a commit e334d71

File tree

3 files changed

+52
-18
lines changed

3 files changed

+52
-18
lines changed

crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,18 @@ def _():
520520
# dict() call with keyword args should be inferred as Dog
521521
animal: MaybeDog = dict(name="Buddy", breed="Labrador")
522522
reveal_type(animal) # revealed: Dog
523+
524+
def _(dog: Dog):
525+
# dict() call with unpacked TypedDict kwargs should also be inferred as Dog
526+
animal: MaybeDog = dict(**dog)
527+
reveal_type(animal) # revealed: Dog
528+
529+
def returns_dog(dog: Dog) -> Dog:
530+
return dict(**dog)
531+
532+
def takes_dog(dog: Dog) -> None: ...
533+
def _(dog: Dog):
534+
takes_dog(dict(**dog))
523535
```
524536

525537
And with set literal inference:

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

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use ruff_python_ast::{self as ast, HasNodeIndex};
33
use rustc_hash::FxHashMap;
44

55
use super::{ArgExpr, TypeInferenceBuilder};
6-
use crate::types::typed_dict::validate_typed_dict_constructor;
6+
use crate::types::typed_dict::{supports_typed_dict_unpack, validate_typed_dict_constructor};
77
use crate::types::{KnownClass, Type, TypeContext};
88

99
impl<'db> TypeInferenceBuilder<'db, '_> {
@@ -13,44 +13,62 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
1313
arguments: &ast::Arguments,
1414
call_expression_tcx: TypeContext<'db>,
1515
) -> Option<Type<'db>> {
16-
if !arguments.args.is_empty()
17-
|| arguments
18-
.keywords
19-
.iter()
20-
.any(|keyword| keyword.arg.is_none())
21-
{
16+
if !arguments.args.is_empty() {
2217
return None;
2318
}
2419

2520
// Fast-path dict(...) in TypedDict context: infer keyword values against fields,
26-
// then validate and return the TypedDict type.
21+
// then validate and return the TypedDict type. This also covers `dict(**src)` when `src`
22+
// is `TypedDict`-shaped.
2723
if let Some(tcx) = call_expression_tcx.annotation
2824
&& let Some(typed_dict) = tcx
2925
.filter_union(self.db(), Type::is_typed_dict)
3026
.as_typed_dict()
3127
{
32-
let items = typed_dict.items(self.db());
28+
let mut speculative_builder = self.speculate();
29+
let items = typed_dict.items(speculative_builder.db());
30+
let mut unpacked_keyword_types = Vec::with_capacity(arguments.keywords.len());
3331
for keyword in &arguments.keywords {
34-
if let Some(arg_name) = &keyword.arg {
35-
let value_tcx = items
36-
.get(arg_name.id.as_str())
37-
.map(|field| TypeContext::new(Some(field.declared_ty)))
38-
.unwrap_or_default();
39-
self.infer_expression(&keyword.value, value_tcx);
40-
}
32+
let value_tcx = keyword
33+
.arg
34+
.as_ref()
35+
.and_then(|arg_name| items.get(arg_name.id.as_str()))
36+
.map(|field| TypeContext::new(Some(field.declared_ty)))
37+
.unwrap_or_default();
38+
let value_ty = speculative_builder.infer_expression(&keyword.value, value_tcx);
39+
unpacked_keyword_types.push(keyword.arg.is_none().then_some(value_ty));
40+
}
41+
42+
let supports_typed_dict_context = unpacked_keyword_types
43+
.iter()
44+
.copied()
45+
.flatten()
46+
.all(|keyword_ty| supports_typed_dict_unpack(speculative_builder.db(), keyword_ty));
47+
48+
if !supports_typed_dict_context {
49+
return None;
4150
}
4251

4352
validate_typed_dict_constructor(
44-
&self.context,
53+
&speculative_builder.context,
4554
typed_dict,
4655
arguments,
4756
func.into(),
48-
|expr, _| self.expression_type(expr),
57+
|expr, _| speculative_builder.expression_type(expr),
4958
);
59+
self.extend(speculative_builder);
5060

5161
return Some(Type::TypedDict(typed_dict));
5262
}
5363

64+
if arguments
65+
.keywords
66+
.iter()
67+
.any(|keyword| keyword.arg.is_none())
68+
{
69+
return None;
70+
}
71+
5472
// Lower `dict(a=..., b=...)` to synthetic `(Literal["a"], value)` pairs so we can
5573
// reuse dict-literal inference. We key the synthetic name off the value node because
5674
// `infer_collection_literal` operates on expressions rather than keywords.

crates/ty_python_semantic/src/types/typed_dict.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,10 @@ fn extract_unpacked_typed_dict_keys<'db>(
958958
}
959959
}
960960

961+
pub(super) fn supports_typed_dict_unpack<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
962+
ty.is_dynamic() || ty.is_never() || extract_unpacked_typed_dict_keys(db, ty).is_some()
963+
}
964+
961965
/// Infers each unpacked `**kwargs` constructor argument exactly once.
962966
///
963967
/// Mixed positional-and-keyword `TypedDict` construction needs to inspect unpacked keyword types

0 commit comments

Comments
 (0)