[ty] Check base classes when determining subtyping etc for generic aliases (#17927)

#17897 added variance handling for legacy typevars — but they were only
being considered when checking generic aliases of the same class:

```py
class A: ...
class B(A): ...

class C[T]: ...

static_assert(is_subtype_of(C[B], C[A]))
```

and not for generic subclasses:

```py
class D[U](C[U]): ...

static_assert(is_subtype_of(D[B], C[A]))
```

Now we check those too!

Closes https://github.com/astral-sh/ty/issues/101
This commit is contained in:
Douglas Creager 2025-05-07 15:21:11 -04:00 committed by GitHub
parent ce0800fccf
commit 2cf5cba7ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 363 additions and 98 deletions

View file

@ -5,8 +5,8 @@ relations. Much more detail can be found in the [spec]. To summarize, each typev
**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not **covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not
currently mentioned in the typing spec, but is a fourth case that we must consider.) currently mentioned in the typing spec, but is a fourth case that we must consider.)
For all of the examples below, we will consider a typevar `T`, a generic class using that typevar For all of the examples below, we will consider typevars `T` and `U`, two generic classes using
`C[T]`, and two types `A` and `B`. those typevars `C[T]` and `D[U]`, and two types `A` and `B`.
(Note that dynamic types like `Any` never participate in subtyping, so `C[Any]` is neither a subtype (Note that dynamic types like `Any` never participate in subtyping, so `C[Any]` is neither a subtype
nor supertype of any other specialization of `C`, regardless of `T`'s variance. It is, however, nor supertype of any other specialization of `C`, regardless of `T`'s variance. It is, however,
@ -14,8 +14,8 @@ assignable to any specialization of `C`, regardless of variance, via materializa
## Covariance ## Covariance
With a covariant typevar, subtyping and assignability are in "alignment": if `A <: B`, then With a covariant typevar, subtyping and assignability are in "alignment": if `A <: B` and `C <: D`,
`C[A] <: C[B]`. then `C[A] <: C[B]` and `C[A] <: D[B]`.
Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of
`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would `int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would
@ -29,11 +29,15 @@ class A: ...
class B(A): ... class B(A): ...
T = TypeVar("T", covariant=True) T = TypeVar("T", covariant=True)
U = TypeVar("U", covariant=True)
class C(Generic[T]): class C(Generic[T]):
def receive(self) -> T: def receive(self) -> T:
raise ValueError raise ValueError
class D(C[U]):
pass
static_assert(is_assignable_to(C[B], C[A])) static_assert(is_assignable_to(C[B], C[A]))
static_assert(not is_assignable_to(C[A], C[B])) static_assert(not is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any])) static_assert(is_assignable_to(C[A], C[Any]))
@ -41,6 +45,13 @@ 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[A]))
static_assert(is_assignable_to(C[Any], C[B])) static_assert(is_assignable_to(C[Any], C[B]))
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]))
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]))
static_assert(is_subtype_of(C[B], C[A])) 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[B]))
static_assert(not is_subtype_of(C[A], C[Any])) static_assert(not is_subtype_of(C[A], C[Any]))
@ -48,6 +59,13 @@ 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[A]))
static_assert(not is_subtype_of(C[Any], C[B])) static_assert(not is_subtype_of(C[Any], C[B]))
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]))
static_assert(not is_subtype_of(D[B], C[Any]))
static_assert(not is_subtype_of(D[Any], C[A]))
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[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B])) static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A])) static_assert(not is_equivalent_to(C[B], C[A]))
@ -57,6 +75,15 @@ static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A])) static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B])) static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(not is_equivalent_to(D[A], C[A]))
static_assert(not is_equivalent_to(D[B], C[B]))
static_assert(not is_equivalent_to(D[B], C[A]))
static_assert(not is_equivalent_to(D[A], C[B]))
static_assert(not is_equivalent_to(D[A], C[Any]))
static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A])) static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B])) static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any])) static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
@ -67,12 +94,23 @@ static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any])) static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A])) static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B])) static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
``` ```
## Contravariance ## Contravariance
With a contravariant typevar, subtyping and assignability are in "opposition": if `A <: B`, then With a contravariant typevar, subtyping and assignability are in "opposition": if `A <: B` and
`C[B] <: C[A]`. `C <: D`, then `C[B] <: C[A]` and `D[B] <: C[A]`.
Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives
`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool` `bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool`
@ -86,10 +124,14 @@ class A: ...
class B(A): ... class B(A): ...
T = TypeVar("T", contravariant=True) T = TypeVar("T", contravariant=True)
U = TypeVar("U", contravariant=True)
class C(Generic[T]): class C(Generic[T]):
def send(self, value: T): ... def send(self, value: T): ...
class D(C[U]):
pass
static_assert(not is_assignable_to(C[B], C[A])) static_assert(not is_assignable_to(C[B], C[A]))
static_assert(is_assignable_to(C[A], C[B])) static_assert(is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any])) static_assert(is_assignable_to(C[A], C[Any]))
@ -97,6 +139,13 @@ 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[A]))
static_assert(is_assignable_to(C[Any], C[B])) static_assert(is_assignable_to(C[Any], C[B]))
static_assert(not is_assignable_to(D[B], C[A]))
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]))
static_assert(not is_subtype_of(C[B], C[A])) static_assert(not is_subtype_of(C[B], C[A]))
static_assert(is_subtype_of(C[A], C[B])) 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[A], C[Any]))
@ -104,6 +153,13 @@ 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[A]))
static_assert(not is_subtype_of(C[Any], C[B])) static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(not is_subtype_of(D[B], C[A]))
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]))
static_assert(not is_subtype_of(D[Any], C[A]))
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[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B])) static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A])) static_assert(not is_equivalent_to(C[B], C[A]))
@ -113,6 +169,15 @@ static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A])) static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B])) static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(not is_equivalent_to(D[A], C[A]))
static_assert(not is_equivalent_to(D[B], C[B]))
static_assert(not is_equivalent_to(D[B], C[A]))
static_assert(not is_equivalent_to(D[A], C[B]))
static_assert(not is_equivalent_to(D[A], C[Any]))
static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A])) static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B])) static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any])) static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
@ -123,6 +188,17 @@ static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any])) static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A])) static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B])) static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
``` ```
## Invariance ## Invariance
@ -155,12 +231,16 @@ class A: ...
class B(A): ... class B(A): ...
T = TypeVar("T") T = TypeVar("T")
U = TypeVar("U")
class C(Generic[T]): class C(Generic[T]):
def send(self, value: T): ... def send(self, value: T): ...
def receive(self) -> T: def receive(self) -> T:
raise ValueError raise ValueError
class D(C[U]):
pass
static_assert(not is_assignable_to(C[B], C[A])) static_assert(not is_assignable_to(C[B], C[A]))
static_assert(not is_assignable_to(C[A], C[B])) static_assert(not is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any])) static_assert(is_assignable_to(C[A], C[Any]))
@ -168,6 +248,13 @@ 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[A]))
static_assert(is_assignable_to(C[Any], C[B])) static_assert(is_assignable_to(C[Any], C[B]))
static_assert(not 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]))
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]))
static_assert(not is_subtype_of(C[B], C[A])) static_assert(not 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[B]))
static_assert(not is_subtype_of(C[A], C[Any])) static_assert(not is_subtype_of(C[A], C[Any]))
@ -175,6 +262,13 @@ 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[A]))
static_assert(not is_subtype_of(C[Any], C[B])) static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(not 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]))
static_assert(not is_subtype_of(D[B], C[Any]))
static_assert(not is_subtype_of(D[Any], C[A]))
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[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B])) static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A])) static_assert(not is_equivalent_to(C[B], C[A]))
@ -184,6 +278,15 @@ static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A])) static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B])) static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(not is_equivalent_to(D[A], C[A]))
static_assert(not is_equivalent_to(D[B], C[B]))
static_assert(not is_equivalent_to(D[B], C[A]))
static_assert(not is_equivalent_to(D[A], C[B]))
static_assert(not is_equivalent_to(D[A], C[Any]))
static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A])) static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B])) static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any])) static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
@ -194,6 +297,17 @@ static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any])) static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A])) static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B])) static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
``` ```
## Bivariance ## Bivariance

