Use deferred evaluation
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user