
There can be semi-cyclic inheritance patterns (e.g. recursive generics) that are not technically inheritance cycles, but that can cause us to hit Salsa query cycles in evaluating a type's MRO. Add fixed-point handling to these MRO-related queries so we don't panic on these cycles. The details of what queries we hit in what order in this case will change as we implement support for generics, but ultimately we will probably need cycle handling for all queries that can re-enter type inference, otherwise we are susceptible to small changes in query execution order causing panics. Fixes #14333 Further reduces the panicking set of seeds in #14737
4.2 KiB
Generic classes
PEP 695 syntax
TODO: Add a red_knot_extension
function that asserts whether a function or class is generic.
This is a generic class defined using PEP 695 syntax:
class C[T]: ...
A class that inherits from a generic class, and fills its type parameters with typevars, is generic:
# TODO: no error
# error: [non-subscriptable]
class D[U](C[U]): ...
A class that inherits from a generic class, but fills its type parameters with concrete types, is not generic:
# TODO: no error
# error: [non-subscriptable]
class E(C[int]): ...
A class that inherits from a generic class, and doesn't fill its type parameters at all, implicitly
uses the default value for the typevar. In this case, that default type is Unknown
, so F
inherits from C[Unknown]
and is not itself generic.
class F(C): ...
Legacy syntax
This is a generic class defined using the legacy syntax:
from typing import Generic, TypeVar
T = TypeVar("T")
# TODO: no error
# error: [invalid-base]
class C(Generic[T]): ...
A class that inherits from a generic class, and fills its type parameters with typevars, is generic.
class D(C[T]): ...
(Examples E
and F
from above do not have analogues in the legacy syntax.)
Inferring generic class parameters
The type parameter can be specified explicitly:
class C[T]:
x: T
# TODO: no error
# TODO: revealed: C[int]
# error: [non-subscriptable]
reveal_type(C[int]()) # revealed: C
We can infer the type parameter from a type context:
c: C[int] = C()
# TODO: revealed: C[int]
reveal_type(c) # revealed: C
The typevars of a fully specialized generic class should no longer be visible:
# TODO: revealed: int
reveal_type(c.x) # revealed: T
If the type parameter is not specified explicitly, and there are no constraints that let us infer a specific type, we infer the typevar's default type:
class D[T = int]: ...
# TODO: revealed: D[int]
reveal_type(D()) # revealed: D
If a typevar does not provide a default, we use Unknown
:
# TODO: revealed: C[Unknown]
reveal_type(C()) # revealed: C
If the type of a constructor parameter is a class typevar, we can use that to infer the type parameter:
class E[T]:
def __init__(self, x: T) -> None: ...
# TODO: revealed: E[int] or E[Literal[1]]
reveal_type(E(1)) # revealed: E
The types inferred from a type context and from a constructor parameter must be consistent with each other:
# TODO: error
wrong_innards: E[int] = E("five")
Generic subclass
When a generic subclass fills its superclass's type parameter with one of its own, the actual types propagate through:
class Base[T]:
x: T | None = None
# TODO: no error
# error: [non-subscriptable]
class Sub[U](Base[U]): ...
# TODO: no error
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Base[int].x) # revealed: T | None
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Sub[int].x) # revealed: T | None
Cyclic class definition
A class can use itself as the type parameter of one of its superclasses. (This is also known as the curiously recurring template pattern or F-bounded quantification.)
Here, Sub
is not a generic class, since it fills its superclass's type parameter (with itself).
stub.pyi
:
class Base[T]: ...
# TODO: no error
# error: [non-subscriptable]
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:
string_annotation.py
:
class Base[T]: ...
# TODO: no error
# error: [non-subscriptable]
class Sub(Base["Sub"]): ...
reveal_type(Sub) # revealed: Literal[Sub]
In a non-stub file, without stringified forward references, this raises a NameError
:
bare_annotation.py
:
class Base[T]: ...
# TODO: error: [unresolved-reference]
# error: [non-subscriptable]
class Sub(Base[Sub]): ...
Another cyclic case
# TODO no error (generics)
# error: [invalid-base]
class Derived[T](list[Derived[T]]): ...