View file

@ -10,16 +10,17 @@ relations. Much more detail can be found in the [spec]. To summarize, each typev
**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not **covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not
currently mentioned in the typing spec, but is a fourth case that we must consider.) currently mentioned in the typing spec, but is a fourth case that we must consider.)
For all of the examples below, we will consider a typevar `T`, a generic class using that typevar For all of the examples below, we will consider typevars `T` and `U`, two generic classes using
`C[T]`, and two types `A` and `B`. those typevars `C[T]` and `D[U]`, and two types `A` and `B`.
(Note that dynamic types like `Any` never participate in subtyping, so `C[Any]` is neither a subtype (Note that dynamic types like `Any` never participate in subtyping, so `C[Any]` is neither a subtype
nor supertype of any other specialization of `C`, regardless of `T`'s variance.) nor supertype of any other specialization of `C`, regardless of `T`'s variance. It is, however,
assignable to any specialization of `C`, regardless of variance, via materialization.)
## Covariance ## Covariance
With a covariant typevar, subtyping and assignability are in "alignment": if `A <: B`, then With a covariant typevar, subtyping and assignability are in "alignment": if `A <: B` and `C <: D`,
`C[A] <: C[B]`. then `C[A] <: C[B]` and `C[A] <: D[B]`.
Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of
`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would `int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would
@ -36,6 +37,9 @@ class C[T]:
def receive(self) -> T: def receive(self) -> T:
raise ValueError raise ValueError
class D[U](C[U]):
pass
# TODO: no error # TODO: no error
# error: [static-assert-error] # error: [static-assert-error]
static_assert(is_assignable_to(C[B], C[A])) static_assert(is_assignable_to(C[B], C[A]))
@ -45,6 +49,15 @@ 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[A]))
static_assert(is_assignable_to(C[Any], C[B])) 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]))
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 # TODO: no error
# error: [static-assert-error] # error: [static-assert-error]
static_assert(is_subtype_of(C[B], C[A])) static_assert(is_subtype_of(C[B], C[A]))
@ -54,6 +67,15 @@ 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[A]))
static_assert(not is_subtype_of(C[Any], C[B])) 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]))
static_assert(not is_subtype_of(D[B], C[Any]))
static_assert(not is_subtype_of(D[Any], C[A]))
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[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B])) static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A])) static_assert(not is_equivalent_to(C[B], C[A]))
@ -63,6 +85,15 @@ static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A])) static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B])) static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(not is_equivalent_to(D[A], C[A]))
static_assert(not is_equivalent_to(D[B], C[B]))
static_assert(not is_equivalent_to(D[B], C[A]))
static_assert(not is_equivalent_to(D[A], C[B]))
static_assert(not is_equivalent_to(D[A], C[Any]))
static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A])) static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B])) static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any])) static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
@ -73,12 +104,23 @@ static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any])) static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A])) static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B])) static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
``` ```
## Contravariance ## Contravariance
With a contravariant typevar, subtyping are assignability are in "opposition": if `A <: B`, then With a contravariant typevar, subtyping and assignability are in "opposition": if `A <: B` and
`C[B] <: C[A]`. `C <: D`, then `C[B] <: C[A]` and `D[B] <: C[A]`.
Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives
`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool` `bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool`
@ -94,6 +136,9 @@ class B(A): ...
class C[T]: class C[T]:
def send(self, value: T): ... def send(self, value: T): ...
class D[U](C[U]):
pass
static_assert(not is_assignable_to(C[B], C[A])) static_assert(not is_assignable_to(C[B], C[A]))
# TODO: no error # TODO: no error
# error: [static-assert-error] # error: [static-assert-error]
@ -103,6 +148,15 @@ 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[A]))
static_assert(is_assignable_to(C[Any], C[B])) 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]))
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])) static_assert(not is_subtype_of(C[B], C[A]))
# TODO: no error # TODO: no error
# error: [static-assert-error] # error: [static-assert-error]
@ -112,6 +166,15 @@ 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[A]))
static_assert(not is_subtype_of(C[Any], C[B])) 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]))
static_assert(not is_subtype_of(D[Any], C[A]))
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[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B])) static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A])) static_assert(not is_equivalent_to(C[B], C[A]))
@ -121,6 +184,15 @@ static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A])) static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B])) static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(not is_equivalent_to(D[A], C[A]))
static_assert(not is_equivalent_to(D[B], C[B]))
static_assert(not is_equivalent_to(D[B], C[A]))
static_assert(not is_equivalent_to(D[A], C[B]))
static_assert(not is_equivalent_to(D[A], C[Any]))
static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A])) static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B])) static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any])) static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
@ -131,12 +203,23 @@ static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any])) static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A])) static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B])) static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
``` ```
## Invariance ## Invariance
With an invariant typevar, _no_ specializations of the generic class are subtypes of or assignable With an invariant typevar, only equivalent specializations of the generic class are subtypes of or
to each other. assignable to each other.
This often occurs for types that are both producers _and_ consumers, like a mutable `list`. This often occurs for types that are both producers _and_ consumers, like a mutable `list`.
Iterating over the elements in a list would work with a covariant typevar, just like with the Iterating over the elements in a list would work with a covariant typevar, just like with the
@ -167,6 +250,9 @@ class C[T]:
def receive(self) -> T: def receive(self) -> T:
raise ValueError raise ValueError
class D[U](C[U]):
pass
static_assert(not is_assignable_to(C[B], C[A])) static_assert(not is_assignable_to(C[B], C[A]))
static_assert(not is_assignable_to(C[A], C[B])) static_assert(not is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any])) static_assert(is_assignable_to(C[A], C[Any]))
@ -174,6 +260,13 @@ 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[A]))
static_assert(is_assignable_to(C[Any], C[B])) static_assert(is_assignable_to(C[Any], C[B]))
static_assert(not 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]))
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]))
static_assert(not is_subtype_of(C[B], C[A])) static_assert(not 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[B]))
static_assert(not is_subtype_of(C[A], C[Any])) static_assert(not is_subtype_of(C[A], C[Any]))
@ -181,6 +274,13 @@ 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[A]))
static_assert(not is_subtype_of(C[Any], C[B])) static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(not 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]))
static_assert(not is_subtype_of(D[B], C[Any]))
static_assert(not is_subtype_of(D[Any], C[A]))
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[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B])) static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A])) static_assert(not is_equivalent_to(C[B], C[A]))
@ -190,6 +290,15 @@ static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A])) static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B])) static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(not is_equivalent_to(D[A], C[A]))
static_assert(not is_equivalent_to(D[B], C[B]))
static_assert(not is_equivalent_to(D[B], C[A]))
static_assert(not is_equivalent_to(D[A], C[B]))
static_assert(not is_equivalent_to(D[A], C[Any]))
static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A])) static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B])) static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any])) static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
@ -200,6 +309,17 @@ static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any])) static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A])) static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B])) static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
``` ```
## Bivariance ## Bivariance
@ -222,6 +342,9 @@ class B(A): ...
class C[T]: class C[T]:
pass pass
class D[U](C[U]):
pass
# TODO: no error # TODO: no error
# error: [static-assert-error] # error: [static-assert-error]
static_assert(is_assignable_to(C[B], C[A])) static_assert(is_assignable_to(C[B], C[A]))
@ -233,6 +356,17 @@ 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[A]))
static_assert(is_assignable_to(C[Any], C[B])) 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]))
# 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 # TODO: no error
# error: [static-assert-error] # error: [static-assert-error]
static_assert(is_subtype_of(C[B], C[A])) static_assert(is_subtype_of(C[B], C[A]))
@ -244,6 +378,17 @@ 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[A]))
static_assert(not is_subtype_of(C[Any], C[B])) 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]))
# 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]))
static_assert(not is_subtype_of(D[Any], C[A]))
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[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B])) static_assert(is_equivalent_to(C[B], C[B]))
# TODO: no error # TODO: no error
@ -257,6 +402,15 @@ static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A])) static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B])) static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(not is_equivalent_to(D[A], C[A]))
static_assert(not is_equivalent_to(D[B], C[B]))
static_assert(not is_equivalent_to(D[B], C[A]))
static_assert(not is_equivalent_to(D[A], C[B]))
static_assert(not is_equivalent_to(D[A], C[Any]))
static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A])) static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B])) static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any])) static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
@ -279,6 +433,17 @@ static_assert(is_gradual_equivalent_to(C[Any], C[A]))
# TODO: no error # TODO: no error
# error: [static-assert-error] # error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[Any], C[B])) static_assert(is_gradual_equivalent_to(C[Any], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
``` ```
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance [spec]: https://typing.python.org/en/latest/spec/generics.html#variance

