diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md index 8ef5a570a1..948d3d8eff 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md @@ -1,6 +1,6 @@ # Unsupported special types -We do not understand the functional syntax for creating `NamedTuple`s, `TypedDict`s or `Enum`s yet. +We do not understand the functional syntax for creating `NamedTuple`s or `Enum`s yet. But we also do not emit false positives when these are used in type expressions. ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 41f1d172dd..d88e770b28 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4764,6 +4764,9 @@ impl<'db> Type<'db> { Parameter::positional_only(Some(Name::new_static("typename"))) .with_annotated_type(KnownClass::Str.to_instance(db)), Parameter::positional_only(Some(Name::new_static("fields"))) + // We infer this type as an anonymous `TypedDict` instance, such that the + // complete `TypeDict` instance can be constructed from it after. Note that + // `typing.TypedDict` is not otherwise allowed in type-form expressions. .with_annotated_type(Type::SpecialForm(SpecialFormType::TypedDict)) .with_default_type(Type::any()), Parameter::keyword_only(Name::new_static("total")) @@ -4858,6 +4861,7 @@ impl<'db> Type<'db> { Type::KnownInstance(KnownInstanceType::TypedDictType(typed_dict)) => Binding::single( self, Signature::new( + // TODO: List more specific parameter types here for better code completion. Parameters::new([Parameter::keyword_variadic(Name::new_static("kwargs")) .with_annotated_type(Type::any())]), Some(Type::TypedDict(typed_dict)), diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 17d921ebb9..3f025ef672 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5473,11 +5473,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { items, } = dict; - // Validate `TypedDict` dictionary literal assignments. + // Infer `TypedDict` dictionary literal assignments. if let Some(ty) = self.infer_typed_dict_expression(dict, tcx) { return ty; } + // Infer the dictionary literal passed to the `TypedDict` constructor. + if let Some(ty) = self.infer_typed_dict_constructor_literal(dict, tcx) { + return ty; + } + // Avoid false positives for the functional `TypedDict` form, which is currently // unsupported. if let Some(Type::Dynamic(DynamicType::Todo(_))) = tcx.annotation { @@ -5603,52 +5608,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { items, } = dict; - // Evaluate the dictionary literal passed to the `TypedDict` constructor. - if let Some(Type::SpecialForm(SpecialFormType::TypedDict)) = tcx.annotation { - let mut typed_dict_items = FxOrderMap::default(); - - for item in items { - let Some(Type::StringLiteral(key)) = - self.infer_optional_expression(item.key.as_ref(), TypeContext::default()) - else { - // Emit a diagnostic here? We seem to support non-string literals. - unimplemented!() - }; - - let field_ty = self.infer_typed_dict_field_type_expression(&item.value); - - let is_required = if field_ty.qualifiers.contains(TypeQualifiers::REQUIRED) { - // Explicit Required[T] annotation - always required - Truthiness::AlwaysTrue - } else if field_ty.qualifiers.contains(TypeQualifiers::NOT_REQUIRED) { - // Explicit NotRequired[T] annotation - never required - Truthiness::AlwaysFalse - } else { - // No explicit qualifier - we don't have access to the `total` qualifier here, - // so we leave this to be filled in by the `TypedDict` constructor. - Truthiness::Ambiguous - }; - - let field = Field { - single_declaration: None, - declared_ty: field_ty.inner_type(), - kind: FieldKind::TypedDict { - is_required, - is_read_only: field_ty.qualifiers.contains(TypeQualifiers::READ_ONLY), - }, - }; - - typed_dict_items.insert(ast::name::Name::new(key.value(self.db())), field); - } - - // Create an incomplete synthesized `TypedDictType`, to be completed by the `TypedDict` - // constructor binding. - return Some(Type::TypedDict(TypedDictType::from_items( - self.db(), - typed_dict_items, - ))); - } - let typed_dict = tcx.annotation.and_then(Type::into_typed_dict)?; let typed_dict_items = typed_dict.items(self.db()); @@ -5672,6 +5631,64 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|_| Type::TypedDict(typed_dict)) } + // Infer the dictionary literal passed to the `TypedDict` constructor. + fn infer_typed_dict_constructor_literal( + &mut self, + dict: &ast::ExprDict, + tcx: TypeContext<'db>, + ) -> Option> { + let ast::ExprDict { + range: _, + node_index: _, + items, + } = dict; + + let Some(Type::SpecialForm(SpecialFormType::TypedDict)) = tcx.annotation else { + return None; + }; + + let mut typed_dict_items = FxOrderMap::default(); + + for item in items { + let Some(Type::StringLiteral(key)) = + self.infer_optional_expression(item.key.as_ref(), TypeContext::default()) + else { + continue; + }; + + let field_ty = self.infer_typed_dict_field_type_expression(&item.value); + + let is_required = if field_ty.qualifiers.contains(TypeQualifiers::REQUIRED) { + // Explicit Required[T] annotation - always required + Truthiness::AlwaysTrue + } else if field_ty.qualifiers.contains(TypeQualifiers::NOT_REQUIRED) { + // Explicit NotRequired[T] annotation - never required + Truthiness::AlwaysFalse + } else { + // No explicit qualifier - we don't have access to the `total` qualifier here, + // so we leave this to be filled in by the `TypedDict` constructor. + Truthiness::Ambiguous + }; + + let field = Field { + single_declaration: None, + declared_ty: field_ty.inner_type(), + kind: FieldKind::TypedDict { + is_required, + is_read_only: field_ty.qualifiers.contains(TypeQualifiers::READ_ONLY), + }, + }; + + typed_dict_items.insert(ast::name::Name::new(key.value(self.db())), field); + } + + // Create an anonymous `TypedDict` from the items, to be completed by the `TypedDict` constructor binding. + Some(Type::TypedDict(TypedDictType::from_items( + self.db(), + typed_dict_items, + ))) + } + fn infer_typed_dict_field_type_expression( &mut self, expr: &ast::Expr, diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index 0857207179..f8598cc241 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -62,10 +62,10 @@ impl<'db> TypedDictType<'db> { TypedDictType::FromClass(class) } - /// Returns an incomplete `TypedDictType` from its items. + /// Returns an anonymous (incomplete) `TypedDictType` from its items. /// /// This is used to instantiate a `TypedDictType` from the dictionary literal passed to a - /// `TypedDict` constructor. + /// `typing.TypedDict` constructor (functional form for creating `TypedDict`s). pub(crate) fn from_items(db: &'db dyn Db, items: FxOrderMap>) -> Self { TypedDictType::Synthesized(SynthesizedTypedDictType::new( db, @@ -397,7 +397,7 @@ impl<'db> TypedDictType<'db> { #[derive(PartialOrd, Ord)] pub struct SynthesizedTypedDictType<'db> { // The dictionary literal passed to the `TypedDict` constructor is inferred as - // a nameless `SynthesizedTypedDictType`. + // an anonymous (incomplete) `SynthesizedTypedDictType`. pub(crate) name: Option, pub(crate) params: TypedDictParams,