From 3ec63b964c1b18ffc8f2ea4656b9b8e34776c882 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 20 Dec 2025 16:59:03 +0100 Subject: [PATCH] [ty] Add support for dict(...) calls in typed dict contexts (#22113) ## Summary fixes https://github.com/astral-sh/ty/issues/2127 - handle `dict(...)` calls in TypedDict context with bidirectional inference - validate keys/values using the existing TypedDict constructor implem - mdtest: add 1 positive test, 1 negative test for invalid coverage ## Test Plan ```sh cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting cargo test # Rust testing uvx pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc. ``` fully green --- .../resources/mdtest/bidirectional.md | 3 ++ .../src/types/infer/builder.rs | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index ee233ebeb3..17d31794ab 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -55,10 +55,13 @@ class TD(TypedDict): d1 = {"x": 1} d2: TD = {"x": 1} d3: dict[str, int] = {"x": 1} +d4: TD = dict(x=1) +d5: TD = dict(x="1") # error: [invalid-argument-type] reveal_type(d1) # revealed: dict[Unknown | str, Unknown | int] reveal_type(d2) # revealed: TD reveal_type(d3) # revealed: dict[str, int] +reveal_type(d4) # revealed: TD def _() -> TD: return {"x": 1} diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index fda0a75756..52b4e3847e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8448,6 +8448,43 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { arguments, } = call_expression; + // Fast-path dict(...) in TypedDict context: infer keyword values against fields, + // then validate and return the TypedDict type. + if let Some(tcx) = tcx.annotation + && let Some(typed_dict) = tcx + .filter_union(self.db(), Type::is_typed_dict) + .as_typed_dict() + && callable_type + .as_class_literal() + .is_some_and(|class_literal| class_literal.is_known(self.db(), KnownClass::Dict)) + && arguments.args.is_empty() + && arguments + .keywords + .iter() + .all(|keyword| keyword.arg.is_some()) + { + let items = typed_dict.items(self.db()); + for keyword in &arguments.keywords { + if let Some(arg_name) = &keyword.arg { + let value_tcx = items + .get(arg_name.id.as_str()) + .map(|field| TypeContext::new(Some(field.declared_ty))) + .unwrap_or_default(); + self.infer_expression(&keyword.value, value_tcx); + } + } + + validate_typed_dict_constructor( + &self.context, + typed_dict, + arguments, + func.as_ref().into(), + |expr| self.expression_type(expr), + ); + + return Type::TypedDict(typed_dict); + } + // We don't call `Type::try_call`, because we want to perform type inference on the // arguments after matching them to parameters, but before checking that the argument types // are assignable to any parameter annotations.