diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md index 11d1d36b353df..82b2b8f7eb5b2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -275,14 +275,16 @@ c: C[int] = C[int]() reveal_type(c.method("string")) # revealed: Literal["string"] ``` -## Cyclic class definition +## Cyclic class definitions + +### F-bounded quantification A class can use itself as the type parameter of one of its superclasses. (This is also known as the [curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].) -Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself). +#### In a stub file -`stub.pyi`: +Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself). ```pyi class Base[T]: ... @@ -291,9 +293,9 @@ class Sub(Base[Sub]): ... reveal_type(Sub) # revealed: Literal[Sub] ``` -A similar case can work in a non-stub file, if forward references are stringified: +#### With string forward references -`string_annotation.py`: +A similar case can work in a non-stub file, if forward references are stringified: ```py class Base[T]: ... @@ -302,9 +304,9 @@ class Sub(Base["Sub"]): ... reveal_type(Sub) # revealed: Literal[Sub] ``` -In a non-stub file, without stringified forward references, this raises a `NameError`: +#### Without string forward references -`bare_annotation.py`: +In a non-stub file, without stringified forward references, this raises a `NameError`: ```py class Base[T]: ... @@ -313,11 +315,23 @@ class Base[T]: ... class Sub(Base[Sub]): ... ``` -## Another cyclic case +### Cyclic inheritance as a generic parameter ```pyi class Derived[T](list[Derived[T]]): ... ``` +### Direct cyclic inheritance + +Inheritance that would result in a cyclic MRO is detected as an error. + +```py +# error: [cyclic-class-definition] +class C[T](C): ... + +# error: [cyclic-class-definition] +class D[T](D[int]): ... +``` + [crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern [f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification diff --git a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md index fc66cc12207fe..84499c766ae9d 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 b230c6bab8472..ab190dd448568 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 9f7e2f37388d7..f74e7d276f071 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -174,6 +174,10 @@ impl<'db> GenericAlias<'db> { pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { self.origin(db).class(db).definition(db) } + + pub(crate) fn class_literal(self, db: &'db dyn Db) -> ClassLiteralType<'db> { + ClassLiteralType::Generic(self.origin(db)) + } } impl<'db> From> for Type<'db> { @@ -573,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)] @@ -763,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, @@ -805,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) { @@ -1690,8 +1695,12 @@ impl<'db> ClassLiteralType<'db> { visited_classes: &mut IndexSet>, ) -> bool { let mut result = false; - for explicit_base_class in class.fully_static_explicit_bases(db) { - let (explicit_base_class_literal, _) = explicit_base_class.class_literal(db); + for explicit_base in class.explicit_bases(db) { + let explicit_base_class_literal = match explicit_base { + Type::ClassLiteral(class_literal) => *class_literal, + Type::GenericAlias(generic_alias) => generic_alias.class_literal(db), + _ => continue, + }; if !classes_on_stack.insert(explicit_base_class_literal) { return true; } @@ -1705,7 +1714,6 @@ impl<'db> ClassLiteralType<'db> { visited_classes, ); } - classes_on_stack.pop(); } result @@ -2157,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) } @@ -2224,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 fa5175a0ab84f..395ff5268da16 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) }