[ty] Fix subtyping for dynamic specializations (#20592)

## Summary

Fixes a bug observed by @AlexWaygood where `C[Any] <: C[object]` should
hold for a class that is covariant in its type parameter (and similar
subtyping relations involving dynamic types for other variance
configurations).

## Test Plan

New and updated Markdown tests
This commit is contained in:
David Peter 2025-09-26 15:05:03 +02:00 committed by GitHub
parent 2af8c53110
commit 3932f7c849
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 26 additions and 32 deletions

View file

@ -312,12 +312,10 @@ def match_exhaustive(x: A[D] | B[E] | C[F]):
case C():
pass
case _:
# TODO: both of these are false positives (https://github.com/astral-sh/ty/issues/456)
no_diagnostic_here # error: [unresolved-reference]
assert_never(x) # error: [type-assertion-failure]
no_diagnostic_here
assert_never(x)
# TODO: false-positive diagnostic (https://github.com/astral-sh/ty/issues/456)
def match_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: # error: [invalid-return-type]
def match_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int:
match x:
case A():
return 0

View file

@ -28,7 +28,7 @@ get from the sequence is a valid `int`.
```py
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
from typing import Any, Never
class A: ...
class B(A): ...
@ -60,6 +60,8 @@ 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(is_subtype_of(C[Any], C[object]))
static_assert(is_subtype_of(C[Never], C[Any]))
static_assert(is_subtype_of(D[B], C[A]))
static_assert(not is_subtype_of(D[A], C[B]))
@ -104,7 +106,7 @@ that you pass into the consumer is a valid `int`.
```py
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
from typing import Any, Never
class A: ...
class B(A): ...
@ -135,6 +137,8 @@ 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(is_subtype_of(C[object], C[Any]))
static_assert(is_subtype_of(C[Any], C[Never]))
static_assert(not is_subtype_of(D[B], C[A]))
static_assert(is_subtype_of(D[A], C[B]))
@ -192,7 +196,7 @@ since we can't know in advance which of the allowed methods you'll want to use.
```py
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
from typing import Any, Never
class A: ...
class B(A): ...
@ -225,6 +229,8 @@ 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(C[object], C[Any]))
static_assert(not is_subtype_of(C[Any], C[Never]))
static_assert(not is_subtype_of(D[B], C[A]))
static_assert(not is_subtype_of(D[A], C[B]))
@ -261,8 +267,8 @@ static_assert(not is_equivalent_to(D[Any], C[Unknown]))
## Bivariance
With a bivariant typevar, _all_ specializations of the generic class are assignable to (and in fact,
gradually equivalent to) each other, and all fully static specializations are subtypes of (and
equivalent to) each other.
gradually equivalent to) each other, and all specializations are subtypes of (and equivalent to)
each other.
This is a bit of pathological case, which really only happens when the class doesn't use the typevar
at all. (If it did, it would have to be covariant, contravariant, or invariant, depending on _how_
@ -270,7 +276,7 @@ the typevar was used.)
```py
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
from typing import Any, Never
class A: ...
class B(A): ...
@ -298,18 +304,20 @@ static_assert(is_assignable_to(D[Any], C[B]))
static_assert(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(C[Any], C[Any]))
static_assert(is_subtype_of(C[A], C[Any]))
static_assert(is_subtype_of(C[B], C[Any]))
static_assert(is_subtype_of(C[Any], C[A]))
static_assert(is_subtype_of(C[Any], C[B]))
static_assert(is_subtype_of(C[Any], C[Any]))
static_assert(is_subtype_of(C[object], C[Any]))
static_assert(is_subtype_of(C[Any], C[Never]))
static_assert(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_subtype_of(D[A], C[Any]))
static_assert(is_subtype_of(D[B], C[Any]))
static_assert(is_subtype_of(D[Any], C[A]))
static_assert(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]))

View file

@ -841,18 +841,6 @@ impl<'db> Specialization<'db> {
.zip(self.types(db))
.zip(other.types(db))
{
// As an optimization, we can return early if either type is dynamic, unless
// we're dealing with a top or bottom materialization.
if other_materialization_kind.is_none()
&& self_materialization_kind.is_none()
&& (self_type.is_dynamic() || other_type.is_dynamic())
{
match relation {
TypeRelation::Assignability => continue,
TypeRelation::Subtyping => return ConstraintSet::from(false),
}
}
// Subtyping/assignability of each type in the specialization depends on the variance
// of the corresponding typevar:
// - covariant: verify that self_type <: other_type