Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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]: ...
Expand All @@ -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]: ...
Expand All @@ -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]: ...
Expand All @@ -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
19 changes: 19 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/metaclass.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 8 additions & 7 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 17 additions & 9 deletions crates/red_knot_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<GenericAlias<'db>> for Type<'db> {
Expand Down Expand Up @@ -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<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)]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1690,8 +1695,12 @@ impl<'db> ClassLiteralType<'db> {
visited_classes: &mut IndexSet<ClassLiteralType<'db>>,
) -> 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;
}
Expand All @@ -1705,7 +1714,6 @@ impl<'db> ClassLiteralType<'db> {
visited_classes,
);
}

classes_on_stack.pop();
}
result
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion crates/red_knot_python_semantic/src/types/class_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Loading