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
13 changes: 10 additions & 3 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 @@ -1690,8 +1694,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 +1713,6 @@ impl<'db> ClassLiteralType<'db> {
visited_classes,
);
}

classes_on_stack.pop();
}
result
Expand Down
Loading