[red-knot] fix detecting a metaclass on a not-explicitly-specialized generic base

This commit is contained in:
Carl Meyer 2025-04-24 18:19:03 -07:00
parent 327b913d68
commit 46a1fd3b3e
No known key found for this signature in database
GPG key ID: 2D1FB7916A52E121
4 changed files with 35 additions and 14 deletions

View file

@ -53,6 +53,25 @@ class B(A): ...
reveal_type(B.__class__) # revealed: Literal[M]
```
## Linear inheritance with PEP 695 generic class
The same is true if the base with the metaclass is a generic class.
```toml
[environment]
python-version = "3.13"
```
```py
class M(type): ...
class A[T](metaclass=M): ...
class B(A): ...
class C(A[int]): ...
reveal_type(B.__class__) # revealed: Literal[M]
reveal_type(C.__class__) # revealed: Literal[M]
```
## Conflict (1)
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its

View file

@ -688,20 +688,21 @@ impl<'db> Type<'db> {
matches!(self, Type::ClassLiteral(..))
}
pub const fn into_class_type(self) -> Option<ClassType<'db>> {
/// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`.
/// Since a `ClassType` must be specialized, apply the default specialization to any
/// unspecialized generic class literal.
pub fn to_class_type(self, db: &'db dyn Db) -> Option<ClassType<'db>> {
match self {
Type::ClassLiteral(ClassLiteralType::NonGeneric(non_generic)) => {
Some(ClassType::NonGeneric(non_generic))
}
Type::ClassLiteral(class_literal) => Some(class_literal.default_specialization(db)),
Type::GenericAlias(alias) => Some(ClassType::Generic(alias)),
_ => None,
}
}
#[track_caller]
pub fn expect_class_type(self) -> ClassType<'db> {
self.into_class_type()
.expect("Expected a Type::GenericAlias or non-generic Type::ClassLiteral variant")
pub fn expect_class_type(self, db: &'db dyn Db) -> ClassType<'db> {
self.to_class_type(db)
.expect("Expected a Type::GenericAlias or Type::ClassLiteral variant")
}
pub const fn is_class_type(&self) -> bool {

View file

@ -577,12 +577,13 @@ impl<'db> ClassLiteralType<'db> {
self.explicit_bases_query(db)
}
/// Iterate over this class's explicit bases, filtering out any bases that are not class objects.
/// Iterate over this class's explicit bases, filtering out any bases that are not class
/// objects, and applying default specialization to any unspecialized generic class literals.
fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator<Item = ClassType<'db>> {
self.explicit_bases(db)
.iter()
.copied()
.filter_map(Type::into_class_type)
.filter_map(|ty| ty.to_class_type(db))
}
#[salsa::tracked(return_ref, cycle_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial)]
@ -767,7 +768,7 @@ impl<'db> ClassLiteralType<'db> {
(KnownClass::Type.to_class_literal(db), self)
};
let mut candidate = if let Some(metaclass_ty) = metaclass.into_class_type() {
let mut candidate = if let Some(metaclass_ty) = metaclass.to_class_type(db) {
MetaclassCandidate {
metaclass: metaclass_ty,
explicit_metaclass_of: class_metaclass_was_from,
@ -809,7 +810,7 @@ impl<'db> ClassLiteralType<'db> {
// - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663
for base_class in base_classes {
let metaclass = base_class.metaclass(db);
let Some(metaclass) = metaclass.into_class_type() else {
let Some(metaclass) = metaclass.to_class_type(db) else {
continue;
};
if metaclass.is_subclass_of(db, candidate.metaclass) {
@ -2164,7 +2165,7 @@ impl<'db> KnownClass {
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> {
self.to_class_literal(db)
.into_class_type()
.to_class_type(db)
.map(Type::instance)
.unwrap_or_else(Type::unknown)
}
@ -2231,7 +2232,7 @@ impl<'db> KnownClass {
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Type<'db> {
self.to_class_literal(db)
.into_class_type()
.to_class_type(db)
.map(|class| SubclassOfType::from(db, class))
.unwrap_or_else(SubclassOfType::subclass_of_unknown)
}

View file

@ -62,7 +62,7 @@ impl<'db> ClassBase<'db> {
pub(super) fn object(db: &'db dyn Db) -> Self {
KnownClass::Object
.to_class_literal(db)
.into_class_type()
.to_class_type(db)
.map_or(Self::unknown(), Self::Class)
}