Rename Red Knot (#17820)

This commit is contained in:
Micha Reiser 2025-05-03 19:49:15 +02:00 committed by GitHub
parent e6a798b962
commit b51c4f82ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1564 changed files with 1598 additions and 1578 deletions

View file

@ -0,0 +1,443 @@
# Generic classes: Legacy syntax
## Defining a generic class
At its simplest, to define a generic class using the legacy syntax, you inherit from the
`typing.Generic` special form, which is "specialized" with the generic class's type variables.
```py
from ty_extensions import generic_context
from typing import Generic, TypeVar
T = TypeVar("T")
S = TypeVar("S")
class SingleTypevar(Generic[T]): ...
class MultipleTypevars(Generic[T, S]): ...
reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T]
reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S]
```
You cannot use the same typevar more than once.
```py
# TODO: error
class RepeatedTypevar(Generic[T, T]): ...
```
You can only specialize `typing.Generic` with typevars (TODO: or param specs or typevar tuples).
```py
# error: [invalid-argument-type] "`Literal[int]` is not a valid argument to `typing.Generic`"
class GenericOfType(Generic[int]): ...
```
You can also define a generic class by inheriting from some _other_ generic class, and specializing
it with typevars.
```py
class InheritedGeneric(MultipleTypevars[T, S]): ...
class InheritedGenericPartiallySpecialized(MultipleTypevars[T, int]): ...
class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ...
reveal_type(generic_context(InheritedGeneric)) # revealed: tuple[T, S]
reveal_type(generic_context(InheritedGenericPartiallySpecialized)) # revealed: tuple[T]
reveal_type(generic_context(InheritedGenericFullySpecialized)) # revealed: None
```
If you don't specialize a generic base class, we use the default specialization, which maps each
typevar to its default value or `Any`. Since that base class is fully specialized, it does not make
the inheriting class generic.
```py
class InheritedGenericDefaultSpecialization(MultipleTypevars): ...
reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None
```
When inheriting from a generic class, you can optionally inherit from `typing.Generic` as well. But
if you do, you have to mention all of the typevars that you use in your other base classes.
```py
class ExplicitInheritedGeneric(MultipleTypevars[T, S], Generic[T, S]): ...
# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes"
class ExplicitInheritedGenericMissingTypevar(MultipleTypevars[T, S], Generic[T]): ...
class ExplicitInheritedGenericPartiallySpecialized(MultipleTypevars[T, int], Generic[T]): ...
class ExplicitInheritedGenericPartiallySpecializedExtraTypevar(MultipleTypevars[T, int], Generic[T, S]): ...
# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes"
class ExplicitInheritedGenericPartiallySpecializedMissingTypevar(MultipleTypevars[T, int], Generic[S]): ...
reveal_type(generic_context(ExplicitInheritedGeneric)) # revealed: tuple[T, S]
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecialized)) # revealed: tuple[T]
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTypevar)) # revealed: tuple[T, S]
```
## Specializing generic classes explicitly
The type parameter can be specified explicitly:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
x: T
reveal_type(C[int]()) # revealed: C[int]
```
The specialization must match the generic types:
```py
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]()) # revealed: Unknown
```
If the type variable has an upper bound, the specialized type must satisfy that bound:
```py
from typing import Union
BoundedT = TypeVar("BoundedT", bound=int)
BoundedByUnionT = TypeVar("BoundedByUnionT", bound=Union[int, str])
class Bounded(Generic[BoundedT]): ...
class BoundedByUnion(Generic[BoundedByUnionT]): ...
class IntSubclass(int): ...
reveal_type(Bounded[int]()) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `str`"
reveal_type(Bounded[str]()) # revealed: Unknown
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`"
reveal_type(Bounded[int | str]()) # revealed: Unknown
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass]
reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str]
reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str]
```
If the type variable is constrained, the specialized type must satisfy those constraints:
```py
ConstrainedT = TypeVar("ConstrainedT", int, str)
class Constrained(Generic[ConstrainedT]): ...
reveal_type(Constrained[int]()) # revealed: Constrained[int]
# TODO: error: [invalid-argument-type]
# TODO: revealed: Constrained[Unknown]
reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass]
reveal_type(Constrained[str]()) # revealed: Constrained[str]
# TODO: error: [invalid-argument-type]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int | str`, found `object`"
reveal_type(Constrained[object]()) # revealed: Unknown
```
## Inferring generic class parameters
We can infer the type parameter from a type context:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
x: T
c: C[int] = C()
# TODO: revealed: C[int]
reveal_type(c) # revealed: C[Unknown]
```
The typevars of a fully specialized generic class should no longer be visible:
```py
# TODO: revealed: int
reveal_type(c.x) # revealed: Unknown
```
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:
```py
DefaultT = TypeVar("DefaultT", default=int)
class D(Generic[DefaultT]): ...
reveal_type(D()) # revealed: D[int]
```
If a typevar does not provide a default, we use `Unknown`:
```py
reveal_type(C()) # revealed: C[Unknown]
```
## Inferring generic class parameters from constructors
If the type of a constructor parameter is a class typevar, we can use that to infer the type
parameter. The types inferred from a type context and from a constructor parameter must be
consistent with each other.
### `__new__` only
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
### `__init__` only
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
### Identical `__new__` and `__init__` signatures
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
### Compatible `__new__` and `__init__` signatures
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
def __new__(cls, *args, **kwargs) -> "C[T]":
return object.__new__(cls)
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
class D(Generic[T]):
def __new__(cls, x: T) -> "D[T]":
return object.__new__(cls)
def __init__(self, *args, **kwargs) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
wrong_innards: D[int] = D("five")
```
### Both present, `__new__` inherited from a generic base class
If either method comes from a generic base class, we don't currently use its inferred specialization
to specialize the class.
```py
from typing import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
V = TypeVar("V")
class C(Generic[T, U]):
def __new__(cls, *args, **kwargs) -> "C[T, U]":
return object.__new__(cls)
class D(C[V, int]):
def __init__(self, x: V) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
```
### `__init__` is itself generic
```py
from typing import Generic, TypeVar
S = TypeVar("S")
T = TypeVar("T")
class C(Generic[T]):
def __init__(self, x: T, y: S) -> None: ...
reveal_type(C(1, 1)) # revealed: C[Literal[1]]
reveal_type(C(1, "string")) # revealed: C[Literal[1]]
reveal_type(C(1, True)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five", 1)
```
## Generic subclass
When a generic subclass fills its superclass's type parameter with one of its own, the actual types
propagate through:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class Base(Generic[T]):
x: T | None = None
class ExplicitlyGenericSub(Base[T], Generic[T]): ...
class ImplicitlyGenericSub(Base[T]): ...
reveal_type(Base[int].x) # revealed: int | None
reveal_type(ExplicitlyGenericSub[int].x) # revealed: int | None
reveal_type(ImplicitlyGenericSub[int].x) # revealed: int | None
```
## Generic methods
Generic classes can contain methods that are themselves generic. The generic methods can refer to
the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in
scope for the method.
```py
from typing import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
class C(Generic[T]):
def method(self, u: U) -> U:
return u
c: C[int] = C[int]()
reveal_type(c.method("string")) # revealed: Literal["string"]
```
## 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].)
#### In a stub file
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
```pyi
from typing import Generic, TypeVar
T = TypeVar("T")
class Base(Generic[T]): ...
class Sub(Base[Sub]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
#### With string forward references
A similar case can work in a non-stub file, if forward references are stringified:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class Base(Generic[T]): ...
class Sub(Base["Sub"]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
#### Without string forward references
In a non-stub file, without stringified forward references, this raises a `NameError`:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class Base(Generic[T]): ...
# error: [unresolved-reference]
class Sub(Base[Sub]): ...
```
### Cyclic inheritance as a generic parameter
```pyi
from typing import Generic, TypeVar
T = TypeVar("T")
class Derived(list[Derived[T]], Generic[T]): ...
```
### Direct cyclic inheritance
Inheritance that would result in a cyclic MRO is detected as an error.
```py
from typing import Generic, TypeVar
T = TypeVar("T")
# error: [unresolved-reference]
class C(C, Generic[T]): ...
# error: [unresolved-reference]
class D(D[int], Generic[T]): ...
```
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification

View file

@ -0,0 +1,272 @@
# Generic functions: Legacy syntax
## Typevar must be used at least twice
If you're only using a typevar for a single parameter, you don't need the typevar — just use
`object` (or the typevar's upper bound):
```py
from typing import TypeVar
T = TypeVar("T")
# TODO: error, should be (x: object)
def typevar_not_needed(x: T) -> None:
pass
BoundedT = TypeVar("BoundedT", bound=int)
# TODO: error, should be (x: int)
def bounded_typevar_not_needed(x: BoundedT) -> None:
pass
```
Typevars are only needed if you use them more than once. For instance, to specify that two
parameters must both have the same type:
```py
def two_params(x: T, y: T) -> T:
return x
```
or to specify that a return value is the same as a parameter:
```py
def return_value(x: T) -> T:
return x
```
Each typevar must also appear _somewhere_ in the parameter list:
```py
def absurd() -> T:
# There's no way to construct a T!
raise ValueError("absurd")
```
## Inferring generic function parameter types
If the type of a generic function parameter is a typevar, then we can infer what type that typevar
is bound to at each call site.
```py
from typing import TypeVar
T = TypeVar("T")
def f(x: T) -> T:
return x
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f(1.0)) # revealed: float
reveal_type(f(True)) # revealed: Literal[True]
reveal_type(f("string")) # revealed: Literal["string"]
```
## Inferring “deep” generic parameter types
The matching up of call arguments and discovery of constraints on typevars can be a recursive
process for arbitrarily-nested generic types in parameters.
```py
from typing import TypeVar
T = TypeVar("T")
def f(x: list[T]) -> T:
return x[0]
# TODO: revealed: float
reveal_type(f([1.0, 2.0])) # revealed: Unknown
```
## Inferring a bound typevar
<!-- snapshot-diagnostics -->
```py
from typing import TypeVar
from typing_extensions import reveal_type
T = TypeVar("T", bound=int)
def f(x: T) -> T:
return x
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f(True)) # revealed: Literal[True]
# error: [invalid-argument-type]
reveal_type(f("string")) # revealed: Unknown
```
## Inferring a constrained typevar
<!-- snapshot-diagnostics -->
```py
from typing import TypeVar
from typing_extensions import reveal_type
T = TypeVar("T", int, None)
def f(x: T) -> T:
return x
reveal_type(f(1)) # revealed: int
reveal_type(f(True)) # revealed: int
reveal_type(f(None)) # revealed: None
# error: [invalid-argument-type]
reveal_type(f("string")) # revealed: Unknown
```
## Typevar constraints
If a type parameter has an upper bound, that upper bound constrains which types can be used for that
typevar. This effectively adds the upper bound as an intersection to every appearance of the typevar
in the function.
```py
from typing import TypeVar
T = TypeVar("T", bound=int)
def good_param(x: T) -> None:
reveal_type(x) # revealed: T
```
If the function is annotated as returning the typevar, this means that the upper bound is _not_
assignable to that typevar, since return types are contravariant. In `bad`, we can infer that
`x + 1` has type `int`. But `T` might be instantiated with a narrower type than `int`, and so the
return value is not guaranteed to be compatible for all `T: int`.
```py
def good_return(x: T) -> T:
return x
def bad_return(x: T) -> T:
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `int`"
return x + 1
```
## All occurrences of the same typevar have the same type
If a typevar appears multiple times in a function signature, all occurrences have the same type.
```py
from typing import TypeVar
T = TypeVar("T")
S = TypeVar("S")
def different_types(cond: bool, t: T, s: S) -> T:
if cond:
return t
else:
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `S`"
return s
def same_types(cond: bool, t1: T, t2: T) -> T:
if cond:
return t1
else:
return t2
```
## All occurrences of the same constrained typevar have the same type
The above is true even when the typevars are constrained. Here, both `int` and `str` have `__add__`
methods that are compatible with the return type, so the `return` expression is always well-typed:
```py
from typing import TypeVar
T = TypeVar("T", int, str)
def same_constrained_types(t1: T, t2: T) -> T:
# TODO: no error
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`"
return t1 + t2
```
This is _not_ the same as a union type, because of this additional constraint that the two
occurrences have the same type. In `unions_are_different`, `t1` and `t2` might have different types,
and an `int` and a `str` cannot be added together:
```py
def unions_are_different(t1: int | str, t2: int | str) -> int | str:
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`"
return t1 + t2
```
## Typevar inference is a unification problem
When inferring typevar assignments in a generic function call, we cannot simply solve constraints
eagerly for each parameter in turn. We must solve a unification problem involving all of the
parameters simultaneously.
```py
from typing import TypeVar
T = TypeVar("T")
def two_params(x: T, y: T) -> T:
return x
reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"]
reveal_type(two_params("a", 1)) # revealed: Literal["a", 1]
```
When one of the parameters is a union, we attempt to find the smallest specialization that satisfies
all of the constraints.
```py
def union_param(x: T | None) -> T:
if x is None:
raise ValueError
return x
reveal_type(union_param("a")) # revealed: Literal["a"]
reveal_type(union_param(1)) # revealed: Literal[1]
reveal_type(union_param(None)) # revealed: Unknown
```
```py
def union_and_nonunion_params(x: T | int, y: T) -> T:
return y
reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"]
reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"]
reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1]
reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1]
reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1]
```
```py
S = TypeVar("S")
def tuple_param(x: T | S, y: tuple[T, S]) -> tuple[T, S]:
return y
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
```
## Inferring nested generic function calls
We can infer type assignments in nested calls to multiple generic functions. If they use the same
type variable, we do not confuse the two; `T@f` and `T@g` have separate types in each example below.
```py
from typing import TypeVar
T = TypeVar("T")
def f(x: T) -> tuple[T, int]:
return (x, 1)
def g(x: T) -> T | None:
return x
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
```

View file

@ -0,0 +1,126 @@
# Legacy type variables
The tests in this file focus on how type variables are defined using the legacy notation. Most
_uses_ of type variables are tested in other files in this directory; we do not duplicate every test
for both type variable syntaxes.
Unless otherwise specified, all quotations come from the [Generics] section of the typing spec.
## Type variables
### Defining legacy type variables
> Generics can be parameterized by using a factory available in `typing` called `TypeVar`.
This was the only way to create type variables prior to PEP 695/Python 3.12. It is still available
in newer Python releases.
```py
from typing import TypeVar
T = TypeVar("T")
reveal_type(type(T)) # revealed: Literal[TypeVar]
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T.__name__) # revealed: Literal["T"]
```
### Directly assigned to a variable
> A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as
> part of a larger expression).
```py
from typing import TypeVar
T = TypeVar("T")
# TODO: no error
# error: [invalid-legacy-type-variable]
U: TypeVar = TypeVar("U")
# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable"
TestList = list[TypeVar("W")]
```
### `TypeVar` parameter must match variable name
> The argument to `TypeVar()` must be a string equal to the variable name to which it is assigned.
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)"
T = TypeVar("Q")
```
### No redefinition
> Type variables must not be redefined.
```py
from typing import TypeVar
T = TypeVar("T")
# TODO: error
T = TypeVar("T")
```
### Type variables with a default
Note that the `__default__` property is only available in Python ≥3.13.
```toml
[environment]
python-version = "3.13"
```
```py
from typing import TypeVar
T = TypeVar("T", default=int)
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
S = TypeVar("S")
reveal_type(S.__default__) # revealed: NoDefault
```
### Type variables with an upper bound
```py
from typing import TypeVar
T = TypeVar("T", bound=int)
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]
S = TypeVar("S")
reveal_type(S.__bound__) # revealed: None
```
### Type variables with constraints
```py
from typing import TypeVar
T = TypeVar("T", int, str)
reveal_type(T.__constraints__) # revealed: tuple[int, str]
S = TypeVar("S")
reveal_type(S.__constraints__) # revealed: tuple[()]
```
### Cannot have only one constraint
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should
> be at least two constraints, if any; specifying a single constraint is disallowed.
```py
from typing import TypeVar
# TODO: error: [invalid-type-variable-constraints]
T = TypeVar("T", int)
```
[generics]: https://typing.python.org/en/latest/spec/generics.html