[ty] Intersect with a dynamic type when calculating the metaclass of a class if that class has a dynamic type in its MRO

This commit is contained in:
Alex Waygood 2025-07-05 21:56:24 +01:00
parent 08d8819c8a
commit 2d04d99315
6 changed files with 35 additions and 13 deletions

View file

@ -1574,7 +1574,7 @@ class B(Any): ...
class C(B, A): ...
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'B'>, Any, <class 'A'>, <class 'object'>]
reveal_type(C.x) # revealed: Literal[1] & Any
reveal_type(C.x) # revealed: @Todo(Type::Intersection.call())
```
## Classes with custom `__getattr__` methods

View file

@ -158,15 +158,13 @@ from nonexistent_module import UnknownClass # error: [unresolved-import]
class C(UnknownClass): ...
# TODO: should be `type[type] & Unknown`
reveal_type(C.__class__) # revealed: <class 'type'>
reveal_type(C.__class__) # revealed: type[type] & Unknown
class M(type): ...
class A(metaclass=M): ...
class B(A, UnknownClass): ...
# TODO: should be `type[M] & Unknown`
reveal_type(B.__class__) # revealed: <class 'M'>
reveal_type(B.__class__) # revealed: type[M] & Unknown
```
## Duplicate
@ -176,7 +174,7 @@ class M(type): ...
class A(metaclass=M): ...
class B(A, A): ... # error: [duplicate-base] "Duplicate base class `A`"
reveal_type(B.__class__) # revealed: <class 'M'>
reveal_type(B.__class__) # revealed: type[M] & Unknown
```
## Non-class

View file

@ -639,16 +639,16 @@ python-version = "3.13"
```pyi
class C(C.a): ...
reveal_type(C.__class__) # revealed: <class 'type'>
reveal_type(C.__class__) # revealed: type[type] & Unknown
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, Unknown, <class 'object'>]
class D(D.a):
a: D
reveal_type(D.__class__) # revealed: <class 'type'>
reveal_type(D.__class__) # revealed: type[type] & Unknown
reveal_type(D.__mro__) # revealed: tuple[<class 'D'>, Unknown, <class 'object'>]
class E[T](E.a): ...
reveal_type(E.__class__) # revealed: <class 'type'>
reveal_type(E.__class__) # revealed: type[type] & Unknown
reveal_type(E.__mro__) # revealed: tuple[<class 'E[Unknown]'>, Unknown, typing.Generic, <class 'object'>]
class F[T](F(), F): ... # error: [cyclic-class-definition]

View file

@ -255,8 +255,14 @@ def test(x: Any):
static_assert(is_assignable_to(TypeOf[Bar], type[int]))
static_assert(is_assignable_to(TypeOf[Bar], type[Any]))
static_assert(not is_assignable_to(TypeOf[Foo], int))
static_assert(not is_assignable_to(TypeOf[Bar], int))
# since the metaclass of `Foo` is `Any`,
# and `Foo` is an instance of its metaclass,
# and `Any` could materialize to `<class 'int'>`,
# the object created by the class definition *could*
# theoretically be an `int` instance, in which case
# `TypeOf[Foo]` would be a subtype of `int`
static_assert(is_assignable_to(TypeOf[Foo], int))
static_assert(is_assignable_to(TypeOf[Bar], int))
```
This is because the `Any` element in the MRO could materialize to any subtype of `type`.

View file

@ -1246,9 +1246,20 @@ impl<'db> ClassLiteral<'db> {
});
}
let metaclass = if let Some(dynamic_element) =
self.iter_mro(db, None).find_map(ClassBase::into_dynamic)
{
IntersectionBuilder::new(db)
.add_positive(SubclassOfType::from(db, candidate.metaclass))
.add_positive(Type::Dynamic(dynamic_element))
.build()
} else {
Type::from(candidate.metaclass)
};
let (metaclass_literal, _) = candidate.metaclass.class_literal(db);
Ok((
candidate.metaclass.into(),
metaclass,
metaclass_literal.dataclass_transformer_params(db),
))
}

View file

@ -233,13 +233,20 @@ impl<'db> ClassBase<'db> {
}
}
pub(super) fn into_class(self) -> Option<ClassType<'db>> {
pub(super) const fn into_class(self) -> Option<ClassType<'db>> {
match self {
Self::Class(class) => Some(class),
Self::Dynamic(_) | Self::Generic | Self::Protocol => None,
}
}
pub(super) const fn into_dynamic(self) -> Option<DynamicType> {
match self {
Self::Dynamic(dynamic) => Some(dynamic),
Self::Class(_) | Self::Generic | Self::Protocol => None,
}
}
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
match self {
Self::Class(class) => Self::Class(class.apply_type_mapping(db, type_mapping)),