This commit is contained in:
Ibraheem Ahmed 2025-10-07 13:25:22 -04:00
parent 98a0b77174
commit 2959ff19bc
4 changed files with 72 additions and 51 deletions

View File

@ -1,6 +1,6 @@
# Unsupported special types # 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. But we also do not emit false positives when these are used in type expressions.
```py ```py

View File

@ -4764,6 +4764,9 @@ impl<'db> Type<'db> {
Parameter::positional_only(Some(Name::new_static("typename"))) Parameter::positional_only(Some(Name::new_static("typename")))
.with_annotated_type(KnownClass::Str.to_instance(db)), .with_annotated_type(KnownClass::Str.to_instance(db)),
Parameter::positional_only(Some(Name::new_static("fields"))) 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_annotated_type(Type::SpecialForm(SpecialFormType::TypedDict))
.with_default_type(Type::any()), .with_default_type(Type::any()),
Parameter::keyword_only(Name::new_static("total")) Parameter::keyword_only(Name::new_static("total"))
@ -4858,6 +4861,7 @@ impl<'db> Type<'db> {
Type::KnownInstance(KnownInstanceType::TypedDictType(typed_dict)) => Binding::single( Type::KnownInstance(KnownInstanceType::TypedDictType(typed_dict)) => Binding::single(
self, self,
Signature::new( Signature::new(
// TODO: List more specific parameter types here for better code completion.
Parameters::new([Parameter::keyword_variadic(Name::new_static("kwargs")) Parameters::new([Parameter::keyword_variadic(Name::new_static("kwargs"))
.with_annotated_type(Type::any())]), .with_annotated_type(Type::any())]),
Some(Type::TypedDict(typed_dict)), Some(Type::TypedDict(typed_dict)),

View File

@ -5473,11 +5473,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
items, items,
} = dict; } = dict;
// Validate `TypedDict` dictionary literal assignments. // Infer `TypedDict` dictionary literal assignments.
if let Some(ty) = self.infer_typed_dict_expression(dict, tcx) { if let Some(ty) = self.infer_typed_dict_expression(dict, tcx) {
return ty; 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 // Avoid false positives for the functional `TypedDict` form, which is currently
// unsupported. // unsupported.
if let Some(Type::Dynamic(DynamicType::Todo(_))) = tcx.annotation { if let Some(Type::Dynamic(DynamicType::Todo(_))) = tcx.annotation {
@ -5603,16 +5608,52 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
items, items,
} = dict; } = dict;
// Evaluate the dictionary literal passed to the `TypedDict` constructor. let typed_dict = tcx.annotation.and_then(Type::into_typed_dict)?;
if let Some(Type::SpecialForm(SpecialFormType::TypedDict)) = tcx.annotation { let typed_dict_items = typed_dict.items(self.db());
for item in items {
self.infer_optional_expression(item.key.as_ref(), TypeContext::default());
if let Some(ast::Expr::StringLiteral(ref key)) = item.key
&& let Some(key) = key.as_single_part_string()
&& let Some(field) = typed_dict_items.get(key.as_str())
{
self.infer_expression(&item.value, TypeContext::new(Some(field.declared_ty)));
} else {
self.infer_expression(&item.value, TypeContext::default());
}
}
validate_typed_dict_dict_literal(&self.context, typed_dict, dict, dict.into(), |expr| {
self.expression_type(expr)
})
.ok()
.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<Type<'db>> {
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(); let mut typed_dict_items = FxOrderMap::default();
for item in items { for item in items {
let Some(Type::StringLiteral(key)) = let Some(Type::StringLiteral(key)) =
self.infer_optional_expression(item.key.as_ref(), TypeContext::default()) self.infer_optional_expression(item.key.as_ref(), TypeContext::default())
else { else {
// Emit a diagnostic here? We seem to support non-string literals. continue;
unimplemented!()
}; };
let field_ty = self.infer_typed_dict_field_type_expression(&item.value); let field_ty = self.infer_typed_dict_field_type_expression(&item.value);
@ -5641,35 +5682,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
typed_dict_items.insert(ast::name::Name::new(key.value(self.db())), field); typed_dict_items.insert(ast::name::Name::new(key.value(self.db())), field);
} }
// Create an incomplete synthesized `TypedDictType`, to be completed by the `TypedDict` // Create an anonymous `TypedDict` from the items, to be completed by the `TypedDict` constructor binding.
// constructor binding. Some(Type::TypedDict(TypedDictType::from_items(
return Some(Type::TypedDict(TypedDictType::from_items(
self.db(), self.db(),
typed_dict_items, typed_dict_items,
))); )))
}
let typed_dict = tcx.annotation.and_then(Type::into_typed_dict)?;
let typed_dict_items = typed_dict.items(self.db());
for item in items {
self.infer_optional_expression(item.key.as_ref(), TypeContext::default());
if let Some(ast::Expr::StringLiteral(ref key)) = item.key
&& let Some(key) = key.as_single_part_string()
&& let Some(field) = typed_dict_items.get(key.as_str())
{
self.infer_expression(&item.value, TypeContext::new(Some(field.declared_ty)));
} else {
self.infer_expression(&item.value, TypeContext::default());
}
}
validate_typed_dict_dict_literal(&self.context, typed_dict, dict, dict.into(), |expr| {
self.expression_type(expr)
})
.ok()
.map(|_| Type::TypedDict(typed_dict))
} }
fn infer_typed_dict_field_type_expression( fn infer_typed_dict_field_type_expression(

View File

@ -62,10 +62,10 @@ impl<'db> TypedDictType<'db> {
TypedDictType::FromClass(class) 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 /// 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<Name, Field<'db>>) -> Self { pub(crate) fn from_items(db: &'db dyn Db, items: FxOrderMap<Name, Field<'db>>) -> Self {
TypedDictType::Synthesized(SynthesizedTypedDictType::new( TypedDictType::Synthesized(SynthesizedTypedDictType::new(
db, db,
@ -397,7 +397,7 @@ impl<'db> TypedDictType<'db> {
#[derive(PartialOrd, Ord)] #[derive(PartialOrd, Ord)]
pub struct SynthesizedTypedDictType<'db> { pub struct SynthesizedTypedDictType<'db> {
// The dictionary literal passed to the `TypedDict` constructor is inferred as // The dictionary literal passed to the `TypedDict` constructor is inferred as
// a nameless `SynthesizedTypedDictType`. // an anonymous (incomplete) `SynthesizedTypedDictType`.
pub(crate) name: Option<Name>, pub(crate) name: Option<Name>,
pub(crate) params: TypedDictParams, pub(crate) params: TypedDictParams,