[ty] Unify Type::is_subtype_of() and Type::is_assignable_to() (#18430)
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 / 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 / mkdocs (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 (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This commit is contained in:
Alex Waygood 2025-06-06 18:28:55 +01:00 committed by GitHub
parent 1274521f9f
commit 6e785867c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 394 additions and 588 deletions

View file

@ -305,10 +305,13 @@ simplify to `Never`, even in the presence of other types:
```py
from ty_extensions import Intersection, Not
from typing import Any
from typing import Any, Generic, TypeVar
T_co = TypeVar("T_co", covariant=True)
class P: ...
class Q: ...
class R(Generic[T_co]): ...
def _(
i1: Intersection[P, Not[P]],
@ -317,6 +320,8 @@ def _(
i4: Intersection[Not[P], Q, P],
i5: Intersection[P, Any, Not[P]],
i6: Intersection[Not[P], Any, P],
i7: Intersection[R[P], Not[R[P]]],
i8: Intersection[R[P], Not[R[Q]]],
) -> None:
reveal_type(i1) # revealed: Never
reveal_type(i2) # revealed: Never
@ -324,6 +329,8 @@ def _(
reveal_type(i4) # revealed: Never
reveal_type(i5) # revealed: Never
reveal_type(i6) # revealed: Never
reveal_type(i7) # revealed: Never
reveal_type(i8) # revealed: R[P] & ~R[Q]
```
### Union of a type and its negation
@ -332,20 +339,28 @@ Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `o
```py
from ty_extensions import Intersection, Not
from typing import Generic, TypeVar
T_co = TypeVar("T_co", covariant=True)
class P: ...
class Q: ...
class R(Generic[T_co]): ...
def _(
i1: P | Not[P],
i2: Not[P] | P,
i3: P | Q | Not[P],
i4: Not[P] | Q | P,
i5: R[P] | Not[R[P]],
i6: R[P] | Not[R[Q]],
) -> None:
reveal_type(i1) # revealed: object
reveal_type(i2) # revealed: object
reveal_type(i3) # revealed: object
reveal_type(i4) # revealed: object
reveal_type(i5) # revealed: object
reveal_type(i6) # revealed: R[P] | ~R[Q]
```
### Negation is an involution

View file

@ -902,8 +902,7 @@ from ty_extensions import is_subtype_of, is_assignable_to, static_assert, TypeOf
class HasX(Protocol):
x: int
# TODO: this should pass
static_assert(is_subtype_of(TypeOf[module], HasX)) # error: [static-assert-error]
static_assert(is_subtype_of(TypeOf[module], HasX))
static_assert(is_assignable_to(TypeOf[module], HasX))
class ExplicitProtocolSubtype(HasX, Protocol):

View file

@ -209,6 +209,34 @@ class AnyMeta(metaclass=Any): ...
static_assert(is_assignable_to(type[AnyMeta], type))
static_assert(is_assignable_to(type[AnyMeta], type[object]))
static_assert(is_assignable_to(type[AnyMeta], type[Any]))
from typing import TypeVar, Generic, Any
T_co = TypeVar("T_co", covariant=True)
class Foo(Generic[T_co]): ...
class Bar(Foo[T_co], Generic[T_co]): ...
static_assert(is_assignable_to(TypeOf[Bar[int]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar], type[Foo]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[Any]]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
# TODO: these should pass (all subscripts inside `type[]` type expressions are currently TODO types)
static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]])) # error: [static-assert-error]
static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) # error: [static-assert-error]
```
## `type[]` is not assignable to types disjoint from `builtins.type`
```py
from typing import Any
from ty_extensions import is_assignable_to, static_assert
static_assert(not is_assignable_to(type[Any], None))
```
## Class-literals that inherit from `Any`
@ -717,6 +745,53 @@ def f(x: int, y: str) -> None: ...
c1: Callable[[int], None] = partial(f, y="a")
```
### Generic classes with `__call__`
```toml
[environment]
python-version = "3.12"
```
```py
from typing_extensions import Callable, Any, Generic, TypeVar, ParamSpec
from ty_extensions import static_assert, is_assignable_to
T = TypeVar("T")
P = ParamSpec("P")
class Foo[T]:
def __call__(self): ...
class FooLegacy(Generic[T]):
def __call__(self): ...
class Bar[T, **P]:
def __call__(self): ...
# TODO: should not error
class BarLegacy(Generic[T, P]): # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
def __call__(self): ...
static_assert(is_assignable_to(Foo, Callable[..., Any]))
static_assert(is_assignable_to(FooLegacy, Callable[..., Any]))
static_assert(is_assignable_to(Bar, Callable[..., Any]))
static_assert(is_assignable_to(BarLegacy, Callable[..., Any]))
class Spam[T]: ...
class SpamLegacy(Generic[T]): ...
class Eggs[T, **P]: ...
# TODO: should not error
class EggsLegacy(Generic[T, P]): ... # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
static_assert(not is_assignable_to(Spam, Callable[..., Any]))
static_assert(not is_assignable_to(SpamLegacy, Callable[..., Any]))
static_assert(not is_assignable_to(Eggs, Callable[..., Any]))
# TODO: should pass
static_assert(not is_assignable_to(EggsLegacy, Callable[..., Any])) # error: [static-assert-error]
```
### Classes with `__call__` as attribute
An instance type is assignable to a compatible callable type if the instance type's class has a