mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 06:11:21 +00:00
[ty] linear variance inference for PEP-695 type parameters (#18713)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
## Summary Implement linear-time variance inference for type variables (https://github.com/astral-sh/ty/issues/488). Inspired by Martin Huschenbett's [PyCon 2025 Talk](https://www.youtube.com/watch?v=7uixlNTOY4s&t=9705s). ## Test Plan update tests, add new tests, including for mutually recursive classes --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
656fc335f2
commit
33030b34cd
12 changed files with 1088 additions and 95 deletions
|
@ -237,10 +237,15 @@ If the type of a constructor parameter is a class typevar, we can use that to in
|
|||
parameter. The types inferred from a type context and from a constructor parameter must be
|
||||
consistent with each other.
|
||||
|
||||
We have to add `x: T` to the classes to ensure they're not bivariant in `T` (__new__ and __init__
|
||||
signatures don't count towards variance).
|
||||
|
||||
### `__new__` only
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
|
@ -254,6 +259,8 @@ wrong_innards: C[int] = C("five")
|
|||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
@ -266,6 +273,8 @@ wrong_innards: C[int] = C("five")
|
|||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
|
@ -281,6 +290,8 @@ wrong_innards: C[int] = C("five")
|
|||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __new__(cls, *args, **kwargs) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
|
@ -292,6 +303,8 @@ reveal_type(C(1)) # revealed: C[int]
|
|||
wrong_innards: C[int] = C("five")
|
||||
|
||||
class D[T]:
|
||||
x: T
|
||||
|
||||
def __new__(cls, x: T) -> "D[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
|
@ -378,6 +391,8 @@ def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: t
|
|||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __init__[S](self, x: T, y: S) -> None: ...
|
||||
|
||||
reveal_type(C(1, 1)) # revealed: C[int]
|
||||
|
@ -395,6 +410,10 @@ from __future__ import annotations
|
|||
from typing import overload
|
||||
|
||||
class C[T]:
|
||||
# we need to use the type variable or else the class is bivariant in T, and
|
||||
# specializations become meaningless
|
||||
x: T
|
||||
|
||||
@overload
|
||||
def __init__(self: C[str], x: str) -> None: ...
|
||||
@overload
|
||||
|
|
|
@ -40,8 +40,6 @@ class C[T]:
|
|||
class D[U](C[U]):
|
||||
pass
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[B], C[A]))
|
||||
static_assert(not is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
|
@ -49,8 +47,6 @@ static_assert(is_assignable_to(C[B], C[Any]))
|
|||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[B], C[A]))
|
||||
static_assert(not is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
|
@ -58,8 +54,6 @@ static_assert(is_assignable_to(D[B], C[Any]))
|
|||
static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
|
@ -67,8 +61,6 @@ static_assert(not is_subtype_of(C[B], C[Any]))
|
|||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[B], C[A]))
|
||||
static_assert(not is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
|
@ -124,8 +116,6 @@ class D[U](C[U]):
|
|||
pass
|
||||
|
||||
static_assert(not is_assignable_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
static_assert(is_assignable_to(C[B], C[Any]))
|
||||
|
@ -133,8 +123,6 @@ static_assert(is_assignable_to(C[Any], C[A]))
|
|||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_assignable_to(D[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
static_assert(is_assignable_to(D[B], C[Any]))
|
||||
|
@ -142,8 +130,6 @@ static_assert(is_assignable_to(D[Any], C[A]))
|
|||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
|
@ -151,8 +137,6 @@ static_assert(not is_subtype_of(C[Any], C[A]))
|
|||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(D[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
||||
|
@ -297,34 +281,22 @@ class C[T]:
|
|||
class D[U](C[U]):
|
||||
pass
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[B], C[A]))
|
||||
static_assert(is_subtype_of(C[A], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
static_assert(is_assignable_to(D[B], C[Any]))
|
||||
static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
|
@ -332,11 +304,7 @@ static_assert(not is_subtype_of(C[Any], C[A]))
|
|||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
static_assert(not is_subtype_of(C[Any], C[Any]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
||||
|
@ -345,23 +313,11 @@ static_assert(not is_subtype_of(D[Any], C[B]))
|
|||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[A], C[B]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[A], C[Any]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[B], C[Any]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[Any], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_equivalent_to(D[A], C[A]))
|
||||
|
@ -380,4 +336,504 @@ static_assert(not is_equivalent_to(D[Any], C[Any]))
|
|||
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
|
||||
```
|
||||
|
||||
## Mutual Recursion
|
||||
|
||||
This example due to Martin Huschenbett's PyCon 2025 talk,
|
||||
[Linear Time variance Inference for PEP 695][linear-time-variance-talk]
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[X]:
|
||||
def f(self) -> "D[X]":
|
||||
return D()
|
||||
|
||||
def g(self, x: X) -> None: ...
|
||||
|
||||
class D[Y]:
|
||||
def h(self) -> C[Y]:
|
||||
return C()
|
||||
```
|
||||
|
||||
`C` is contravariant in `X`, and `D` in `Y`:
|
||||
|
||||
- `C` has two occurrences of `X`
|
||||
- `X` occurs in the return type of `f` as `D[X]` (`X` is substituted in for `Y`)
|
||||
- `D` has one occurrence of `Y`
|
||||
- `Y` occurs in the return type of `h` as `C[Y]`
|
||||
- `X` occurs contravariantly as a parameter in `g`
|
||||
|
||||
Thus the variance of `X` in `C` depends on itself. We want to infer the least restrictive possible
|
||||
variance, so in such cases we begin by assuming that the point where we detect the cycle is
|
||||
bivariant.
|
||||
|
||||
If we thus assume `X` is bivariant in `C`, then `Y` will be bivariant in `D`, as `D`'s only
|
||||
occurrence of `Y` is in `C`. Then we consider `X` in `C` once more. We have two occurrences: `D[X]`
|
||||
covariantly in a return type, and `X` contravariantly in an argument type. With one bivariant and
|
||||
one contravariant occurrence, we update our inference of `X` in `C` to contravariant---the supremum
|
||||
of contravariant and bivariant in the lattice.
|
||||
|
||||
Now that we've updated the variance of `X` in `C`, we re-evaluate `Y` in `D`. It only has the one
|
||||
occurrence `C[Y]`, which we now infer is contravariant, and so we infer contravariance for `Y` in
|
||||
`D` as well.
|
||||
|
||||
Because the variance of `X` in `C` depends on that of `Y` in `D`, we have to re-evaluate now that
|
||||
we've updated the latter to contravariant. The variance of `X` in `C` is now the supremum of
|
||||
contravariant and contravariant---giving us contravariant---and so remains unchanged.
|
||||
|
||||
Once we've completed a turn around the cycle with nothing changed, we've reached a fixed-point---the
|
||||
variance inference will not change any further---and so we finally conclude that both `X` in `C` and
|
||||
`Y` in `D` are contravariant.
|
||||
|
||||
```py
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(D[B], D[A]))
|
||||
static_assert(is_subtype_of(D[A], D[B]))
|
||||
static_assert(not is_subtype_of(D[A], D[Any]))
|
||||
static_assert(not is_subtype_of(D[B], D[Any]))
|
||||
static_assert(not is_subtype_of(D[Any], D[A]))
|
||||
static_assert(not is_subtype_of(D[Any], D[B]))
|
||||
```
|
||||
|
||||
## Class Attributes
|
||||
|
||||
### Mutable Attributes
|
||||
|
||||
Normal attributes are mutable, and so make the enclosing class invariant in this typevar (see
|
||||
[inv]).
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
```
|
||||
|
||||
One might think that occurrences in the types of normal attributes are covariant, but they are
|
||||
mutable, and thus the occurrences are invariant.
|
||||
|
||||
### Immutable Attributes
|
||||
|
||||
Immutable attributes can't be written to, and thus constrain the typevar to covariance, not
|
||||
invariance.
|
||||
|
||||
#### Final attributes
|
||||
|
||||
```py
|
||||
from typing import Final
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
x: Final[T]
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
```
|
||||
|
||||
#### Underscore-prefixed attributes
|
||||
|
||||
Underscore-prefixed instance attributes are considered private, and thus are assumed not externally
|
||||
mutated.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
_x: T
|
||||
|
||||
@property
|
||||
def x(self) -> T:
|
||||
return self._x
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
|
||||
class D[T]:
|
||||
def __init__(self, x: T):
|
||||
self._x = x
|
||||
|
||||
@property
|
||||
def x(self) -> T:
|
||||
return self._x
|
||||
|
||||
static_assert(is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
#### Frozen dataclasses in Python 3.12 and earlier
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass, field
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class D[U]:
|
||||
y: U
|
||||
|
||||
static_assert(is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class E[U]:
|
||||
y: U = field()
|
||||
|
||||
static_assert(is_subtype_of(E[B], E[A]))
|
||||
static_assert(not is_subtype_of(E[A], E[B]))
|
||||
```
|
||||
|
||||
#### Frozen dataclasses in Python 3.13 and later
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
Python 3.13 introduced a new synthesized `__replace__` method on dataclasses, which uses every field
|
||||
type in a contravariant position (as a parameter to `__replace__`). This means that frozen
|
||||
dataclasses on Python 3.13+ can't be covariant in their field types.
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class D[U]:
|
||||
y: U
|
||||
|
||||
static_assert(not is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
#### NamedTuple
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class E[V](NamedTuple):
|
||||
z: V
|
||||
|
||||
static_assert(is_subtype_of(E[B], E[A]))
|
||||
static_assert(not is_subtype_of(E[A], E[B]))
|
||||
```
|
||||
|
||||
A subclass of a `NamedTuple` can still be covariant:
|
||||
|
||||
```py
|
||||
class D[T](E[T]):
|
||||
pass
|
||||
|
||||
static_assert(is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
But adding a new generic attribute on the subclass makes it invariant (the added attribute is not a
|
||||
`NamedTuple` field, and thus not immutable):
|
||||
|
||||
```py
|
||||
class C[T](E[T]):
|
||||
w: T
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
Properties constrain to covariance if they are get-only and invariant if they are get-set:
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_subtype_of
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
@property
|
||||
def x(self) -> T | None:
|
||||
return None
|
||||
|
||||
class D[U]:
|
||||
@property
|
||||
def y(self) -> U | None:
|
||||
return None
|
||||
|
||||
@y.setter
|
||||
def y(self, value: U): ...
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
### Implicit Attributes
|
||||
|
||||
Implicit attributes work like normal ones
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_subtype_of
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
def f(self) -> None:
|
||||
self.x: T | None = None
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
```
|
||||
|
||||
### Constructors: excluding `__init__` and `__new__`
|
||||
|
||||
We consider it invalid to call `__init__` explicitly on an existing object. Likewise, `__new__` is
|
||||
only used at the beginning of an object's life. As such, we don't need to worry about the variance
|
||||
impact of these methods.
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_subtype_of
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
def __init__(self, x: T): ...
|
||||
def __new__(self, x: T): ...
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(is_subtype_of(C[A], C[B]))
|
||||
```
|
||||
|
||||
This example is then bivariant because it doesn't use `T` outside of the two exempted methods.
|
||||
|
||||
This holds likewise for dataclasses with synthesized `__init__`:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(init=True, frozen=True)
|
||||
class D[T]:
|
||||
x: T
|
||||
|
||||
# Covariant due to the read-only T-typed attribute; the `__init__` is ignored and doesn't make it
|
||||
# invariant:
|
||||
|
||||
static_assert(is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
## Union Types
|
||||
|
||||
Union types are covariant in all their members. If `A <: B`, then `A | C <: B | C` and
|
||||
`C | A <: C | B`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C: ...
|
||||
|
||||
# Union types are covariant in their members
|
||||
static_assert(is_subtype_of(B | C, A | C))
|
||||
static_assert(is_subtype_of(C | B, C | A))
|
||||
static_assert(not is_subtype_of(A | C, B | C))
|
||||
static_assert(not is_subtype_of(C | A, C | B))
|
||||
|
||||
# Assignability follows the same pattern
|
||||
static_assert(is_assignable_to(B | C, A | C))
|
||||
static_assert(is_assignable_to(C | B, C | A))
|
||||
static_assert(not is_assignable_to(A | C, B | C))
|
||||
static_assert(not is_assignable_to(C | A, C | B))
|
||||
```
|
||||
|
||||
## Intersection Types
|
||||
|
||||
Intersection types cannot be expressed directly in Python syntax, but they occur when type narrowing
|
||||
creates constraints through control flow. In ty's representation, intersection types are covariant
|
||||
in their positive conjuncts and contravariant in their negative conjuncts.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_subtype_of, static_assert, Intersection, Not
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C: ...
|
||||
|
||||
# Test covariance in positive conjuncts
|
||||
# If B <: A, then Intersection[X, B] <: Intersection[X, A]
|
||||
static_assert(is_subtype_of(Intersection[C, B], Intersection[C, A]))
|
||||
static_assert(not is_subtype_of(Intersection[C, A], Intersection[C, B]))
|
||||
|
||||
static_assert(is_assignable_to(Intersection[C, B], Intersection[C, A]))
|
||||
static_assert(not is_assignable_to(Intersection[C, A], Intersection[C, B]))
|
||||
|
||||
# Test contravariance in negative conjuncts
|
||||
# If B <: A, then Intersection[X, Not[A]] <: Intersection[X, Not[B]]
|
||||
# (excluding supertype A is more restrictive than excluding subtype B)
|
||||
static_assert(is_subtype_of(Intersection[C, Not[A]], Intersection[C, Not[B]]))
|
||||
static_assert(not is_subtype_of(Intersection[C, Not[B]], Intersection[C, Not[A]]))
|
||||
|
||||
static_assert(is_assignable_to(Intersection[C, Not[A]], Intersection[C, Not[B]]))
|
||||
static_assert(not is_assignable_to(Intersection[C, Not[B]], Intersection[C, Not[A]]))
|
||||
```
|
||||
|
||||
## Subclass Types (type[T])
|
||||
|
||||
The `type[T]` construct represents the type of classes that are subclasses of `T`. It is covariant
|
||||
in `T` because if `A <: B`, then `type[A] <: type[B]` holds.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
# type[T] is covariant in T
|
||||
static_assert(is_subtype_of(type[B], type[A]))
|
||||
static_assert(not is_subtype_of(type[A], type[B]))
|
||||
|
||||
static_assert(is_assignable_to(type[B], type[A]))
|
||||
static_assert(not is_assignable_to(type[A], type[B]))
|
||||
|
||||
# With generic classes using type[T]
|
||||
class ClassContainer[T]:
|
||||
def __init__(self, cls: type[T]) -> None:
|
||||
self.cls = cls
|
||||
|
||||
def create_instance(self) -> T:
|
||||
return self.cls()
|
||||
|
||||
# ClassContainer is covariant in T due to type[T]
|
||||
static_assert(is_subtype_of(ClassContainer[B], ClassContainer[A]))
|
||||
static_assert(not is_subtype_of(ClassContainer[A], ClassContainer[B]))
|
||||
|
||||
static_assert(is_assignable_to(ClassContainer[B], ClassContainer[A]))
|
||||
static_assert(not is_assignable_to(ClassContainer[A], ClassContainer[B]))
|
||||
|
||||
# Practical example: you can pass a ClassContainer[B] where ClassContainer[A] is expected
|
||||
# because type[B] can safely be used where type[A] is expected
|
||||
def use_a_class_container(container: ClassContainer[A]) -> A:
|
||||
return container.create_instance()
|
||||
|
||||
b_container = ClassContainer[B](B)
|
||||
a_instance: A = use_a_class_container(b_container) # This should work
|
||||
```
|
||||
|
||||
## Inheriting from generic classes with inferred variance
|
||||
|
||||
When inheriting from a generic class with our type variable substituted in, we count its occurrences
|
||||
as well. In the following example, `T` is covariant in `C`, and contravariant in the subclass `D` if
|
||||
you only count its own occurrences. Because we count both then, `T` is invariant in `D`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A:
|
||||
pass
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
|
||||
class C[T]:
|
||||
def f() -> T | None:
|
||||
pass
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
|
||||
class D[T](C[T]):
|
||||
def g(x: T) -> None:
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
## Inheriting from generic classes with explicit variance
|
||||
|
||||
```py
|
||||
from typing import TypeVar, Generic
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
T = TypeVar("T")
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
T_contra = TypeVar("T_contra", contravariant=True)
|
||||
|
||||
class A:
|
||||
pass
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
|
||||
class Invariant(Generic[T]):
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(Invariant[B], Invariant[A]))
|
||||
static_assert(not is_subtype_of(Invariant[A], Invariant[B]))
|
||||
|
||||
class DerivedInvariant[T](Invariant[T]):
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(DerivedInvariant[B], DerivedInvariant[A]))
|
||||
static_assert(not is_subtype_of(DerivedInvariant[A], DerivedInvariant[B]))
|
||||
|
||||
class Covariant(Generic[T_co]):
|
||||
pass
|
||||
|
||||
static_assert(is_subtype_of(Covariant[B], Covariant[A]))
|
||||
static_assert(not is_subtype_of(Covariant[A], Covariant[B]))
|
||||
|
||||
class DerivedCovariant[T](Covariant[T]):
|
||||
pass
|
||||
|
||||
static_assert(is_subtype_of(DerivedCovariant[B], DerivedCovariant[A]))
|
||||
static_assert(not is_subtype_of(DerivedCovariant[A], DerivedCovariant[B]))
|
||||
|
||||
class Contravariant(Generic[T_contra]):
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(Contravariant[B], Contravariant[A]))
|
||||
static_assert(is_subtype_of(Contravariant[A], Contravariant[B]))
|
||||
|
||||
class DerivedContravariant[T](Contravariant[T]):
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(DerivedContravariant[B], DerivedContravariant[A]))
|
||||
static_assert(is_subtype_of(DerivedContravariant[A], DerivedContravariant[B]))
|
||||
```
|
||||
|
||||
[linear-time-variance-talk]: https://www.youtube.com/watch?v=7uixlNTOY4s&t=9705s
|
||||
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance
|
||||
|
|
|
@ -217,8 +217,8 @@ class B: ...
|
|||
|
||||
def _[T](x: A | B):
|
||||
if type(x) is A[str]:
|
||||
# `type()` never returns a generic alias, so `type(x)` cannot be `A[str]`
|
||||
reveal_type(x) # revealed: Never
|
||||
# TODO: `type()` never returns a generic alias, so `type(x)` cannot be `A[str]`
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
```
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue