[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:
Carl Meyer 2025-06-24 18:02:05 -07:00 committed by GitHub
parent eee5a5a3d6
commit 62975b3ab2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 957 additions and 1633 deletions

View file

@ -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)
```

View file

@ -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"))
```

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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
```

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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
```

View file

@ -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

View file

@ -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: ...

View file

@ -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))
```

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]))
```

View file

@ -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

View file

@ -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: ...

View file

@ -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))
```

View file

@ -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""]
```