From 002f9057dba537e726fb40b8689871dbada288eb Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 15 Jul 2025 12:47:19 +0100 Subject: [PATCH] [ty] Reduce false positives for `TypedDict` types (#19354) --- .../resources/mdtest/narrow/assignment.md | 2 +- .../ty_python_semantic/resources/mdtest/typed_dict.md | 6 ++---- crates/ty_python_semantic/src/types.rs | 10 ++++++++++ crates/ty_python_semantic/src/types/call/bind.rs | 2 +- crates/ty_python_semantic/src/types/class.rs | 11 ++++++++++- crates/ty_python_semantic/src/types/class_base.rs | 5 +++-- crates/ty_python_semantic/src/types/infer.rs | 6 ++++-- crates/ty_python_semantic/src/types/type_ordering.rs | 3 +++ 8 files changed, 34 insertions(+), 11 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md index d5a59ad275..3ba2200598 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md @@ -245,7 +245,7 @@ class D(TypedDict): td = D(x=1, label="a") td["x"] = 0 # TODO: should be Literal[0] -reveal_type(td["x"]) # revealed: @Todo(TypedDict) +reveal_type(td["x"]) # revealed: @Todo(Support for `TypedDict`) # error: [unresolved-reference] does["not"]["exist"] = 0 diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 217bfc9e4d..2415f71049 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -10,8 +10,6 @@ class Person(TypedDict): name: str age: int | None -# TODO: This should not be an error: -# error: [invalid-assignment] alice: Person = {"name": "Alice", "age": 30} # Alternative syntax @@ -22,6 +20,6 @@ msg = Message(id=1, content="Hello") # No errors for yet-unsupported features (`closed`): OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True) -reveal_type(Person.__required_keys__) # revealed: @Todo(TypedDict) -reveal_type(Message.__required_keys__) # revealed: @Todo(TypedDict) +reveal_type(Person.__required_keys__) # revealed: @Todo(Support for `TypedDict`) +reveal_type(Message.__required_keys__) # revealed: @Todo(Support for `TypedDict`) ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index d5f56c027a..2bb6a955eb 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -5880,6 +5880,9 @@ pub enum DynamicType { /// A special Todo-variant for type aliases declared using `typing.TypeAlias`. /// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions. TodoTypeAlias, + /// A special Todo-variant for classes inheriting from `TypedDict`. + /// A temporary variant to avoid false positives while we wait for full support. + TodoTypedDict, } impl DynamicType { @@ -5911,6 +5914,13 @@ impl std::fmt::Display for DynamicType { f.write_str("@Todo") } } + DynamicType::TodoTypedDict => { + if cfg!(debug_assertions) { + f.write_str("@Todo(Support for `TypedDict`)") + } else { + f.write_str("@Todo") + } + } } } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 771dec6d89..e8cfeb8311 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -985,7 +985,7 @@ impl<'db> Bindings<'db> { }, Type::SpecialForm(SpecialFormType::TypedDict) => { - overload.set_return_type(todo_type!("TypedDict")); + overload.set_return_type(todo_type!("Support for `TypedDict`")); } // Not a special case diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 48f2bfaf58..b2c2c0ff32 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -21,7 +21,7 @@ use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signatu use crate::types::tuple::TupleType; use crate::types::{ BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams, - KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation, TypeTransformer, + DynamicType, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation, TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, infer_definition_types, }; use crate::{ @@ -415,6 +415,15 @@ impl<'db> ClassType<'db> { other: Self, relation: TypeRelation, ) -> bool { + // TODO: remove this branch once we have proper support for TypedDicts. + if self.is_known(db, KnownClass::Dict) + && other + .iter_mro(db) + .any(|b| matches!(b, ClassBase::Dynamic(DynamicType::TodoTypedDict))) + { + return true; + } + self.iter_mro(db).any(|base| { match base { ClassBase::Dynamic(_) => match relation { diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index c4b3d1f2a6..f682f5d9f4 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -51,7 +51,8 @@ impl<'db> ClassBase<'db> { ClassBase::Dynamic( DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec - | DynamicType::TodoTypeAlias, + | DynamicType::TodoTypeAlias + | DynamicType::TodoTypedDict, ) => "@Todo", ClassBase::Protocol => "Protocol", ClassBase::Generic => "Generic", @@ -229,7 +230,7 @@ impl<'db> ClassBase<'db> { SpecialFormType::OrderedDict => { Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db)) } - SpecialFormType::TypedDict => Self::try_from_type(db, todo_type!("TypedDict")), + SpecialFormType::TypedDict => Some(Self::Dynamic(DynamicType::TodoTypedDict)), SpecialFormType::Callable => { Self::try_from_type(db, todo_type!("Support for Callable as a base class")) } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 94efef80b4..d0a1ee33ee 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -6508,7 +6508,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { todo @ Type::Dynamic( DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec - | DynamicType::TodoTypeAlias, + | DynamicType::TodoTypeAlias + | DynamicType::TodoTypedDict, ), _, _, @@ -6518,7 +6519,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { todo @ Type::Dynamic( DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec - | DynamicType::TodoTypeAlias, + | DynamicType::TodoTypeAlias + | DynamicType::TodoTypedDict, ), _, ) => Some(todo), diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index 5393fd8933..4054f58e17 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -253,6 +253,9 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering (DynamicType::TodoTypeAlias, _) => Ordering::Less, (_, DynamicType::TodoTypeAlias) => Ordering::Greater, + + (DynamicType::TodoTypedDict, _) => Ordering::Less, + (_, DynamicType::TodoTypedDict) => Ordering::Greater, } }