mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:38:25 +00:00
[ty] eliminate is_fully_static (#18799)
## Summary Having a recursive type method to check whether a type is fully static is inefficient, unnecessary, and makes us overly strict about subtyping relations. It's inefficient because we end up re-walking the same types many times to check for fully-static-ness. It's unnecessary because we can check relations involving the dynamic type appropriately, depending whether the relation is subtyping or assignability. We use the subtyping relation to simplify unions and intersections. We can usefully consider that `S <: T` for gradual types also, as long as it remains true that `S | T` is equivalent to `T` and `S & T` is equivalent to `S`. One conservative definition (implemented here) that satisfies this requirement is that we consider `S <: T` if, for every possible pair of materializations `S'` and `T'`, `S' <: T'`. Or put differently the top materialization of `S` (`S+` -- the union of all possible materializations of `S`) is a subtype of the bottom materialization of `T` (`T-` -- the intersection of all possible materializations of `T`). In the most basic cases we can usefully say that `Any <: object` and that `Never <: Any`, and we can handle more complex cases inductively from there. This definition of subtyping for gradual subtypes is not reflexive (`Any` is not a subtype of `Any`). As a corollary, we also remove `is_gradual_equivalent_to` -- `is_equivalent_to` now has the meaning that `is_gradual_equivalent_to` used to have. If necessary, we could restore an `is_fully_static_equivalent_to` or similar (which would not do an `is_fully_static` pre-check of the types, but would instead pass a relation-kind enum down through a recursive equivalence check, similar to `has_relation_to`), but so far this doesn't appear to be necessary. Credit to @JelleZijlstra for the observation that `is_fully_static` is unnecessary and overly restrictive on subtyping. There is another possible definition of gradual subtyping: instead of requiring that `S+ <: T-`, we could instead require that `S+ <: T+` and `S- <: T-`. In other words, instead of requiring all materializations of `S` to be a subtype of every materialization of `T`, we just require that every materialization of `S` be a subtype of _some_ materialization of `T`, and that every materialization of `T` be a supertype of some materialization of `S`. This definition also preserves the core invariant that `S <: T` implies that `S | T = T` and `S & T = S`, and it restores reflexivity: under this definition, `Any` is a subtype of `Any`, and for any equivalent types `S` and `T`, `S <: T` and `T <: S`. But unfortunately, this definition breaks transitivity of subtyping, because nominal subclasses in Python use assignability ("consistent subtyping") to define acceptable overrides. This means that we may have a class `A` with `def method(self) -> Any` and a subtype `B(A)` with `def method(self) -> int`, since `int` is assignable to `Any`. This means that if we have a protocol `P` with `def method(self) -> Any`, we would have `B <: A` (from nominal subtyping) and `A <: P` (`Any` is a subtype of `Any`), but not `B <: P` (`int` is not a subtype of `Any`). Breaking transitivity of subtyping is not tenable, so we don't use this definition of subtyping. ## Test Plan Existing tests (modified in some cases to account for updated semantics.) Stable property tests pass at a million iterations: `QUICKCHECK_TESTS=1000000 cargo test -p ty_python_semantic -- --ignored types::property_tests::stable` ### Changes to property test type generation Since we no longer have a method of categorizing built types as fully-static or not-fully-static, I had to add a previously-discussed feature to the property tests so that some tests can build types that are known by construction to be fully static, because there are still properties that only apply to fully-static types (for example, reflexiveness of subtyping.) ## Changes to handling of `*args, **kwargs` signatures This PR "discovered" that, once we allow non-fully-static types to participate in subtyping under the above definitions, `(*args: Any, **kwargs: Any) -> Any` is now a subtype of `() -> object`. This is true, if we take a literal interpretation of the former signature: all materializations of the parameters `*args: Any, **kwargs: Any` can accept zero arguments, making the former signature a subtype of the latter. But the spec actually says that `*args: Any, **kwargs: Any` should be interpreted as equivalent to `...`, and that makes a difference here: `(...) -> Any` is not a subtype of `() -> object`, because (unlike a literal reading of `(*args: Any, **kwargs: Any)`), `...` can materialize to _any_ signature, including a signature with required positional arguments. This matters for this PR because it makes the "any two types are both assignable to their union" property test fail if we don't implement the equivalence to `...`. Because `FunctionType.__call__` has the signature `(*args: Any, **kwargs: Any) -> Any`, and if we take that at face value it's a subtype of `() -> object`, making `FunctionType` a subtype of `() -> object)` -- but then a function with a required argument is also a subtype of `FunctionType`, but not a subtype of `() -> object`. So I went ahead and implemented the equivalence to `...` in this PR. ## Ecosystem analysis * Most of the ecosystem report are cases of improved union/intersection simplification. For example, we can now simplify a union like `bool | (bool & Unknown) | Unknown` to simply `bool | Unknown`, because we can now observe that every possible materialization of `bool & Unknown` is still a subtype of `bool` (whereas before we would set aside `bool & Unknown` as a not-fully-static type.) This is clearly an improvement. * The `possibly-unresolved-reference` errors in sockeye, pymongo, ignite, scrapy and others are true positives for conditional imports that were formerly silenced by bogus conflicting-declarations (which we currently don't issue a diagnostic for), because we considered two different declarations of `Unknown` to be conflicting (we used `is_equivalent_to` not `is_gradual_equivalent_to`). In this PR that distinction disappears and all equivalence is gradual, so a declaration of `Unknown` no longer conflicts with a declaration of `Unknown`, which then results in us surfacing the possibly-unbound error. * We will now issue "redundant cast" for casting from a typevar with a gradual bound to the same typevar (the hydra-zen diagnostic). This seems like an improvement. * The new diagnostics in bandersnatch are interesting. For some reason primer in CI seems to be checking bandersnatch on Python 3.10 (not yet sure why; this doesn't happen when I run it locally). But bandersnatch uses `enum.StrEnum`, which doesn't exist on 3.10. That makes the `class SimpleDigest(StrEnum)` a class that inherits from `Unknown` (and bypasses our current TODO handling for accessing attributes on enum classes, since we don't recognize it as an enum class at all). This PR improves our understanding of assignability to classes that inherit from `Any` / `Unknown`, and we now recognize that a string literal is not assignable to a class inheriting `Any` or `Unknown`.
This commit is contained in:
parent
eee5a5a3d6
commit
62975b3ab2
39 changed files with 957 additions and 1633 deletions
|
@ -313,7 +313,7 @@ len([], 1)
|
|||
### Type API predicates
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, is_fully_static
|
||||
from ty_extensions import is_subtype_of
|
||||
|
||||
# error: [missing-argument]
|
||||
is_subtype_of()
|
||||
|
@ -326,10 +326,4 @@ is_subtype_of(int, int, int)
|
|||
|
||||
# error: [too-many-positional-arguments]
|
||||
is_subtype_of(int, int, int, int)
|
||||
|
||||
# error: [missing-argument]
|
||||
is_fully_static()
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
is_fully_static(int, int)
|
||||
```
|
||||
|
|
|
@ -144,8 +144,7 @@ from typing import Any
|
|||
def _(a: Any, tuple_of_any: tuple[Any]):
|
||||
reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"]
|
||||
|
||||
# TODO: Ideally, this would just be `def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int`
|
||||
# revealed: (def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int) | Literal["default"]
|
||||
# revealed: def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int
|
||||
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default"))
|
||||
```
|
||||
|
||||
|
|
|
@ -203,15 +203,15 @@ def _(
|
|||
## Cannot use an argument as both a value and a type form
|
||||
|
||||
```py
|
||||
from ty_extensions import is_fully_static
|
||||
from ty_extensions import is_singleton
|
||||
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = repr
|
||||
else:
|
||||
f = is_fully_static
|
||||
f = is_singleton
|
||||
# error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call"
|
||||
reveal_type(f(int)) # revealed: str | Literal[True]
|
||||
reveal_type(f(int)) # revealed: str | Literal[False]
|
||||
```
|
||||
|
||||
## Size limit on unions of literals
|
||||
|
|
|
@ -90,11 +90,12 @@ from typing import Any
|
|||
|
||||
@dataclass
|
||||
class C:
|
||||
w: type[Any]
|
||||
x: Any
|
||||
y: int | Any
|
||||
z: tuple[int, Any]
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, x: Any, y: int | Any, z: tuple[int, Any]) -> None
|
||||
reveal_type(C.__init__) # revealed: (self: C, w: type[Any], x: Any, y: int | Any, z: tuple[int, Any]) -> None
|
||||
```
|
||||
|
||||
Variables without annotations are ignored:
|
||||
|
|
|
@ -22,7 +22,7 @@ Types that "produce" data on demand are covariant in their typevar. If you expec
|
|||
get from the sequence is a valid `int`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_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, Generic, TypeVar
|
||||
|
||||
class A: ...
|
||||
|
@ -53,11 +53,13 @@ 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[A], C[A]))
|
||||
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[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(D[B], C[A]))
|
||||
static_assert(not is_subtype_of(D[A], C[B]))
|
||||
|
@ -84,27 +86,11 @@ 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[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
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[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Unknown]))
|
||||
|
||||
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]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
|
||||
```
|
||||
|
||||
## Contravariance
|
||||
|
@ -117,7 +103,7 @@ Types that "consume" data are contravariant in their typevar. If you expect a co
|
|||
that you pass into the consumer is a valid `int`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_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, Generic, TypeVar
|
||||
|
||||
class A: ...
|
||||
|
@ -178,27 +164,11 @@ 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[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
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[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Unknown]))
|
||||
|
||||
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]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
|
||||
```
|
||||
|
||||
## Invariance
|
||||
|
@ -224,7 +194,7 @@ In the end, if you expect a mutable list, you must always be given a list of exa
|
|||
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_gradual_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, Generic, TypeVar
|
||||
|
||||
class A: ...
|
||||
|
@ -287,27 +257,11 @@ 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[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
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[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Unknown]))
|
||||
|
||||
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]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
|
||||
```
|
||||
|
||||
## Bivariance
|
||||
|
|
|
@ -113,33 +113,6 @@ class C[T]:
|
|||
reveal_type(x) # revealed: T
|
||||
```
|
||||
|
||||
## Fully static typevars
|
||||
|
||||
We consider a typevar to be fully static unless it has a non-fully-static bound or constraint. This
|
||||
is true even though a fully static typevar might be specialized to a gradual form like `Any`. (This
|
||||
is similar to how you can assign an expression whose type is not fully static to a target whose type
|
||||
is.)
|
||||
|
||||
```py
|
||||
from ty_extensions import is_fully_static, static_assert
|
||||
from typing import Any
|
||||
|
||||
def unbounded_unconstrained[T](t: T) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def bounded[T: int](t: T) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def bounded_by_gradual[T: Any](t: T) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
|
||||
def constrained[T: (int, str)](t: T) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def constrained_by_gradual[T: (int, Any)](t: T) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
```
|
||||
|
||||
## Subtyping and assignability
|
||||
|
||||
(Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static
|
||||
|
@ -372,14 +345,14 @@ def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
|
|||
|
||||
## Equivalence
|
||||
|
||||
A fully static `TypeVar` is always equivalent to itself, but never to another `TypeVar`, since there
|
||||
is no guarantee that they will be specialized to the same type. (This is true even if both typevars
|
||||
are bounded by the same final class, since you can specialize the typevars to `Never` in addition to
|
||||
A `TypeVar` is always equivalent to itself, but never to another `TypeVar`, since there is no
|
||||
guarantee that they will be specialized to the same type. (This is true even if both typevars are
|
||||
bounded by the same final class, since you can specialize the typevars to `Never` in addition to
|
||||
that final class.)
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
from ty_extensions import is_equivalent_to, static_assert, is_gradual_equivalent_to
|
||||
from ty_extensions import is_equivalent_to, static_assert
|
||||
|
||||
@final
|
||||
class FinalClass: ...
|
||||
|
@ -395,28 +368,16 @@ def f[A, B, C: FinalClass, D: FinalClass, E: (FinalClass, SecondFinalClass), F:
|
|||
static_assert(is_equivalent_to(E, E))
|
||||
static_assert(is_equivalent_to(F, F))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(A, A))
|
||||
static_assert(is_gradual_equivalent_to(B, B))
|
||||
static_assert(is_gradual_equivalent_to(C, C))
|
||||
static_assert(is_gradual_equivalent_to(D, D))
|
||||
static_assert(is_gradual_equivalent_to(E, E))
|
||||
static_assert(is_gradual_equivalent_to(F, F))
|
||||
|
||||
static_assert(not is_equivalent_to(A, B))
|
||||
static_assert(not is_equivalent_to(C, D))
|
||||
static_assert(not is_equivalent_to(E, F))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(A, B))
|
||||
static_assert(not is_gradual_equivalent_to(C, D))
|
||||
static_assert(not is_gradual_equivalent_to(E, F))
|
||||
```
|
||||
|
||||
TypeVars which have non-fully-static bounds or constraints do not participate in equivalence
|
||||
relations, but do participate in gradual equivalence relations.
|
||||
TypeVars which have non-fully-static bounds or constraints are also self-equivalent.
|
||||
|
||||
```py
|
||||
from typing import final, Any
|
||||
from ty_extensions import is_equivalent_to, static_assert, is_gradual_equivalent_to
|
||||
from ty_extensions import is_equivalent_to, static_assert
|
||||
|
||||
# fmt: off
|
||||
|
||||
|
@ -426,15 +387,10 @@ def f[
|
|||
C: (tuple[Any], tuple[Any, Any]),
|
||||
D: (tuple[Any], tuple[Any, Any])
|
||||
]():
|
||||
static_assert(not is_equivalent_to(A, A))
|
||||
static_assert(not is_equivalent_to(B, B))
|
||||
static_assert(not is_equivalent_to(C, C))
|
||||
static_assert(not is_equivalent_to(D, D))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(A, A))
|
||||
static_assert(is_gradual_equivalent_to(B, B))
|
||||
static_assert(is_gradual_equivalent_to(C, C))
|
||||
static_assert(is_gradual_equivalent_to(D, D))
|
||||
static_assert(is_equivalent_to(A, A))
|
||||
static_assert(is_equivalent_to(B, B))
|
||||
static_assert(is_equivalent_to(C, C))
|
||||
static_assert(is_equivalent_to(D, D))
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
|
|
@ -27,7 +27,7 @@ Types that "produce" data on demand are covariant in their typevar. If you expec
|
|||
get from the sequence is a valid `int`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_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
|
||||
|
||||
class A: ...
|
||||
|
@ -94,27 +94,11 @@ 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[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
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[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Unknown]))
|
||||
|
||||
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]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
|
||||
```
|
||||
|
||||
## Contravariance
|
||||
|
@ -127,7 +111,7 @@ Types that "consume" data are contravariant in their typevar. If you expect a co
|
|||
that you pass into the consumer is a valid `int`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_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
|
||||
|
||||
class A: ...
|
||||
|
@ -193,27 +177,11 @@ 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[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
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[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Unknown]))
|
||||
|
||||
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]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
|
||||
```
|
||||
|
||||
## Invariance
|
||||
|
@ -239,7 +207,7 @@ In the end, if you expect a mutable list, you must always be given a list of exa
|
|||
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_gradual_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
|
||||
|
||||
class A: ...
|
||||
|
@ -299,27 +267,11 @@ 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[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
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[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Unknown]))
|
||||
|
||||
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]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
|
||||
```
|
||||
|
||||
## Bivariance
|
||||
|
@ -333,7 +285,7 @@ at all. (If it did, it would have to be covariant, contravariant, or invariant,
|
|||
the typevar was used.)
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_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
|
||||
|
||||
class A: ...
|
||||
|
@ -359,6 +311,7 @@ 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(is_subtype_of(C[A], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[A], C[B]))
|
||||
|
@ -377,6 +330,7 @@ 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]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
|
@ -397,10 +351,18 @@ static_assert(is_equivalent_to(C[B], C[A]))
|
|||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||
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[B]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[A], C[Any]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[B], C[Any]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[Any], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(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]))
|
||||
|
@ -411,39 +373,11 @@ 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[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[B]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[Any]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[Any]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[B]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_equivalent_to(C[Any], C[Unknown]))
|
||||
|
||||
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]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
|
||||
```
|
||||
|
||||
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance
|
||||
|
|
|
@ -717,6 +717,18 @@ def never(
|
|||
reveal_type(d) # revealed: Never
|
||||
```
|
||||
|
||||
Regression tests for complex nested simplifications:
|
||||
|
||||
```py
|
||||
from typing_extensions import Any, assert_type
|
||||
|
||||
def _(x: Intersection[bool, Not[Intersection[Any, Not[AlwaysTruthy], Not[AlwaysFalsy]]]]):
|
||||
assert_type(x, bool)
|
||||
|
||||
def _(x: Intersection[bool, Any] | Literal[True] | Literal[False]):
|
||||
assert_type(x, bool)
|
||||
```
|
||||
|
||||
## Simplification of `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy`
|
||||
|
||||
Similarly, intersections between `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy` can be
|
||||
|
|
|
@ -1346,107 +1346,6 @@ def h(obj: InstanceAttrBool):
|
|||
reveal_type(bool(obj)) # revealed: bool
|
||||
```
|
||||
|
||||
## Fully static protocols; gradual protocols
|
||||
|
||||
A protocol is only fully static if all of its members are fully static:
|
||||
|
||||
```py
|
||||
from typing import Protocol, Any
|
||||
from ty_extensions import is_fully_static, static_assert
|
||||
|
||||
class FullyStatic(Protocol):
|
||||
x: int
|
||||
|
||||
class NotFullyStatic(Protocol):
|
||||
x: Any
|
||||
|
||||
static_assert(is_fully_static(FullyStatic))
|
||||
static_assert(not is_fully_static(NotFullyStatic))
|
||||
```
|
||||
|
||||
Non-fully-static protocols do not participate in subtyping or equivalence, only assignability and
|
||||
gradual equivalence:
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, is_assignable_to, is_equivalent_to, is_gradual_equivalent_to
|
||||
|
||||
class NominalWithX:
|
||||
x: int = 42
|
||||
|
||||
static_assert(is_assignable_to(NominalWithX, FullyStatic))
|
||||
static_assert(is_assignable_to(NominalWithX, NotFullyStatic))
|
||||
|
||||
static_assert(not is_subtype_of(FullyStatic, NotFullyStatic))
|
||||
static_assert(is_assignable_to(FullyStatic, NotFullyStatic))
|
||||
|
||||
static_assert(not is_subtype_of(NotFullyStatic, FullyStatic))
|
||||
static_assert(is_assignable_to(NotFullyStatic, FullyStatic))
|
||||
|
||||
static_assert(not is_subtype_of(NominalWithX, NotFullyStatic))
|
||||
static_assert(is_assignable_to(NominalWithX, NotFullyStatic))
|
||||
|
||||
static_assert(is_subtype_of(NominalWithX, FullyStatic))
|
||||
|
||||
static_assert(is_equivalent_to(FullyStatic, FullyStatic))
|
||||
static_assert(not is_equivalent_to(NotFullyStatic, NotFullyStatic))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(FullyStatic, FullyStatic))
|
||||
static_assert(is_gradual_equivalent_to(NotFullyStatic, NotFullyStatic))
|
||||
|
||||
class AlsoNotFullyStatic(Protocol):
|
||||
x: Any
|
||||
|
||||
static_assert(not is_equivalent_to(NotFullyStatic, AlsoNotFullyStatic))
|
||||
static_assert(is_gradual_equivalent_to(NotFullyStatic, AlsoNotFullyStatic))
|
||||
```
|
||||
|
||||
Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to
|
||||
the nominal type `object` (as described above):
|
||||
|
||||
```py
|
||||
class Empty(Protocol): ...
|
||||
|
||||
static_assert(is_fully_static(Empty))
|
||||
```
|
||||
|
||||
A method member is only considered fully static if all its parameter annotations and its return
|
||||
annotation are fully static:
|
||||
|
||||
```py
|
||||
class FullyStaticMethodMember(Protocol):
|
||||
def method(self, x: int) -> str: ...
|
||||
|
||||
class DynamicParameter(Protocol):
|
||||
def method(self, x: Any) -> str: ...
|
||||
|
||||
class DynamicReturn(Protocol):
|
||||
def method(self, x: int) -> Any: ...
|
||||
|
||||
static_assert(is_fully_static(FullyStaticMethodMember))
|
||||
|
||||
# TODO: these should pass
|
||||
static_assert(not is_fully_static(DynamicParameter)) # error: [static-assert-error]
|
||||
static_assert(not is_fully_static(DynamicReturn)) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
The [typing spec][spec_protocol_members] states:
|
||||
|
||||
> If any parameters of a protocol method are not annotated, then their types are assumed to be `Any`
|
||||
|
||||
Thus, a partially unannotated method member can also not be considered to be fully static:
|
||||
|
||||
```py
|
||||
class NoParameterAnnotation(Protocol):
|
||||
def method(self, x) -> str: ...
|
||||
|
||||
class NoReturnAnnotation(Protocol):
|
||||
def method(self, x: int): ...
|
||||
|
||||
# TODO: these should pass
|
||||
static_assert(not is_fully_static(NoParameterAnnotation)) # error: [static-assert-error]
|
||||
static_assert(not is_fully_static(NoReturnAnnotation)) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
## Callable protocols
|
||||
|
||||
An instance of a protocol type is callable if the protocol defines a `__call__` method:
|
||||
|
@ -1560,7 +1459,7 @@ def two(some_list: list, some_tuple: tuple[int, str], some_sized: Sized):
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol, Any
|
||||
from ty_extensions import is_fully_static, static_assert, is_assignable_to, is_subtype_of, is_equivalent_to
|
||||
from ty_extensions import static_assert, is_assignable_to, is_subtype_of, is_equivalent_to
|
||||
|
||||
class RecursiveFullyStatic(Protocol):
|
||||
parent: RecursiveFullyStatic
|
||||
|
@ -1570,11 +1469,9 @@ class RecursiveNonFullyStatic(Protocol):
|
|||
parent: RecursiveNonFullyStatic
|
||||
x: Any
|
||||
|
||||
static_assert(is_fully_static(RecursiveFullyStatic))
|
||||
static_assert(not is_fully_static(RecursiveNonFullyStatic))
|
||||
|
||||
static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic))
|
||||
static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic))
|
||||
# TODO: these should pass, once we take into account types of members
|
||||
static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic)) # error: [static-assert-error]
|
||||
static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic)) # error: [static-assert-error]
|
||||
|
||||
static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveNonFullyStatic))
|
||||
static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic))
|
||||
|
@ -1589,8 +1486,6 @@ static_assert(is_equivalent_to(AlsoRecursiveFullyStatic, RecursiveFullyStatic))
|
|||
class RecursiveOptionalParent(Protocol):
|
||||
parent: RecursiveOptionalParent | None
|
||||
|
||||
static_assert(is_fully_static(RecursiveOptionalParent))
|
||||
|
||||
static_assert(is_assignable_to(RecursiveOptionalParent, RecursiveOptionalParent))
|
||||
|
||||
static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveOptionalParent))
|
||||
|
@ -1635,7 +1530,7 @@ python-version = "3.12"
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol, Callable
|
||||
from ty_extensions import Intersection, Not, is_fully_static, is_assignable_to, is_equivalent_to, static_assert
|
||||
from ty_extensions import Intersection, Not, is_assignable_to, is_equivalent_to, static_assert
|
||||
|
||||
class C: ...
|
||||
|
||||
|
@ -1663,7 +1558,6 @@ class Recursive(Protocol):
|
|||
|
||||
nested: Recursive | Callable[[Recursive | Recursive, tuple[Recursive, Recursive]], Recursive | Recursive]
|
||||
|
||||
static_assert(is_fully_static(Recursive))
|
||||
static_assert(is_equivalent_to(Recursive, Recursive))
|
||||
static_assert(is_assignable_to(Recursive, Recursive))
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ error[parameter-already-assigned]: Multiple values provided for parameter `name`
|
|||
| ^^^^^^^^^^
|
||||
|
|
||||
info: Union variant `def f1(name: str) -> int` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f1(name: str) -> int) | (def any(*args, **kwargs) -> int)`
|
||||
info: Attempted to call union type `(def f1(name: str) -> int) | (def any(...) -> int)`
|
||||
info: rule `parameter-already-assigned` is enabled by default
|
||||
|
||||
```
|
||||
|
@ -55,7 +55,7 @@ error[unknown-argument]: Argument `unknown` does not match any known parameter o
|
|||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
info: Union variant `def f1(name: str) -> int` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f1(name: str) -> int) | (def any(*args, **kwargs) -> int)`
|
||||
info: Attempted to call union type `(def f1(name: str) -> int) | (def any(...) -> int)`
|
||||
info: rule `unknown-argument` is enabled by default
|
||||
|
||||
```
|
||||
|
|
|
@ -91,13 +91,11 @@ The `Unknown` type is a special type that we use to represent actually unknown t
|
|||
annotation), as opposed to `Any` which represents an explicitly unknown type.
|
||||
|
||||
```py
|
||||
from ty_extensions import Unknown, static_assert, is_assignable_to, is_fully_static
|
||||
from ty_extensions import Unknown, static_assert, is_assignable_to
|
||||
|
||||
static_assert(is_assignable_to(Unknown, int))
|
||||
static_assert(is_assignable_to(int, Unknown))
|
||||
|
||||
static_assert(not is_fully_static(Unknown))
|
||||
|
||||
def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None:
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: tuple[str, Unknown]
|
||||
|
@ -333,19 +331,6 @@ static_assert(is_disjoint_from(None, int))
|
|||
static_assert(not is_disjoint_from(Literal[2] | str, int))
|
||||
```
|
||||
|
||||
### Fully static types
|
||||
|
||||
```py
|
||||
from ty_extensions import is_fully_static, static_assert
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_fully_static(int | str))
|
||||
static_assert(is_fully_static(type[int]))
|
||||
|
||||
static_assert(not is_fully_static(int | Any))
|
||||
static_assert(not is_fully_static(type[Any]))
|
||||
```
|
||||
|
||||
### Singleton types
|
||||
|
||||
```py
|
||||
|
|
|
@ -2,24 +2,13 @@
|
|||
|
||||
## Introduction
|
||||
|
||||
The type `Any` is the dynamic type in Python's gradual type system. It represents an unknown
|
||||
fully-static type, which means that it represents an *unknown* set of runtime values.
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_fully_static
|
||||
from typing import Any
|
||||
```
|
||||
|
||||
`Any` is a dynamic type:
|
||||
|
||||
```py
|
||||
static_assert(not is_fully_static(Any))
|
||||
```
|
||||
The type `Any` is the dynamic type in Python's gradual type system. It represents an unknown static
|
||||
type, which means that it represents an *unknown* set of runtime values.
|
||||
|
||||
## Every type is assignable to `Any`, and `Any` is assignable to every type
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_fully_static, is_assignable_to
|
||||
from ty_extensions import static_assert, is_assignable_to
|
||||
from typing_extensions import Never, Any
|
||||
|
||||
class C: ...
|
||||
|
|
|
@ -113,8 +113,8 @@ static_assert(is_equivalent_to(Not[Intersection[P, Q]], Not[P] | Not[Q]))
|
|||
The two gradual types are equivalent:
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_gradual_equivalent_to, Not
|
||||
from ty_extensions import static_assert, is_equivalent_to, Not
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_gradual_equivalent_to(Not[Any], Any))
|
||||
static_assert(is_equivalent_to(Not[Any], Any))
|
||||
```
|
||||
|
|
|
@ -36,8 +36,7 @@ static_assert(not is_assignable_to(Child1, Child2))
|
|||
|
||||
### Gradual types
|
||||
|
||||
Gradual types do not participate in subtyping, but can still be assignable to other types (and
|
||||
static types can be assignable to gradual types):
|
||||
The dynamic type is assignable to or from any type.
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_assignable_to, Unknown
|
||||
|
@ -47,13 +46,6 @@ static_assert(is_assignable_to(Unknown, Literal[1]))
|
|||
static_assert(is_assignable_to(Any, Literal[1]))
|
||||
static_assert(is_assignable_to(Literal[1], Unknown))
|
||||
static_assert(is_assignable_to(Literal[1], Any))
|
||||
|
||||
class SubtypeOfAny(Any): ...
|
||||
|
||||
static_assert(is_assignable_to(SubtypeOfAny, Any))
|
||||
static_assert(is_assignable_to(SubtypeOfAny, int))
|
||||
static_assert(is_assignable_to(Any, SubtypeOfAny))
|
||||
static_assert(not is_assignable_to(int, SubtypeOfAny))
|
||||
```
|
||||
|
||||
## Literal types
|
||||
|
@ -239,7 +231,9 @@ from ty_extensions import is_assignable_to, static_assert
|
|||
static_assert(not is_assignable_to(type[Any], None))
|
||||
```
|
||||
|
||||
## Class-literals that inherit from `Any`
|
||||
## Inheriting `Any`
|
||||
|
||||
### Class-literal types
|
||||
|
||||
Class-literal types that inherit from `Any` are assignable to any type `T` where `T` is assignable
|
||||
to `type`:
|
||||
|
@ -267,6 +261,39 @@ def test(x: Any):
|
|||
|
||||
This is because the `Any` element in the MRO could materialize to any subtype of `type`.
|
||||
|
||||
### Nominal instance and subclass-of types
|
||||
|
||||
Instances of classes that inherit `Any` are assignable to any non-final type.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, static_assert
|
||||
from typing_extensions import Any, final
|
||||
|
||||
class InheritsAny(Any):
|
||||
pass
|
||||
|
||||
class Arbitrary:
|
||||
pass
|
||||
|
||||
@final
|
||||
class FinalClass:
|
||||
pass
|
||||
|
||||
static_assert(is_assignable_to(InheritsAny, Arbitrary))
|
||||
static_assert(is_assignable_to(InheritsAny, Any))
|
||||
static_assert(is_assignable_to(InheritsAny, object))
|
||||
static_assert(not is_assignable_to(InheritsAny, FinalClass))
|
||||
```
|
||||
|
||||
Similar for subclass-of types:
|
||||
|
||||
```py
|
||||
static_assert(is_assignable_to(type[Any], type[Any]))
|
||||
static_assert(is_assignable_to(type[object], type[Any]))
|
||||
static_assert(is_assignable_to(type[Any], type[Arbitrary]))
|
||||
static_assert(is_assignable_to(type[Any], type[object]))
|
||||
```
|
||||
|
||||
## Heterogeneous tuple types
|
||||
|
||||
```py
|
||||
|
|
|
@ -193,8 +193,8 @@ static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3]))
|
|||
## Intersections
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, final, Any
|
||||
from ty_extensions import Intersection, is_disjoint_from, static_assert, Not
|
||||
from typing_extensions import Literal, final, Any, LiteralString
|
||||
from ty_extensions import Intersection, is_disjoint_from, static_assert, Not, AlwaysFalsy
|
||||
|
||||
@final
|
||||
class P: ...
|
||||
|
@ -249,6 +249,9 @@ static_assert(not is_disjoint_from(Intersection[Any, Not[Y]], Intersection[Any,
|
|||
|
||||
static_assert(is_disjoint_from(Intersection[int, Any], Not[int]))
|
||||
static_assert(is_disjoint_from(Not[int], Intersection[int, Any]))
|
||||
|
||||
# TODO https://github.com/astral-sh/ty/issues/216
|
||||
static_assert(is_disjoint_from(AlwaysFalsy, LiteralString & ~Literal[""])) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
## Special types
|
||||
|
|
|
@ -1,41 +1,76 @@
|
|||
# Equivalence relation
|
||||
|
||||
`is_equivalent_to` implements [the equivalence relation] for fully static types.
|
||||
`is_equivalent_to` implements [the equivalence relation] on types.
|
||||
|
||||
Two types `A` and `B` are equivalent iff `A` is a subtype of `B` and `B` is a subtype of `A`.
|
||||
For fully static types, two types `A` and `B` are equivalent iff `A` is a subtype of `B` and `B` is
|
||||
a subtype of `A` (that is, the two types represent the same set of values).
|
||||
|
||||
Two gradual types `A` and `B` are equivalent if all [materializations] of `A` are also
|
||||
materializations of `B`, and all materializations of `B` are also materializations of `A`.
|
||||
|
||||
## Basic
|
||||
|
||||
### Fully static
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import Literal
|
||||
from ty_extensions import Unknown, is_equivalent_to, static_assert
|
||||
from typing_extensions import Literal, LiteralString, Never
|
||||
from ty_extensions import Unknown, is_equivalent_to, static_assert, TypeOf, AlwaysTruthy, AlwaysFalsy
|
||||
|
||||
static_assert(is_equivalent_to(Literal[1, 2], Literal[1, 2]))
|
||||
static_assert(is_equivalent_to(type[object], type))
|
||||
|
||||
static_assert(not is_equivalent_to(Any, Any))
|
||||
static_assert(not is_equivalent_to(Unknown, Unknown))
|
||||
static_assert(not is_equivalent_to(Any, None))
|
||||
static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 0]))
|
||||
static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 2, 3]))
|
||||
```
|
||||
|
||||
## Equivalence is commutative
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
from ty_extensions import is_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_equivalent_to(type, type[object]))
|
||||
|
||||
static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 0]))
|
||||
static_assert(not is_equivalent_to(Literal[1, 0], Literal[1, 2]))
|
||||
static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 2, 3]))
|
||||
static_assert(not is_equivalent_to(Literal[1, 2, 3], Literal[1, 2]))
|
||||
|
||||
static_assert(is_equivalent_to(Never, Never))
|
||||
static_assert(is_equivalent_to(AlwaysTruthy, AlwaysTruthy))
|
||||
static_assert(is_equivalent_to(AlwaysFalsy, AlwaysFalsy))
|
||||
static_assert(is_equivalent_to(LiteralString, LiteralString))
|
||||
|
||||
static_assert(is_equivalent_to(Literal[True], Literal[True]))
|
||||
static_assert(is_equivalent_to(Literal[False], Literal[False]))
|
||||
static_assert(is_equivalent_to(TypeOf[0:1:2], TypeOf[0:1:2]))
|
||||
|
||||
static_assert(is_equivalent_to(TypeOf[str], TypeOf[str]))
|
||||
static_assert(is_equivalent_to(type, type[object]))
|
||||
```
|
||||
|
||||
## Differently ordered intersections and unions are equivalent
|
||||
### Gradual
|
||||
|
||||
```py
|
||||
from ty_extensions import is_equivalent_to, static_assert, Intersection, Not
|
||||
from typing import Any
|
||||
from typing_extensions import Literal, LiteralString, Never
|
||||
from ty_extensions import Unknown, is_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_equivalent_to(Any, Any))
|
||||
static_assert(is_equivalent_to(Unknown, Unknown))
|
||||
static_assert(is_equivalent_to(Any, Unknown))
|
||||
static_assert(not is_equivalent_to(Any, None))
|
||||
|
||||
static_assert(not is_equivalent_to(type, type[Any]))
|
||||
static_assert(not is_equivalent_to(type[object], type[Any]))
|
||||
```
|
||||
|
||||
## Unions and intersections
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import Intersection, Not, Unknown, is_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_equivalent_to(str | int, str | int))
|
||||
static_assert(is_equivalent_to(str | int | Any, str | int | Unknown))
|
||||
static_assert(is_equivalent_to(str | int, int | str))
|
||||
static_assert(is_equivalent_to(Intersection[str, int, Not[bytes], Not[None]], Intersection[int, str, Not[None], Not[bytes]]))
|
||||
static_assert(is_equivalent_to(Intersection[str | int, Not[type[Any]]], Intersection[int | str, Not[type[Unknown]]]))
|
||||
|
||||
static_assert(not is_equivalent_to(str | int, int | str | bytes))
|
||||
static_assert(not is_equivalent_to(str | int | bytes, int | str | dict))
|
||||
|
||||
static_assert(is_equivalent_to(Unknown, Unknown | Any))
|
||||
static_assert(is_equivalent_to(Unknown, Intersection[Unknown, Any]))
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
@ -66,6 +101,18 @@ static_assert(is_equivalent_to(Intersection[Q, R, Not[P]], Intersection[Not[P],
|
|||
static_assert(is_equivalent_to(Intersection[Q | R, Not[P | S]], Intersection[Not[S | P], R | Q]))
|
||||
```
|
||||
|
||||
## Tuples
|
||||
|
||||
```py
|
||||
from ty_extensions import Unknown, is_equivalent_to, static_assert
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_equivalent_to(tuple[str, Any], tuple[str, Unknown]))
|
||||
|
||||
static_assert(not is_equivalent_to(tuple[str, int], tuple[str, int, bytes]))
|
||||
static_assert(not is_equivalent_to(tuple[str, int], tuple[int, str]))
|
||||
```
|
||||
|
||||
## Tuples containing equivalent but differently ordered unions/intersections are equivalent
|
||||
|
||||
```py
|
||||
|
@ -193,21 +240,14 @@ def f2(a: int, b: int) -> None: ...
|
|||
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
|
||||
```
|
||||
|
||||
When either of the callable types uses a gradual form for the parameters:
|
||||
|
||||
```py
|
||||
static_assert(not is_equivalent_to(Callable[..., None], Callable[[int], None]))
|
||||
static_assert(not is_equivalent_to(Callable[[int], None], Callable[..., None]))
|
||||
```
|
||||
|
||||
When the return types are not equivalent or absent in one or both of the callable types:
|
||||
When the return types are not equivalent in one or both of the callable types:
|
||||
|
||||
```py
|
||||
def f3(): ...
|
||||
def f4() -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(Callable[[], int], Callable[[], None]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3]))
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f4], CallableTypeOf[f3]))
|
||||
```
|
||||
|
@ -247,7 +287,7 @@ def f11(a) -> None: ...
|
|||
static_assert(not is_equivalent_to(CallableTypeOf[f9], CallableTypeOf[f10]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f10], CallableTypeOf[f11]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f10]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f11]))
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f11]))
|
||||
```
|
||||
|
||||
When the default value for a parameter is present only in one of the callable type:
|
||||
|
@ -334,10 +374,9 @@ static_assert(is_equivalent_to(CallableTypeOf[pg], CallableTypeOf[cpg]))
|
|||
static_assert(is_equivalent_to(CallableTypeOf[cpg], CallableTypeOf[pg]))
|
||||
```
|
||||
|
||||
## Function-literal types and bound-method types
|
||||
### Function-literal types and bound-method types
|
||||
|
||||
Function-literal types and bound-method types are always considered self-equivalent, even if they
|
||||
have unannotated parameters, or parameters with not-fully-static annotations.
|
||||
Function-literal types and bound-method types are always considered self-equivalent.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
|
@ -360,4 +399,94 @@ type X = TypeOf[A.method]
|
|||
static_assert(is_equivalent_to(X, X))
|
||||
```
|
||||
|
||||
### Non-fully-static callable types
|
||||
|
||||
The examples provided below are only a subset of the possible cases and only include the ones with
|
||||
gradual types. The cases with fully static types and using different combinations of parameter kinds
|
||||
are covered above.
|
||||
|
||||
```py
|
||||
from ty_extensions import Unknown, CallableTypeOf, is_equivalent_to, static_assert
|
||||
from typing import Any, Callable
|
||||
|
||||
static_assert(is_equivalent_to(Callable[..., int], Callable[..., int]))
|
||||
static_assert(is_equivalent_to(Callable[..., Any], Callable[..., Unknown]))
|
||||
static_assert(is_equivalent_to(Callable[[int, Any], None], Callable[[int, Unknown], None]))
|
||||
|
||||
static_assert(not is_equivalent_to(Callable[[int, Any], None], Callable[[Any, int], None]))
|
||||
static_assert(not is_equivalent_to(Callable[[int, str], None], Callable[[int, str, bytes], None]))
|
||||
static_assert(not is_equivalent_to(Callable[..., None], Callable[[], None]))
|
||||
```
|
||||
|
||||
A function with no explicit return type should be gradual equivalent to a callable with a return
|
||||
type of `Any`.
|
||||
|
||||
```py
|
||||
def f1():
|
||||
return
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f1], Callable[[], Any]))
|
||||
```
|
||||
|
||||
And, similarly for parameters with no annotations.
|
||||
|
||||
```py
|
||||
def f2(a, b, /) -> None:
|
||||
return
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None]))
|
||||
```
|
||||
|
||||
Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs`
|
||||
parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable
|
||||
with `...` as the parameter type.
|
||||
|
||||
```py
|
||||
def variadic_without_annotation(*args, **kwargs):
|
||||
return
|
||||
|
||||
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
|
||||
return
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any]))
|
||||
static_assert(is_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any]))
|
||||
```
|
||||
|
||||
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
|
||||
callable with `...` as the parameter type.
|
||||
|
||||
```py
|
||||
def variadic_args(*args):
|
||||
return
|
||||
|
||||
def variadic_kwargs(**kwargs):
|
||||
return
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any]))
|
||||
```
|
||||
|
||||
Parameter names, default values, and it's kind should also be considered when checking for gradual
|
||||
equivalence.
|
||||
|
||||
```py
|
||||
def f1(a): ...
|
||||
def f2(b): ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
|
||||
|
||||
def f3(a=1): ...
|
||||
def f4(a=2): ...
|
||||
def f5(a): ...
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5]))
|
||||
|
||||
def f6(a, /): ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
|
||||
```
|
||||
|
||||
[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
|
||||
[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
# Fully-static types
|
||||
|
||||
A type is fully static iff it does not contain any gradual forms.
|
||||
|
||||
## Fully-static
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString, Never, Callable
|
||||
from ty_extensions import Intersection, Not, TypeOf, is_fully_static, static_assert
|
||||
|
||||
static_assert(is_fully_static(Never))
|
||||
static_assert(is_fully_static(None))
|
||||
|
||||
static_assert(is_fully_static(Literal[1]))
|
||||
static_assert(is_fully_static(Literal[True]))
|
||||
static_assert(is_fully_static(Literal["abc"]))
|
||||
static_assert(is_fully_static(Literal[b"abc"]))
|
||||
|
||||
static_assert(is_fully_static(LiteralString))
|
||||
|
||||
static_assert(is_fully_static(str))
|
||||
static_assert(is_fully_static(object))
|
||||
static_assert(is_fully_static(type))
|
||||
|
||||
static_assert(is_fully_static(TypeOf[str]))
|
||||
static_assert(is_fully_static(TypeOf[Literal]))
|
||||
|
||||
static_assert(is_fully_static(str | None))
|
||||
static_assert(is_fully_static(Intersection[str, Not[LiteralString]]))
|
||||
|
||||
static_assert(is_fully_static(tuple[()]))
|
||||
static_assert(is_fully_static(tuple[int, object]))
|
||||
|
||||
static_assert(is_fully_static(type[str]))
|
||||
static_assert(is_fully_static(type[object]))
|
||||
```
|
||||
|
||||
## Non-fully-static
|
||||
|
||||
```py
|
||||
from typing_extensions import Any, Literal, LiteralString, Callable
|
||||
from ty_extensions import Intersection, Not, TypeOf, Unknown, is_fully_static, static_assert
|
||||
|
||||
static_assert(not is_fully_static(Any))
|
||||
static_assert(not is_fully_static(Unknown))
|
||||
|
||||
static_assert(not is_fully_static(Any | str))
|
||||
static_assert(not is_fully_static(str | Unknown))
|
||||
static_assert(not is_fully_static(Intersection[Any, Not[LiteralString]]))
|
||||
|
||||
static_assert(not is_fully_static(tuple[Any, ...]))
|
||||
|
||||
static_assert(not is_fully_static(tuple[int, Any]))
|
||||
static_assert(not is_fully_static(type[Any]))
|
||||
```
|
||||
|
||||
## Callable
|
||||
|
||||
```py
|
||||
from typing_extensions import Callable, Any
|
||||
from ty_extensions import Unknown, is_fully_static, static_assert
|
||||
|
||||
static_assert(is_fully_static(Callable[[], int]))
|
||||
static_assert(is_fully_static(Callable[[int, str], int]))
|
||||
|
||||
static_assert(not is_fully_static(Callable[..., int]))
|
||||
static_assert(not is_fully_static(Callable[[], Any]))
|
||||
static_assert(not is_fully_static(Callable[[int, Unknown], int]))
|
||||
```
|
||||
|
||||
The invalid forms of `Callable` annotation are never fully static because we represent them with the
|
||||
`(...) -> Unknown` signature.
|
||||
|
||||
```py
|
||||
static_assert(not is_fully_static(Callable))
|
||||
# error: [invalid-type-form]
|
||||
static_assert(not is_fully_static(Callable[int, int]))
|
||||
```
|
||||
|
||||
Using function literals, we can check more variations of callable types as it allows us to define
|
||||
parameters without annotations and no return type.
|
||||
|
||||
```py
|
||||
from ty_extensions import CallableTypeOf, is_fully_static, static_assert
|
||||
|
||||
def f00() -> None: ...
|
||||
def f01(a: int, b: str) -> None: ...
|
||||
def f11(): ...
|
||||
def f12(a, b): ...
|
||||
def f13(a, b: int): ...
|
||||
def f14(a, b: int) -> None: ...
|
||||
def f15(a, b) -> None: ...
|
||||
|
||||
static_assert(is_fully_static(CallableTypeOf[f00]))
|
||||
static_assert(is_fully_static(CallableTypeOf[f01]))
|
||||
|
||||
static_assert(not is_fully_static(CallableTypeOf[f11]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f12]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f13]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f14]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f15]))
|
||||
```
|
||||
|
||||
## Overloads
|
||||
|
||||
`overloaded.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import Any, overload
|
||||
|
||||
@overload
|
||||
def gradual() -> None: ...
|
||||
@overload
|
||||
def gradual(a: Any) -> None: ...
|
||||
|
||||
@overload
|
||||
def static() -> None: ...
|
||||
@overload
|
||||
def static(x: int) -> None: ...
|
||||
@overload
|
||||
def static(x: str) -> str: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from ty_extensions import CallableTypeOf, TypeOf, is_fully_static, static_assert
|
||||
from overloaded import gradual, static
|
||||
|
||||
static_assert(is_fully_static(TypeOf[gradual]))
|
||||
static_assert(is_fully_static(TypeOf[static]))
|
||||
|
||||
static_assert(not is_fully_static(CallableTypeOf[gradual]))
|
||||
static_assert(is_fully_static(CallableTypeOf[static]))
|
||||
```
|
|
@ -1,159 +0,0 @@
|
|||
# Gradual equivalence relation
|
||||
|
||||
Two gradual types `A` and `B` are equivalent if all [materializations] of `A` are also
|
||||
materializations of `B`, and all materializations of `B` are also materializations of `A`.
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import Literal, LiteralString, Never
|
||||
from ty_extensions import AlwaysFalsy, AlwaysTruthy, TypeOf, Unknown, is_gradual_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_gradual_equivalent_to(Any, Any))
|
||||
static_assert(is_gradual_equivalent_to(Unknown, Unknown))
|
||||
static_assert(is_gradual_equivalent_to(Any, Unknown))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(Never, Never))
|
||||
static_assert(is_gradual_equivalent_to(AlwaysTruthy, AlwaysTruthy))
|
||||
static_assert(is_gradual_equivalent_to(AlwaysFalsy, AlwaysFalsy))
|
||||
static_assert(is_gradual_equivalent_to(LiteralString, LiteralString))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(Literal[True], Literal[True]))
|
||||
static_assert(is_gradual_equivalent_to(Literal[False], Literal[False]))
|
||||
static_assert(is_gradual_equivalent_to(TypeOf[0:1:2], TypeOf[0:1:2]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(TypeOf[str], TypeOf[str]))
|
||||
static_assert(is_gradual_equivalent_to(type, type[object]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(type, type[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(type[object], type[Any]))
|
||||
```
|
||||
|
||||
## Unions and intersections
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import Intersection, Not, Unknown, is_gradual_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_gradual_equivalent_to(str | int, str | int))
|
||||
static_assert(is_gradual_equivalent_to(str | int | Any, str | int | Unknown))
|
||||
static_assert(is_gradual_equivalent_to(str | int, int | str))
|
||||
static_assert(
|
||||
is_gradual_equivalent_to(Intersection[str, int, Not[bytes], Not[None]], Intersection[int, str, Not[None], Not[bytes]])
|
||||
)
|
||||
static_assert(is_gradual_equivalent_to(Intersection[str | int, Not[type[Any]]], Intersection[int | str, Not[type[Unknown]]]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(str | int, int | str | bytes))
|
||||
static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(Unknown, Unknown | Any))
|
||||
static_assert(is_gradual_equivalent_to(Unknown, Intersection[Unknown, Any]))
|
||||
```
|
||||
|
||||
## Tuples
|
||||
|
||||
```py
|
||||
from ty_extensions import Unknown, is_gradual_equivalent_to, static_assert
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_gradual_equivalent_to(tuple[str, Any], tuple[str, Unknown]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[str, int, bytes]))
|
||||
static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str]))
|
||||
```
|
||||
|
||||
## Callable
|
||||
|
||||
The examples provided below are only a subset of the possible cases and only include the ones with
|
||||
gradual types. The cases with fully static types and using different combinations of parameter kinds
|
||||
are covered in the [equivalence tests](./is_equivalent_to.md#callable).
|
||||
|
||||
```py
|
||||
from ty_extensions import Unknown, CallableTypeOf, is_gradual_equivalent_to, static_assert
|
||||
from typing import Any, Callable
|
||||
|
||||
static_assert(is_gradual_equivalent_to(Callable[..., int], Callable[..., int]))
|
||||
static_assert(is_gradual_equivalent_to(Callable[..., Any], Callable[..., Unknown]))
|
||||
static_assert(is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[int, Unknown], None]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[Any, int], None]))
|
||||
static_assert(not is_gradual_equivalent_to(Callable[[int, str], None], Callable[[int, str, bytes], None]))
|
||||
static_assert(not is_gradual_equivalent_to(Callable[..., None], Callable[[], None]))
|
||||
```
|
||||
|
||||
A function with no explicit return type should be gradual equivalent to a callable with a return
|
||||
type of `Any`.
|
||||
|
||||
```py
|
||||
def f1():
|
||||
return
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[f1], Callable[[], Any]))
|
||||
```
|
||||
|
||||
And, similarly for parameters with no annotations.
|
||||
|
||||
```py
|
||||
def f2(a, b, /) -> None:
|
||||
return
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None]))
|
||||
```
|
||||
|
||||
Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs`
|
||||
parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable
|
||||
with `...` as the parameter type.
|
||||
|
||||
```py
|
||||
def variadic_without_annotation(*args, **kwargs):
|
||||
return
|
||||
|
||||
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
|
||||
return
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any]))
|
||||
```
|
||||
|
||||
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
|
||||
callable with `...` as the parameter type.
|
||||
|
||||
```py
|
||||
def variadic_args(*args):
|
||||
return
|
||||
|
||||
def variadic_kwargs(**kwargs):
|
||||
return
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any]))
|
||||
```
|
||||
|
||||
Parameter names, default values, and it's kind should also be considered when checking for gradual
|
||||
equivalence.
|
||||
|
||||
```py
|
||||
def f1(a): ...
|
||||
def f2(b): ...
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
|
||||
|
||||
def f3(a=1): ...
|
||||
def f4(a=2): ...
|
||||
def f5(a): ...
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
|
||||
static_assert(
|
||||
is_gradual_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3])
|
||||
)
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5]))
|
||||
|
||||
def f6(a, /): ...
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
|
||||
```
|
||||
|
||||
TODO: Overloads
|
||||
|
||||
[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
|
|
@ -10,6 +10,10 @@ The `is_subtype_of(S, T)` relation below checks if type `S` is a subtype of type
|
|||
A fully static type `S` is a subtype of another fully static type `T` iff the set of values
|
||||
represented by `S` is a subset of the set of values represented by `T`.
|
||||
|
||||
A non fully static type `S` can also be safely considered a subtype of a non fully static type `T`,
|
||||
if all possible materializations of `S` represent sets of values that are a subset of every possible
|
||||
set of values represented by a materialization of `T`.
|
||||
|
||||
See the [typing documentation] for more information.
|
||||
|
||||
## Basic builtin types
|
||||
|
@ -316,12 +320,13 @@ static_assert(
|
|||
python-version = "3.12"
|
||||
```
|
||||
|
||||
As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type.
|
||||
However, the special-case behavior of assignability does not also apply to subtyping, since gradual
|
||||
types to not participate in subtyping.
|
||||
As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type, not
|
||||
only in the type of its elements, but also in its length.
|
||||
|
||||
Its subtyping follows the general rule for subtyping of gradual types.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing import Any, Never
|
||||
from ty_extensions import static_assert, is_subtype_of
|
||||
|
||||
static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, ...]))
|
||||
|
@ -330,9 +335,11 @@ static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, Any]))
|
|||
static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, ...]))
|
||||
static_assert(not is_subtype_of(tuple[Any, ...], tuple[int]))
|
||||
static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, int]))
|
||||
static_assert(is_subtype_of(tuple[Any, ...], tuple[object, ...]))
|
||||
static_assert(is_subtype_of(tuple[Never, ...], tuple[Any, ...]))
|
||||
```
|
||||
|
||||
Subtyping also does not apply when `tuple[Any, ...]` is unpacked into a mixed tuple.
|
||||
Same applies when `tuple[Any, ...]` is unpacked into a mixed tuple.
|
||||
|
||||
```py
|
||||
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[Any, ...]]))
|
||||
|
@ -363,9 +370,9 @@ static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int]))
|
|||
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, int]))
|
||||
```
|
||||
|
||||
Subtyping does apply to unbounded homogeneous tuples of a fully static type. However, such tuples
|
||||
are defined to be the _union_ of all tuple lengths, not the _gradual choice_ of them, so no
|
||||
variable-length tuples are a subtyping of _any_ fixed-length tuple.
|
||||
Unbounded homogeneous tuples of a non-Any type are defined to be the _union_ of all tuple lengths,
|
||||
not the _gradual choice_ of them, so no variable-length tuples are a subtype of _any_ fixed-length
|
||||
tuple.
|
||||
|
||||
```py
|
||||
static_assert(not is_subtype_of(tuple[int, ...], tuple[Any, ...]))
|
||||
|
@ -642,7 +649,7 @@ static_assert(not is_subtype_of(_SpecialForm, TypeOf[Literal]))
|
|||
### Basic
|
||||
|
||||
```py
|
||||
from typing import _SpecialForm
|
||||
from typing import _SpecialForm, Any
|
||||
from typing_extensions import Literal, assert_type
|
||||
from ty_extensions import TypeOf, is_subtype_of, static_assert
|
||||
|
||||
|
@ -674,6 +681,8 @@ static_assert(not is_subtype_of(LiteralBool, bool))
|
|||
|
||||
static_assert(not is_subtype_of(type, type[bool]))
|
||||
|
||||
static_assert(not is_subtype_of(LiteralBool, type[Any]))
|
||||
|
||||
# int
|
||||
|
||||
static_assert(is_subtype_of(LiteralInt, LiteralInt))
|
||||
|
@ -687,7 +696,9 @@ static_assert(not is_subtype_of(LiteralInt, int))
|
|||
|
||||
static_assert(not is_subtype_of(type, type[int]))
|
||||
|
||||
# LiteralString
|
||||
static_assert(not is_subtype_of(LiteralInt, type[Any]))
|
||||
|
||||
# str
|
||||
|
||||
static_assert(is_subtype_of(LiteralStr, type[str]))
|
||||
static_assert(is_subtype_of(LiteralStr, type))
|
||||
|
@ -695,6 +706,8 @@ static_assert(is_subtype_of(LiteralStr, type[object]))
|
|||
|
||||
static_assert(not is_subtype_of(type[str], LiteralStr))
|
||||
|
||||
static_assert(not is_subtype_of(LiteralStr, type[Any]))
|
||||
|
||||
# custom metaclasses
|
||||
|
||||
type LiteralHasCustomMetaclass = TypeOf[HasCustomMetaclass]
|
||||
|
@ -704,6 +717,18 @@ static_assert(is_subtype_of(Meta, type[object]))
|
|||
static_assert(is_subtype_of(Meta, type))
|
||||
|
||||
static_assert(not is_subtype_of(Meta, type[type]))
|
||||
|
||||
static_assert(not is_subtype_of(Meta, type[Any]))
|
||||
|
||||
# generics
|
||||
|
||||
type LiteralListOfInt = TypeOf[list[int]]
|
||||
|
||||
assert_type(list[int], LiteralListOfInt)
|
||||
|
||||
static_assert(is_subtype_of(LiteralListOfInt, type))
|
||||
|
||||
static_assert(not is_subtype_of(LiteralListOfInt, type[Any]))
|
||||
```
|
||||
|
||||
### Unions of class literals
|
||||
|
@ -740,7 +765,9 @@ static_assert(is_subtype_of(LiteralBase | LiteralUnrelated, object))
|
|||
|
||||
## Non-fully-static types
|
||||
|
||||
`Any`, `Unknown`, `Todo` and derivatives thereof do not participate in subtyping.
|
||||
A non-fully-static type can be considered a subtype of another type if all possible materializations
|
||||
of the first type represent sets of values that are a subset of every possible set of values
|
||||
represented by a materialization of the second type.
|
||||
|
||||
```py
|
||||
from ty_extensions import Unknown, is_subtype_of, static_assert, Intersection
|
||||
|
@ -749,25 +776,58 @@ from typing_extensions import Any
|
|||
static_assert(not is_subtype_of(Any, Any))
|
||||
static_assert(not is_subtype_of(Any, int))
|
||||
static_assert(not is_subtype_of(int, Any))
|
||||
static_assert(not is_subtype_of(Any, object))
|
||||
static_assert(is_subtype_of(Any, object))
|
||||
static_assert(not is_subtype_of(object, Any))
|
||||
|
||||
static_assert(not is_subtype_of(int, Any | int))
|
||||
static_assert(not is_subtype_of(Intersection[Any, int], int))
|
||||
static_assert(is_subtype_of(int, Any | int))
|
||||
static_assert(is_subtype_of(Intersection[Any, int], int))
|
||||
static_assert(not is_subtype_of(tuple[int, int], tuple[int, Any]))
|
||||
```
|
||||
|
||||
# The same for `Unknown`:
|
||||
The same for `Unknown`:
|
||||
|
||||
```py
|
||||
static_assert(not is_subtype_of(Unknown, Unknown))
|
||||
static_assert(not is_subtype_of(Unknown, int))
|
||||
static_assert(not is_subtype_of(int, Unknown))
|
||||
static_assert(not is_subtype_of(Unknown, object))
|
||||
static_assert(is_subtype_of(Unknown, object))
|
||||
static_assert(not is_subtype_of(object, Unknown))
|
||||
|
||||
static_assert(not is_subtype_of(int, Unknown | int))
|
||||
static_assert(not is_subtype_of(Intersection[Unknown, int], int))
|
||||
static_assert(is_subtype_of(int, Unknown | int))
|
||||
static_assert(is_subtype_of(Intersection[Unknown, int], int))
|
||||
static_assert(not is_subtype_of(tuple[int, int], tuple[int, Unknown]))
|
||||
```
|
||||
|
||||
Instances of classes that inherit `Any` are not subtypes of some other `Arbitrary` class, because
|
||||
the `Any` they inherit from could materialize to something (e.g. `object`) that is not a subclass of
|
||||
that class.
|
||||
|
||||
Similarly, they are not subtypes of `Any`, because there are possible materializations of `Any` that
|
||||
would not satisfy the subtype relation.
|
||||
|
||||
They are subtypes of `object`.
|
||||
|
||||
```py
|
||||
class InheritsAny(Any):
|
||||
pass
|
||||
|
||||
class Arbitrary:
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(InheritsAny, Arbitrary))
|
||||
static_assert(not is_subtype_of(InheritsAny, Any))
|
||||
static_assert(is_subtype_of(InheritsAny, object))
|
||||
```
|
||||
|
||||
Similar for subclass-of types:
|
||||
|
||||
```py
|
||||
static_assert(not is_subtype_of(type[Any], type[Any]))
|
||||
static_assert(not is_subtype_of(type[object], type[Any]))
|
||||
static_assert(not is_subtype_of(type[Any], type[Arbitrary]))
|
||||
static_assert(is_subtype_of(type[Any], type[object]))
|
||||
```
|
||||
|
||||
## Callable
|
||||
|
||||
The general principle is that a callable type is a subtype of another if it's more flexible in what
|
||||
|
@ -1389,10 +1449,45 @@ static_assert(is_subtype_of(TypeOf[C.foo], object))
|
|||
static_assert(not is_subtype_of(object, TypeOf[C.foo]))
|
||||
```
|
||||
|
||||
#### Gradual form
|
||||
|
||||
A callable type with `...` parameters can be considered a supertype of a callable type that accepts
|
||||
any arguments of any type, but otherwise is not a subtype or supertype of any callable type.
|
||||
|
||||
```py
|
||||
from typing import Callable, Never
|
||||
from ty_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
|
||||
def bottom(*args: object, **kwargs: object) -> Never:
|
||||
raise Exception()
|
||||
|
||||
type BottomCallable = CallableTypeOf[bottom]
|
||||
|
||||
static_assert(is_subtype_of(BottomCallable, Callable[..., Never]))
|
||||
static_assert(is_subtype_of(BottomCallable, Callable[..., int]))
|
||||
|
||||
static_assert(not is_subtype_of(Callable[[], object], Callable[..., object]))
|
||||
static_assert(not is_subtype_of(Callable[..., object], Callable[[], object]))
|
||||
```
|
||||
|
||||
According to the spec, `*args: Any, **kwargs: Any` is equivalent to `...`. This is a subtle but
|
||||
important distinction. No materialization of the former signature (if taken literally) can have any
|
||||
required arguments, but `...` can materialize to a signature with required arguments. The below test
|
||||
would not pass if we didn't handle this special case.
|
||||
|
||||
```py
|
||||
from typing import Callable, Any
|
||||
from ty_extensions import is_subtype_of, static_assert, CallableTypeOf
|
||||
|
||||
def f(*args: Any, **kwargs: Any) -> Any: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[f], Callable[[], object]))
|
||||
```
|
||||
|
||||
### Classes with `__call__`
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from typing import Callable, Any
|
||||
from ty_extensions import TypeOf, is_subtype_of, static_assert, is_assignable_to
|
||||
|
||||
class A:
|
||||
|
@ -1404,6 +1499,8 @@ a = A()
|
|||
static_assert(is_subtype_of(A, Callable[[int], int]))
|
||||
static_assert(not is_subtype_of(A, Callable[[], int]))
|
||||
static_assert(not is_subtype_of(Callable[[int], int], A))
|
||||
static_assert(not is_subtype_of(A, Callable[[Any], int]))
|
||||
static_assert(not is_subtype_of(A, Callable[[int], Any]))
|
||||
|
||||
def f(fn: Callable[[int], int]) -> None: ...
|
||||
|
||||
|
|
|
@ -326,29 +326,20 @@ from ty_extensions import (
|
|||
Unknown,
|
||||
bottom_materialization,
|
||||
top_materialization,
|
||||
is_fully_static,
|
||||
static_assert,
|
||||
is_subtype_of,
|
||||
)
|
||||
|
||||
def bounded_by_gradual[T: Any](t: T) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
|
||||
# Top materialization of `T: Any` is `T: object`
|
||||
static_assert(is_fully_static(TypeOf[top_materialization(T)]))
|
||||
|
||||
# Bottom materialization of `T: Any` is `T: Never`
|
||||
static_assert(is_fully_static(TypeOf[bottom_materialization(T)]))
|
||||
static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], Never))
|
||||
|
||||
def constrained_by_gradual[T: (int, Any)](t: T) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
|
||||
# Top materialization of `T: (int, Any)` is `T: (int, object)`
|
||||
static_assert(is_fully_static(TypeOf[top_materialization(T)]))
|
||||
|
||||
# Bottom materialization of `T: (int, Any)` is `T: (int, Never)`
|
||||
static_assert(is_fully_static(TypeOf[bottom_materialization(T)]))
|
||||
static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], int))
|
||||
```
|
||||
|
||||
|
|
|
@ -175,8 +175,8 @@ python-version = "3.12"
|
|||
```
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from ty_extensions import AlwaysTruthy, AlwaysFalsy
|
||||
from typing import Literal, Union
|
||||
from ty_extensions import AlwaysTruthy, AlwaysFalsy, is_equivalent_to, static_assert
|
||||
|
||||
type strings = Literal["foo", ""]
|
||||
type ints = Literal[0, 1]
|
||||
|
@ -213,4 +213,61 @@ def _(
|
|||
|
||||
reveal_type(bytes_or_falsy) # revealed: Literal[b"foo"] | AlwaysFalsy
|
||||
reveal_type(falsy_or_bytes) # revealed: AlwaysFalsy | Literal[b"foo"]
|
||||
|
||||
type SA = Union[Literal[""], AlwaysTruthy, Literal["foo"]]
|
||||
static_assert(is_equivalent_to(SA, Literal[""] | AlwaysTruthy))
|
||||
|
||||
type SD = Union[Literal[""], AlwaysTruthy, Literal["foo"], AlwaysFalsy, AlwaysTruthy, int]
|
||||
static_assert(is_equivalent_to(SD, AlwaysTruthy | AlwaysFalsy | int))
|
||||
|
||||
type BA = Union[Literal[b""], AlwaysTruthy, Literal[b"foo"]]
|
||||
static_assert(is_equivalent_to(BA, Literal[b""] | AlwaysTruthy))
|
||||
|
||||
type BD = Union[Literal[b""], AlwaysTruthy, Literal[b"foo"], AlwaysFalsy, AlwaysTruthy, int]
|
||||
static_assert(is_equivalent_to(BD, AlwaysTruthy | AlwaysFalsy | int))
|
||||
|
||||
type IA = Union[Literal[0], AlwaysTruthy, Literal[1]]
|
||||
static_assert(is_equivalent_to(IA, Literal[0] | AlwaysTruthy))
|
||||
|
||||
type ID = Union[Literal[0], AlwaysTruthy, Literal[1], AlwaysFalsy, AlwaysTruthy, str]
|
||||
static_assert(is_equivalent_to(ID, AlwaysTruthy | AlwaysFalsy | str))
|
||||
```
|
||||
|
||||
## Unions with intersections of literals and Any
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any, Literal
|
||||
from ty_extensions import Intersection
|
||||
|
||||
type SA = Literal[""]
|
||||
type SB = Intersection[Literal[""], Any]
|
||||
type SC = SA | SB
|
||||
type SD = SB | SA
|
||||
|
||||
def _(c: SC, d: SD):
|
||||
reveal_type(c) # revealed: Literal[""]
|
||||
reveal_type(d) # revealed: Literal[""]
|
||||
|
||||
type IA = Literal[0]
|
||||
type IB = Intersection[Literal[0], Any]
|
||||
type IC = IA | IB
|
||||
type ID = IB | IA
|
||||
|
||||
def _(c: IC, d: ID):
|
||||
reveal_type(c) # revealed: Literal[0]
|
||||
reveal_type(d) # revealed: Literal[0]
|
||||
|
||||
type BA = Literal[b""]
|
||||
type BB = Intersection[Literal[b""], Any]
|
||||
type BC = BA | BB
|
||||
type BD = BB | BA
|
||||
|
||||
def _(c: BC, d: BD):
|
||||
reveal_type(c) # revealed: Literal[b""]
|
||||
reveal_type(d) # revealed: Literal[b""]
|
||||
```
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue