mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[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:
parent
2af8c53110
commit
3932f7c849
3 changed files with 26 additions and 32 deletions
|
@ -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
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue