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,52 +5608,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
items, items,
} = dict; } = 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 = tcx.annotation.and_then(Type::into_typed_dict)?;
let typed_dict_items = typed_dict.items(self.db()); let typed_dict_items = typed_dict.items(self.db());
@ -5672,6 +5631,64 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.map(|_| Type::TypedDict(typed_dict)) .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();
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( fn infer_typed_dict_field_type_expression(
&mut self, &mut self,
expr: &ast::Expr, expr: &ast::Expr,

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,