mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34: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():
|
case C():
|
||||||
pass
|
pass
|
||||||
case _:
|
case _:
|
||||||
# TODO: both of these are false positives (https://github.com/astral-sh/ty/issues/456)
|
no_diagnostic_here
|
||||||
no_diagnostic_here # error: [unresolved-reference]
|
assert_never(x)
|
||||||
assert_never(x) # error: [type-assertion-failure]
|
|
||||||
|
|
||||||
# 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:
|
||||||
def match_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: # error: [invalid-return-type]
|
|
||||||
match x:
|
match x:
|
||||||
case A():
|
case A():
|
||||||
return 0
|
return 0
|
||||||
|
|
|
@ -28,7 +28,7 @@ get from the sequence is a valid `int`.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
|
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 A: ...
|
||||||
class B(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[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(C[Any], C[object]))
|
||||||
|
static_assert(is_subtype_of(C[Never], C[Any]))
|
||||||
|
|
||||||
static_assert(is_subtype_of(D[B], C[A]))
|
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[B]))
|
||||||
|
@ -104,7 +106,7 @@ that you pass into the consumer is a valid `int`.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
|
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 A: ...
|
||||||
class B(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[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(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(not is_subtype_of(D[B], C[A]))
|
||||||
static_assert(is_subtype_of(D[A], C[B]))
|
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
|
```py
|
||||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
|
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 A: ...
|
||||||
class B(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[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(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[B], C[A]))
|
||||||
static_assert(not is_subtype_of(D[A], C[B]))
|
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
|
## Bivariance
|
||||||
|
|
||||||
With a bivariant typevar, _all_ specializations of the generic class are assignable to (and in fact,
|
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
|
gradually equivalent to) each other, and all specializations are subtypes of (and equivalent to)
|
||||||
equivalent to) each other.
|
each other.
|
||||||
|
|
||||||
This is a bit of pathological case, which really only happens when the class doesn't use the typevar
|
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_
|
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
|
```py
|
||||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
|
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 A: ...
|
||||||
class B(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[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(is_subtype_of(C[A], C[Any]))
|
||||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
static_assert(is_subtype_of(C[B], C[Any]))
|
||||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
static_assert(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[B]))
|
||||||
static_assert(not is_subtype_of(C[Any], C[Any]))
|
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[B], C[A]))
|
||||||
static_assert(is_subtype_of(D[A], C[B]))
|
static_assert(is_subtype_of(D[A], C[B]))
|
||||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
static_assert(is_subtype_of(D[A], C[Any]))
|
||||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
static_assert(is_subtype_of(D[B], C[Any]))
|
||||||
static_assert(not is_subtype_of(D[Any], C[A]))
|
static_assert(is_subtype_of(D[Any], C[A]))
|
||||||
static_assert(not is_subtype_of(D[Any], C[B]))
|
static_assert(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]))
|
||||||
|
|
|
@ -841,18 +841,6 @@ impl<'db> Specialization<'db> {
|
||||||
.zip(self.types(db))
|
.zip(self.types(db))
|
||||||
.zip(other.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
|
// Subtyping/assignability of each type in the specialization depends on the variance
|
||||||
// of the corresponding typevar:
|
// of the corresponding typevar:
|
||||||
// - covariant: verify that self_type <: other_type
|
// - covariant: verify that self_type <: other_type
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue