Use deferred evaluation

This commit is contained in:
Charlie Marsh
2026-01-02 13:18:02 -05:00
parent b624521633
commit fdd9a97aef
5 changed files with 381 additions and 97 deletions

View File

@@ -1297,6 +1297,9 @@ class Person(TypedDict):
reveal_type(Person.__total__) # revealed: Literal[True]
reveal_type(Person.__required_keys__) # revealed: tuple[Literal["age"], Literal["name"]]
reveal_type(Person.__optional_keys__) # revealed: tuple[()]
reveal_type(Person.__annotations__) # revealed: dict[Unknown, Unknown]
# Calling .keys() on __annotations__ should work.
reveal_type(Person.__annotations__.keys()) # revealed: dict_keys[Unknown, Unknown]
```
These attributes cannot be accessed on inhabitants:

View File

@@ -27,7 +27,7 @@ use crate::db::Db;
use crate::dunder_all::dunder_all_names;
use crate::place::{Definedness, Place, known_module_symbol};
use crate::types::call::arguments::{Expansion, is_expandable_type};
use crate::types::class::FunctionalTypedDictLiteral;
use crate::types::class::{FunctionalTypedDictFieldsEvaluation, FunctionalTypedDictLiteral};
use crate::types::class_base::ClassBase;
use crate::types::constraints::ConstraintSet;
use crate::types::diagnostic::{
@@ -1606,7 +1606,12 @@ impl<'db> Bindings<'db> {
})
.collect();
let typeddict = FunctionalTypedDictLiteral::new(db, name, fields);
let typeddict = FunctionalTypedDictLiteral::new(
db,
name,
None,
Some(FunctionalTypedDictFieldsEvaluation::Eager(fields)),
);
let return_type = Type::ClassLiteral(
ClassLiteral::FunctionalTypedDict(typeddict),
);
@@ -1623,69 +1628,69 @@ impl<'db> Bindings<'db> {
.map(|s| Name::new(s.value(db)));
// Check if fields_type is a TypingNamedTupleFieldsSchema (from literal inference).
let fields: Option<Box<[(Name, Type<'db>, Option<Type<'db>>)]>> =
if let Type::KnownInstance(
KnownInstanceType::TypingNamedTupleFieldsSchema(schema),
) = fields_type
{
// Extract fields from the schema.
Some(
schema
.fields(db)
.iter()
.map(|(name, ty)| (name.clone(), *ty, None))
.collect(),
)
} else {
// Fall back to extracting from a tuple type for the variable case:
// fields = (("x", int), ("y", str))
// NamedTuple("Foo", fields)
let extract_field = |field_tuple: &Type<'db>| -> Option<(
Name,
Type<'db>,
Option<Type<'db>>,
)> {
let field_spec =
field_tuple.exact_tuple_instance_spec(db)?;
let elements: Vec<_> =
field_spec.fixed_elements().collect();
if elements.len() != 2 {
return None;
#[expect(clippy::type_complexity)]
let fields: Option<
Box<[(Name, Type<'db>, Option<Type<'db>>)]>,
> = if let Type::KnownInstance(
KnownInstanceType::TypingNamedTupleFieldsSchema(schema),
) = fields_type
{
// Extract fields from the schema.
Some(
schema
.fields(db)
.iter()
.map(|(name, ty)| (name.clone(), *ty, None))
.collect(),
)
} else {
// Fall back to extracting from a tuple type for the variable case:
// fields = (("x", int), ("y", str))
// NamedTuple("Foo", fields)
let extract_field = |field_tuple: &Type<'db>| -> Option<(
Name,
Type<'db>,
Option<Type<'db>>,
)> {
let field_spec = field_tuple.exact_tuple_instance_spec(db)?;
let elements: Vec<_> = field_spec.fixed_elements().collect();
if elements.len() != 2 {
return None;
}
let field_name = elements[0]
.as_string_literal()
.map(|s| Name::new(s.value(db)))?;
let field_ty = elements[1];
let resolved_ty = match field_ty {
Type::ClassLiteral(class) => {
class.to_non_generic_instance(db)
}
let field_name = elements[0]
.as_string_literal()
.map(|s| Name::new(s.value(db)))?;
let field_ty = elements[1];
let resolved_ty = match field_ty {
Type::ClassLiteral(class) => {
class.to_non_generic_instance(db)
}
Type::GenericAlias(alias) => {
Type::instance(db, ClassType::Generic(*alias))
}
Type::SubclassOf(subclass_of) => {
match subclass_of.subclass_of() {
SubclassOfInner::Class(class) => {
Type::instance(db, class)
}
_ => *field_ty,
Type::GenericAlias(alias) => {
Type::instance(db, ClassType::Generic(*alias))
}
Type::SubclassOf(subclass_of) => {
match subclass_of.subclass_of() {
SubclassOfInner::Class(class) => {
Type::instance(db, class)
}
_ => *field_ty,
}
ty => *ty,
};
Some((field_name, resolved_ty, None))
}
ty => *ty,
};
fields_type.exact_tuple_instance_spec(db).and_then(
|tuple_spec| {
tuple_spec
.fixed_elements()
.map(extract_field)
.collect::<Option<Box<[_]>>>()
},
)
Some((field_name, resolved_ty, None))
};
fields_type
.exact_tuple_instance_spec(db)
.and_then(|tuple_spec| {
tuple_spec
.fixed_elements()
.map(extract_field)
.collect::<Option<Box<[_]>>>()
})
};
if let (Some(name), Some(fields)) = (name, fields) {
let namedtuple = FunctionalNamedTupleLiteral::new(db, name, fields);
overload.set_return_type(Type::ClassLiteral(

View File

@@ -664,7 +664,7 @@ pub enum ClassLiteral<'db> {
/// A namedtuple created via the functional form `namedtuple(name, fields)` or
/// `NamedTuple(name, fields)`.
FunctionalNamedTuple(FunctionalNamedTupleLiteral<'db>),
/// A TypedDict created via the functional form `TypedDict("Name", {"key": Type, ...})`.
/// A `TypedDict` created via the functional form `TypedDict("Name", {"key": Type, ...})`.
FunctionalTypedDict(FunctionalTypedDictLiteral<'db>),
}
@@ -1054,9 +1054,11 @@ impl<'db> ClassType<'db> {
) -> Option<(StmtClassLiteral<'db>, Option<Specialization<'db>>)> {
match self {
Self::NonGeneric(ClassLiteral::Stmt(stmt)) => Some((stmt, None)),
Self::NonGeneric(ClassLiteral::Functional(_))
| Self::NonGeneric(ClassLiteral::FunctionalNamedTuple(_))
| Self::NonGeneric(ClassLiteral::FunctionalTypedDict(_)) => None,
Self::NonGeneric(
ClassLiteral::Functional(_)
| ClassLiteral::FunctionalNamedTuple(_)
| ClassLiteral::FunctionalTypedDict(_),
) => None,
Self::Generic(generic) => Some((generic.origin(db), Some(generic.specialization(db)))),
}
}
@@ -1070,9 +1072,11 @@ impl<'db> ClassType<'db> {
) -> Option<(StmtClassLiteral<'db>, Option<Specialization<'db>>)> {
match self {
Self::NonGeneric(ClassLiteral::Stmt(stmt)) => Some((stmt, None)),
Self::NonGeneric(ClassLiteral::Functional(_))
| Self::NonGeneric(ClassLiteral::FunctionalNamedTuple(_))
| Self::NonGeneric(ClassLiteral::FunctionalTypedDict(_)) => None,
Self::NonGeneric(
ClassLiteral::Functional(_)
| ClassLiteral::FunctionalNamedTuple(_)
| ClassLiteral::FunctionalTypedDict(_),
) => None,
Self::Generic(generic) => Some((
generic.origin(db),
Some(
@@ -2114,9 +2118,11 @@ impl<'db> VarianceInferable<'db> for ClassType<'db> {
fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance {
match self {
Self::NonGeneric(ClassLiteral::Stmt(stmt)) => stmt.variance_of(db, typevar),
Self::NonGeneric(ClassLiteral::Functional(_))
| Self::NonGeneric(ClassLiteral::FunctionalNamedTuple(_))
| Self::NonGeneric(ClassLiteral::FunctionalTypedDict(_)) => TypeVarVariance::Bivariant,
Self::NonGeneric(
ClassLiteral::Functional(_)
| ClassLiteral::FunctionalNamedTuple(_)
| ClassLiteral::FunctionalTypedDict(_),
) => TypeVarVariance::Bivariant,
Self::Generic(generic) => generic.variance_of(db, typevar),
}
}
@@ -4676,10 +4682,10 @@ fn synthesize_namedtuple_class_member<'db>(
}
}
/// Synthesize a class member for a TypedDict.
/// Synthesize a class member for a `TypedDict`.
///
/// This is a shared implementation used by both declarative TypedDicts (class-based)
/// and functional TypedDicts (`TypedDict("Name", {...})`).
/// This is a shared implementation used by both declarative `TypedDict`s (class-based)
/// and functional `TypedDict`s (`TypedDict("Name", {...})`).
fn synthesize_typed_dict_class_member<'db>(
db: &'db dyn Db,
name: &str,
@@ -4707,14 +4713,12 @@ fn synthesize_typed_dict_class_member<'db>(
}
"__annotations__" => {
// dict mapping field names to their types.
Some(
KnownClass::Dict
.to_class_literal(db)
.as_class_literal()
.expect("dict should be a class literal")
.default_specialization(db)
.into(),
)
let dict_class = KnownClass::Dict
.to_class_literal(db)
.as_class_literal()
.expect("dict should be a class literal")
.default_specialization(db);
Some(Type::instance(db, dict_class))
}
"__total__" => {
// `__total__` is `True` if all fields are required, `False` otherwise.
@@ -5070,6 +5074,7 @@ fn synthesize_typed_dict_class_member<'db>(
.with_annotated_type(instance_ty),
];
#[expect(clippy::explicit_iter_loop)]
for (field_name, field) in items.iter() {
let mut param = Parameter::keyword_only(field_name.clone())
.with_annotated_type(field.declared_ty());
@@ -5136,6 +5141,7 @@ impl<'db> FunctionalNamedTupleLiteral<'db> {
/// Get the metaclass of this functional namedtuple.
///
/// Namedtuples always have `type` as their metaclass.
#[expect(clippy::unused_self)]
pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
KnownClass::Type.to_class_literal(db)
}
@@ -5159,7 +5165,7 @@ impl<'db> FunctionalNamedTupleLiteral<'db> {
/// Look up an instance member by name.
pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
// First check if it's one of the field names.
for (field_name, field_ty, _) in self.fields(db).iter() {
for (field_name, field_ty, _) in self.fields(db).as_ref() {
if field_name.as_str() == name {
return Place::bound(create_field_property(db, *field_ty)).into();
}
@@ -5182,7 +5188,7 @@ impl<'db> FunctionalNamedTupleLiteral<'db> {
}
// Check if it's a field name (returns a property descriptor).
for (field_name, field_ty, _) in self.fields(db).iter() {
for (field_name, field_ty, _) in self.fields(db).as_ref() {
if field_name.as_str() == name {
return Place::bound(create_field_property(db, *field_ty)).into();
}
@@ -5223,6 +5229,15 @@ impl<'db> FunctionalNamedTupleLiteral<'db> {
}
}
/// Whether fields are eagerly specified or lazily evaluated.
#[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub enum FunctionalTypedDictFieldsEvaluation<'db> {
/// Field types need to be lazily evaluated.
Lazy,
/// The fields are eagerly specified with their types already resolved.
Eager(Box<[(Name, Type<'db>, bool)]>),
}
/// A TypedDict created via the functional form `TypedDict("Name", {"key": Type, ...})`.
///
/// For example:
@@ -5239,22 +5254,157 @@ pub struct FunctionalTypedDictLiteral<'db> {
#[returns(ref)]
pub name: Name,
/// The fields as (name, type, is_required) tuples.
#[returns(ref)]
pub fields: Box<[(Name, Type<'db>, bool)]>,
/// The definition where this TypedDict was created. Used for lazy field type resolution.
definition: Option<Definition<'db>>,
/// The fields. Don't use this field directly; use the `fields()` method instead
/// (to evaluate any lazy fields).
_fields: Option<FunctionalTypedDictFieldsEvaluation<'db>>,
}
impl get_size2::GetSize for FunctionalTypedDictLiteral<'_> {}
fn lazy_fields_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
_typeddict: FunctionalTypedDictLiteral<'db>,
) -> Box<[(Name, Type<'db>, bool)]> {
Box::new([])
}
#[expect(clippy::borrowed_box)]
fn lazy_fields_cycle_recover<'db>(
_db: &'db dyn Db,
_cycle: &salsa::Cycle,
_previous: &Box<[(Name, Type<'db>, bool)]>,
current: Box<[(Name, Type<'db>, bool)]>,
_typeddict: FunctionalTypedDictLiteral<'db>,
) -> Box<[(Name, Type<'db>, bool)]> {
// Cycle recovery for recursive `TypedDict`s. The `current` value contains the fields
// resolved so far, which may include self-references that resolved to the `TypedDict`
// type being defined. This is the correct behavior for recursive types like:
// `RecursiveMovie = TypedDict("RecursiveMovie", {"predecessor": "RecursiveMovie"})`
current
}
#[salsa::tracked]
impl<'db> FunctionalTypedDictLiteral<'db> {
/// Get the metaclass of this functional TypedDict.
/// Get the resolved fields of this `TypedDict`.
///
/// TypedDicts always have `type` as their metaclass.
/// Returns the fields as `(name, type, is_required)` tuples.
/// If fields are lazily evaluated, this will trigger resolution.
pub(crate) fn fields(self, db: &'db dyn Db) -> Box<[(Name, Type<'db>, bool)]> {
match self._fields(db) {
Some(FunctionalTypedDictFieldsEvaluation::Eager(fields)) => fields,
Some(FunctionalTypedDictFieldsEvaluation::Lazy) => self.lazy_fields(db),
None => Box::new([]),
}
}
#[salsa::tracked(
cycle_fn=lazy_fields_cycle_recover,
cycle_initial=lazy_fields_cycle_initial,
heap_size=ruff_memory_usage::heap_size
)]
fn lazy_fields(self, db: &'db dyn Db) -> Box<[(Name, Type<'db>, bool)]> {
let Some(definition) = self.definition(db) else {
return Box::new([]);
};
let module = parsed_module(db, definition.file(db)).load(db);
let DefinitionKind::Assignment(assignment) = definition.kind(db) else {
return Box::new([]);
};
let value = assignment.value(&module);
let ast::Expr::Call(call_expr) = value else {
return Box::new([]);
};
// Determine the `total` parameter value (default is true).
let is_total = call_expr
.arguments
.find_keyword("total")
.map(|kw| {
let ty = definition_expression_type(db, definition, &kw.value);
ty.bool(db).is_always_true()
})
.unwrap_or(true);
// Find the fields argument (second positional argument or 'fields' keyword).
let fields_arg = call_expr.arguments.find_positional(1).or_else(|| {
call_expr
.arguments
.find_keyword("fields")
.map(|kw| &kw.value)
});
let Some(ast::Expr::Dict(dict_expr)) = fields_arg else {
return Box::new([]);
};
// Build fields by extracting names and types from the dict literal.
let mut fields: Vec<(Name, Type<'db>, bool)> = Vec::with_capacity(dict_expr.items.len());
for item in &dict_expr.items {
// Each key should be a string literal.
let Some(key_expr) = &item.key else {
continue;
};
let key_ty = definition_expression_type(db, definition, key_expr);
let Some(key_lit) = key_ty.as_string_literal() else {
continue;
};
let field_name = Name::new(key_lit.value(db));
// Get the field type.
let field_ty = definition_expression_type(db, definition, &item.value);
// Determine is_required by examining the AST for Required/NotRequired syntax.
let is_required = detect_required_from_ast(&item.value).unwrap_or(is_total);
fields.push((field_name, field_ty, is_required));
}
fields.into_boxed_slice()
}
}
/// Detect whether an expression has `Required` or `NotRequired` qualifier syntactically.
/// Returns `Some(true)` for `Required`, `Some(false)` for `NotRequired`, `None` otherwise.
fn detect_required_from_ast(expr: &ast::Expr) -> Option<bool> {
// Check for Required[...] or NotRequired[...] syntax.
let ast::Expr::Subscript(subscript) = expr else {
return None;
};
let qualifier_name = match subscript.value.as_ref() {
ast::Expr::Attribute(attr) => Some(attr.attr.id()),
ast::Expr::Name(name) => Some(name.id()),
_ => None,
}?;
if qualifier_name == "Required" {
Some(true)
} else if qualifier_name == "NotRequired" {
Some(false)
} else {
None
}
}
impl<'db> FunctionalTypedDictLiteral<'db> {
/// Get the metaclass of this functional `TypedDict`.
///
/// `TypedDict`s always have `type` as their metaclass.
#[expect(clippy::unused_self)]
pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
KnownClass::Type.to_class_literal(db)
}
/// Compute the dict base type that this TypedDict inherits from.
/// Compute the dict base type that this `TypedDict` inherits from.
#[expect(clippy::unused_self)]
pub(crate) fn dict_base_type(self, db: &'db dyn Db) -> ClassType<'db> {
KnownClass::Dict
.to_class_literal(db)
@@ -5266,7 +5416,7 @@ impl<'db> FunctionalTypedDictLiteral<'db> {
/// Look up an instance member by name.
pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
// First check if it's one of the field names.
for (field_name, field_ty, _is_required) in self.fields(db).iter() {
for (field_name, field_ty, _is_required) in self.fields(db).as_ref() {
if field_name.as_str() == name {
return Place::bound(*field_ty).into();
}
@@ -5301,7 +5451,7 @@ impl<'db> FunctionalTypedDictLiteral<'db> {
.class_member(db, name, policy)
}
/// Generate synthesized class members for TypedDicts.
/// Generate synthesized class members for `TypedDict`s.
fn synthesized_class_member(self, db: &'db dyn Db, name: &str) -> Option<Type<'db>> {
let typed_dict_type = TypedDictType::new(ClassType::NonGeneric(self.into()));
let items = typed_dict_type.items(db);
@@ -5309,14 +5459,14 @@ impl<'db> FunctionalTypedDictLiteral<'db> {
synthesize_typed_dict_class_member(db, name, instance_ty, items)
}
/// Returns an instance type for this functional TypedDict.
/// Returns an instance type for this functional `TypedDict`.
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> {
Type::instance(db, ClassType::NonGeneric(self.into()))
}
/// Create a `Type::TypedDict` instance type from this functional TypedDict.
/// Create a `Type::TypedDict` instance type from this functional `TypedDict`.
///
/// This creates a `TypedDictType::Class` variant, which allows TypedDict operations
/// This creates a `TypedDictType::Class` variant, which allows `TypedDict` operations
/// like subscript access to work correctly via synthesized `__getitem__`.
pub(crate) fn to_typed_dict_type(self, _db: &'db dyn Db) -> Type<'db> {
Type::typed_dict(ClassType::NonGeneric(self.into()))

View File

@@ -822,7 +822,20 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
},
Type::SpecialForm(special_form) => {
f.set_invalid_type_annotation();
write!(f.with_type(self.ty), "<special-form '{special_form}'>")
// Display internal schema types with user-friendly names.
match special_form {
SpecialFormType::TypedDictFieldsSchema => {
f.with_type(self.ty).write_str("dict[str, type]")
}
SpecialFormType::TypingNamedTupleFieldsSchema
| SpecialFormType::CollectionsNamedTupleFieldsSchema => {
f.with_type(self.ty).write_str("Iterable[tuple[str, type]]")
}
SpecialFormType::CollectionsNamedTupleDefaultsSchema => {
f.with_type(self.ty).write_str("Iterable[object]")
}
_ => write!(f.with_type(self.ty), "<special-form '{special_form}'>"),
}
}
Type::KnownInstance(known_instance) => known_instance
.display_with(self.db, self.settings.clone())

View File

@@ -53,7 +53,8 @@ use crate::subscript::{PyIndex, PySlice};
use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex};
use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind};
use crate::types::class::{
ClassLiteral, CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator,
ClassLiteral, CodeGeneratorKind, FieldKind, FunctionalTypedDictFieldsEvaluation,
FunctionalTypedDictLiteral, MetaclassErrorKind, MethodDecorator,
};
use crate::types::context::{InNoTypeCheck, InferContext};
use crate::types::cyclic::CycleDetector;
@@ -5257,7 +5258,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_newtype_expression(target, call_expr, definition)
}
Some(_) | None => {
self.infer_call_expression_impl(call_expr, callable_type, tcx)
if callable_type.as_special_form() == Some(SpecialFormType::TypedDict) {
self.infer_functional_typeddict(call_expr, definition)
} else {
self.infer_call_expression_impl(call_expr, callable_type, tcx)
}
}
};
@@ -5788,6 +5793,86 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
)))
}
/// Handle functional `TypedDict` creation: `Movie = TypedDict("Movie", {"name": str, ...})`.
///
/// This method creates a `FunctionalTypedDictLiteral` with lazy field type resolution
/// to avoid cycles when dealing with recursive `TypedDict`s.
fn infer_functional_typeddict(
&mut self,
call_expr: &ast::ExprCall,
definition: Definition<'db>,
) -> Type<'db> {
let db = self.db();
let arguments = &call_expr.arguments;
// Get the dict literal from the second argument (positional or keyword "fields").
// Check this first, before inferring any expressions, so we can fall back cleanly.
let fields_arg = arguments
.find_positional(1)
.or_else(|| arguments.find_keyword("fields").map(|kw| &kw.value));
let Some(ast::Expr::Dict(dict_expr)) = fields_arg else {
// Fields argument is not a dict literal (e.g., dict() call or kwargs),
// fall back to normal call inference.
return self.infer_call_expression_impl(
call_expr,
Type::SpecialForm(SpecialFormType::TypedDict),
TypeContext::default(),
);
};
// Check for dict unpacking and non-string-literal keys before inferring any expressions.
// This ensures we can fall back cleanly without double-inference issues.
for item in &dict_expr.items {
match &item.key {
// Dict unpacking (`**other`) is not supported.
None => {
return self.infer_call_expression_impl(
call_expr,
Type::SpecialForm(SpecialFormType::TypedDict),
TypeContext::default(),
);
}
// Keys must be string literals.
Some(key) if !matches!(key, ast::Expr::StringLiteral(_)) => {
return self.infer_call_expression_impl(
call_expr,
Type::SpecialForm(SpecialFormType::TypedDict),
TypeContext::default(),
);
}
_ => {}
}
}
// Extract the name from the first argument.
let name_ty = arguments
.find_positional(0)
.map(|arg| self.infer_expression(arg, TypeContext::default()));
let Some(name) = name_ty.and_then(|ty| ty.as_string_literal().map(|s| s.value(db))) else {
// Name is not a valid string literal, fall back to normal call inference.
return self.infer_call_expression_impl(
call_expr,
Type::SpecialForm(SpecialFormType::TypedDict),
TypeContext::default(),
);
};
// Mark for deferred evaluation since field types need to be resolved later.
self.deferred.insert(definition, self.multi_inference_state);
// Create the TypedDict literal with lazy fields.
let typeddict = FunctionalTypedDictLiteral::new(
db,
ast::name::Name::new(name),
Some(definition),
Some(FunctionalTypedDictFieldsEvaluation::Lazy),
);
Type::ClassLiteral(ClassLiteral::FunctionalTypedDict(typeddict))
}
/// Extract fields from a list or tuple literal for `typing.NamedTuple`.
fn infer_typing_namedtuple_fields_schema(
&mut self,
@@ -5887,7 +5972,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
fn infer_assignment_deferred(&mut self, value: &ast::Expr) {
// Infer deferred bounds/constraints/defaults of a legacy TypeVar / ParamSpec / NewType.
// Infer deferred bounds/constraints/defaults of a legacy TypeVar / ParamSpec / NewType / TypedDict.
let ast::Expr::Call(ast::ExprCall {
func, arguments, ..
}) = value
@@ -5897,6 +5982,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let func_ty = self
.try_expression_type(func)
.unwrap_or_else(|| self.infer_expression(func, TypeContext::default()));
if func_ty.as_special_form() == Some(SpecialFormType::TypedDict) {
self.infer_functional_typeddict_deferred(arguments);
return;
}
let known_class = func_ty
.as_class_literal()
.and_then(|cls| cls.known(self.db()));
@@ -5922,6 +6013,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
/// Infer field types for a functional `TypedDict` in the deferred pass.
fn infer_functional_typeddict_deferred(&mut self, arguments: &ast::Arguments) {
// Get the dict literal from the second argument (positional or keyword "fields").
let fields_arg = arguments
.find_positional(1)
.or_else(|| arguments.find_keyword("fields").map(|kw| &kw.value));
let Some(ast::Expr::Dict(dict_expr)) = fields_arg else {
return;
};
// Infer field types as annotation expressions.
for item in &dict_expr.items {
// Infer key as a regular expression.
if let Some(key) = &item.key {
self.infer_expression(key, TypeContext::default());
}
// Infer value as an annotation expression to capture Required/NotRequired qualifiers.
self.infer_annotation_expression(&item.value, DeferredExpressionState::Deferred);
}
}
// Infer the deferred base type of a NewType.
fn infer_newtype_assignment_deferred(&mut self, arguments: &ast::Arguments) {
let inferred = self.infer_type_expression(&arguments.args[1]);