diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index eca82c11ff..7121c4c1dd 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -556,3 +556,27 @@ def _(x: type[object], y: type[object], z: type[object]): if issubclass(z, Invariant): reveal_type(z) # revealed: type[Top[Invariant[Unknown]]] ``` + +## Narrowing with TypedDict unions + +Narrowing unions of `int` and multiple TypedDicts using `isinstance(x, dict)` should not panic +during type ordering of normalized intersection types. Regression test for +. + +```py +from typing import Any, TypedDict, cast + +class A(TypedDict): + x: str + +class B(TypedDict): + y: str + +T = int | A | B + +def test(a: Any, items: list[T]) -> None: + combined = a or items + v = combined[0] + if isinstance(v, dict): + cast(T, v) # no panic +``` diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index e49bdd1cba..5d09f432dc 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -228,9 +228,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::TypeAlias(_), _) => Ordering::Less, (_, Type::TypeAlias(_)) => Ordering::Greater, - (Type::TypedDict(left), Type::TypedDict(right)) => { - left.defining_class().cmp(&right.defining_class()) - } + (Type::TypedDict(left), Type::TypedDict(right)) => left.cmp(right), (Type::TypedDict(_), _) => Ordering::Less, (_, Type::TypedDict(_)) => Ordering::Greater, diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index 783d5407d5..af512ae463 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -48,7 +48,14 @@ impl Default for TypedDictParams { /// Type that represents the set of all inhabitants (`dict` instances) that conform to /// a given `TypedDict` schema. -#[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash, get_size2::GetSize)] +/// +/// # Ordering +/// Ordering is derived from the variant order (`Class` < `Synthesized`) and the inner types. +/// The Salsa IDs of inner types may change between runs or when the type was garbage collected +/// and recreated. +#[derive( + Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, salsa::Update, Hash, get_size2::GetSize, +)] pub enum TypedDictType<'db> { /// A reference to the class (inheriting from `typing.TypedDict`) that specifies the /// schema of this `TypedDict`. @@ -878,7 +885,11 @@ pub(super) fn validate_typed_dict_dict_literal<'db>( } } +/// # Ordering +/// Ordering is based on the type's salsa-assigned id and not on its values. +/// The id may change between runs, or when the type was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] pub struct SynthesizedTypedDictType<'db> { #[returns(ref)] pub(crate) items: TypedDictSchema<'db>,