diff --git a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md index fc66cc1220..84499c766a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md +++ b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md @@ -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 diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index b230c6bab8..ab190dd448 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -688,20 +688,21 @@ impl<'db> Type<'db> { matches!(self, Type::ClassLiteral(..)) } - pub const fn into_class_type(self) -> Option> { + /// 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> { 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 { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 6f8465bbe8..f74e7d276f 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -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> { 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) } diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index fa5175a0ab..395ff5268d 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -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) }