View file

@ -334,10 +334,19 @@ static_assert(is_subtype_of(TypeOf[typing], ModuleType))
### Slice literals ### Slice literals
The type of a slice literal is currently inferred as `slice`, which is a generic type whose default
specialization includes `Any`. Slice literals therefore do not participate in the subtyping
relationship.
TODO: Infer a specialized type for the slice literal
```py ```py
from ty_extensions import TypeOf, is_subtype_of, static_assert from ty_extensions import TypeOf, is_subtype_of, static_assert
static_assert(is_subtype_of(TypeOf[1:2:3], slice)) static_assert(not is_subtype_of(TypeOf[1:2:3], slice))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(TypeOf[1:2:3], slice[int]))
``` ```
### Special forms ### Special forms

View file

@ -1515,6 +1515,10 @@ impl<'db> Type<'db> {
false false
} }
(Type::SliceLiteral(_), _) => KnownClass::Slice
.to_instance(db)
.is_assignable_to(db, target),
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => { (Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
self_function_literal self_function_literal
.into_callable_type(db) .into_callable_type(db)

View file

@ -290,109 +290,82 @@ impl<'db> ClassType<'db> {
}) })
} }
/// If `self` and `other` are generic aliases of the same generic class, returns their
/// corresponding specializations.
fn compatible_specializations(
self,
db: &'db dyn Db,
other: ClassType<'db>,
) -> Option<(Specialization<'db>, Specialization<'db>)> {
match (self, other) {
(ClassType::Generic(self_generic), ClassType::Generic(other_generic)) => {
if self_generic.origin(db) == other_generic.origin(db) {
Some((
self_generic.specialization(db),
other_generic.specialization(db),
))
} else {
None
}
}
_ => None,
}
}
/// Return `true` if `other` is present in this class's MRO. /// Return `true` if `other` is present in this class's MRO.
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
// `is_subclass_of` is checking the subtype relation, in which gradual types do not self.iter_mro(db).any(|base| {
// participate, so we should not return `True` if we find `Any/Unknown` in the MRO. match base {
if self.iter_mro(db).contains(&ClassBase::Class(other)) { // `is_subclass_of` is checking the subtype relation, in which gradual types do not
return true; // participate.
} ClassBase::Dynamic(_) => false,
// `self` is a subclass of `other` if they are both generic aliases of the same generic // Protocol and Generic are not represented by a ClassType.
// class, and their specializations are compatible, taking into account the variance of the ClassBase::Protocol | ClassBase::Generic(_) => false,
// class's typevars.
if let Some((self_specialization, other_specialization)) = ClassBase::Class(base) => match (base, other) {
self.compatible_specializations(db, other) (ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
{ (ClassType::Generic(base), ClassType::Generic(other)) => {
if self_specialization.is_subtype_of(db, other_specialization) { base.origin(db) == other.origin(db)
return true; && base
.specialization(db)
.is_subtype_of(db, other.specialization(db))
}
(ClassType::Generic(_), ClassType::NonGeneric(_))
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
},
} }
} })
false
} }
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
if self == other { match (self, other) {
return true; (ClassType::NonGeneric(this), ClassType::NonGeneric(other)) => this == other,
} (ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => false,
// `self` is equivalent to `other` if they are both generic aliases of the same generic (ClassType::Generic(this), ClassType::Generic(other)) => {
// class, and their specializations are compatible, taking into account the variance of the this.origin(db) == other.origin(db)
// class's typevars. && this
if let Some((self_specialization, other_specialization)) = .specialization(db)
self.compatible_specializations(db, other) .is_equivalent_to(db, other.specialization(db))
{
if self_specialization.is_equivalent_to(db, other_specialization) {
return true;
} }
} }
false
} }
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
if self.is_subclass_of(db, other) { self.iter_mro(db).any(|base| {
return true; match base {
} ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown) => !other.is_final(db),
ClassBase::Dynamic(_) => false,
// `self` is assignable to `other` if they are both generic aliases of the same generic // Protocol and Generic are not represented by a ClassType.
// class, and their specializations are compatible, taking into account the variance of the ClassBase::Protocol | ClassBase::Generic(_) => false,
// class's typevars.
if let Some((self_specialization, other_specialization)) = ClassBase::Class(base) => match (base, other) {
self.compatible_specializations(db, other) (ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
{ (ClassType::Generic(base), ClassType::Generic(other)) => {
if self_specialization.is_assignable_to(db, other_specialization) { base.origin(db) == other.origin(db)
return true; && base
.specialization(db)
.is_assignable_to(db, other.specialization(db))
}
(ClassType::Generic(_), ClassType::NonGeneric(_))
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
},
} }
} })
if self.is_subclass_of_any_or_unknown(db) && !other.is_final(db) {
return true;
}
false
} }
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
if self == other { match (self, other) {
return true; (ClassType::NonGeneric(this), ClassType::NonGeneric(other)) => this == other,
} (ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => false,
// `self` is equivalent to `other` if they are both generic aliases of the same generic (ClassType::Generic(this), ClassType::Generic(other)) => {
// class, and their specializations are compatible, taking into account the variance of the this.origin(db) == other.origin(db)
// class's typevars. && this
if let Some((self_specialization, other_specialization)) = .specialization(db)
self.compatible_specializations(db, other) .is_gradual_equivalent_to(db, other.specialization(db))
{
if self_specialization.is_gradual_equivalent_to(db, other_specialization) {
return true;
} }
} }
false
} }
/// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred.