mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +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""]
|
||||
```
|
||||
|
|
|
@ -558,7 +558,6 @@ pub enum Type<'db> {
|
|||
BoundSuper(BoundSuperType<'db>),
|
||||
/// A subtype of `bool` that allows narrowing in both positive and negative cases.
|
||||
TypeIs(TypeIsType<'db>),
|
||||
// TODO protocols, overloads, generics
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
|
@ -1165,11 +1164,88 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return `true` if subtyping is always reflexive for this type; `T <: T` is always true for
|
||||
/// any `T` of this type.
|
||||
///
|
||||
/// This is true for fully static types, but also for some types that may not be fully static.
|
||||
/// For example, a `ClassLiteral` may inherit `Any`, but its subtyping is still reflexive.
|
||||
///
|
||||
/// This method may have false negatives, but it should not have false positives. It should be
|
||||
/// a cheap shallow check, not an exhaustive recursive check.
|
||||
fn subtyping_is_always_reflexive(self) -> bool {
|
||||
match self {
|
||||
Type::Never
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::DataclassDecorator(_)
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::ModuleLiteral(..)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::SpecialForm(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::PropertyInstance(_)
|
||||
// might inherit `Any`, but subtyping is still reflexive
|
||||
| Type::ClassLiteral(_) => true,
|
||||
Type::Dynamic(_)
|
||||
| Type::NominalInstance(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::GenericAlias(_)
|
||||
| Type::SubclassOf(_)
|
||||
| Type::Union(_)
|
||||
| Type::Intersection(_)
|
||||
| Type::Callable(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeIs(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is a [subtype of] type `target`.
|
||||
///
|
||||
/// This method returns `false` if either `self` or `other` is not fully static.
|
||||
/// For fully static types, this means that the set of objects represented by `self` is a
|
||||
/// subset of the objects represented by `target`.
|
||||
///
|
||||
/// For gradual types, it means that the union of all possible sets of values represented by
|
||||
/// `self` (the "top materialization" of `self`) is a subtype of the intersection of all
|
||||
/// possible sets of values represented by `target` (the "bottom materialization" of
|
||||
/// `target`). In other words, for all possible pairs of materializations `self'` and
|
||||
/// `target'`, `self'` is always a subtype of `target'`.
|
||||
///
|
||||
/// Note that this latter expansion of the subtyping relation to non-fully-static types is not
|
||||
/// described in the typing spec, but the primary use of the subtyping relation is for
|
||||
/// simplifying unions and intersections, and this expansion to gradual types is sound and
|
||||
/// allows us to better simplify many unions and intersections. This definition does mean the
|
||||
/// subtyping relation is not reflexive for non-fully-static types (e.g. `Any` is not a subtype
|
||||
/// of `Any`).
|
||||
///
|
||||
/// [subtype of]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
|
||||
///
|
||||
/// There would be an even more general definition of subtyping for gradual types, allowing a
|
||||
/// type `S` to be a subtype of a type `T` if the top materialization of `S` (`S+`) is a
|
||||
/// subtype of `T+`, and the bottom materialization of `S` (`S-`) is a subtype of `T-`. This
|
||||
/// definition is attractive in that it would restore reflexivity of subtyping for all types,
|
||||
/// and would mean that gradual equivalence of `S` and `T` could be defined simply as `S <: T
|
||||
/// && T <: S`. It would also be sound, in that simplifying unions or intersections according
|
||||
/// to this definition of subtyping would still result in an equivalent type.
|
||||
///
|
||||
/// Unfortunately using this definition would break transitivity of subtyping when both nominal
|
||||
/// and structural types are involved, because Liskov enforcement for nominal types is based on
|
||||
/// assignability, so we can have class `A` with method `def meth(self) -> Any` and a subclass
|
||||
/// `B(A)` with method `def meth(self) -> int`. In this case, `A` would be a subtype of a
|
||||
/// protocol `P` with method `def meth(self) -> Any`, but `B` would not be a subtype of `P`,
|
||||
/// and yet `B` is (by nominal subtyping) a subtype of `A`, so we would have `B <: A` and `A <:
|
||||
/// P`, but not `B <: P`. Losing transitivity of subtyping is not tenable (it makes union and
|
||||
/// intersection simplification dependent on the order in which elements are added), so we do
|
||||
/// not use this more general definition of subtyping.
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
self.has_relation_to(db, target, TypeRelation::Subtyping)
|
||||
}
|
||||
|
@ -1182,23 +1258,26 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
|
||||
fn has_relation_to(self, db: &'db dyn Db, target: Type<'db>, relation: TypeRelation) -> bool {
|
||||
if !relation.applies_to(db, self, target) {
|
||||
return false;
|
||||
}
|
||||
if relation.are_equivalent(db, self, target) {
|
||||
// Subtyping implies assignability, so if subtyping is reflexive and the two types are
|
||||
// equivalent, it is both a subtype and assignable. Assignability is always reflexive.
|
||||
if (relation.is_assignability() || self.subtyping_is_always_reflexive())
|
||||
&& self.is_equivalent_to(db, target)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
match (self, target) {
|
||||
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => true,
|
||||
|
||||
// `Never` is the bottom type, the empty set.
|
||||
// It is a subtype of all other fully static types.
|
||||
(Type::Never, _) => true,
|
||||
|
||||
// Everything is a subtype of `object`.
|
||||
(_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true,
|
||||
|
||||
// `Never` is the bottom type, the empty set.
|
||||
// It is a subtype of all other types.
|
||||
(Type::Never, _) => true,
|
||||
|
||||
// Dynamic is only a subtype of `object` and only a supertype of `Never`; both were
|
||||
// handled above. It's always assignable, though.
|
||||
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => relation.is_assignability(),
|
||||
|
||||
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
|
||||
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
|
||||
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
|
||||
|
@ -1219,6 +1298,14 @@ impl<'db> Type<'db> {
|
|||
false
|
||||
}
|
||||
|
||||
// Two identical typevars must always solve to the same type, so they are always
|
||||
// subtypes of each other and assignable to each other.
|
||||
(Type::TypeVar(lhs_typevar), Type::TypeVar(rhs_typevar))
|
||||
if lhs_typevar == rhs_typevar =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// A fully static typevar is a subtype of its upper bound, and to something similar to
|
||||
// the union of its constraints. An unbound, unconstrained, fully static typevar has an
|
||||
// implicit upper bound of `object` (which is handled above).
|
||||
|
@ -1250,7 +1337,7 @@ impl<'db> Type<'db> {
|
|||
|
||||
// `Never` is the bottom type, the empty set.
|
||||
// Other than one unlikely edge case (TypeVars bound to `Never`),
|
||||
// no other fully static type is a subtype of `Never`.
|
||||
// no other type is a subtype of or assignable to `Never`.
|
||||
(_, Type::Never) => false,
|
||||
|
||||
(Type::Union(union), _) => union
|
||||
|
@ -1295,7 +1382,7 @@ impl<'db> Type<'db> {
|
|||
(left, Type::AlwaysTruthy) => left.bool(db).is_always_true(),
|
||||
// Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance).
|
||||
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => {
|
||||
relation.are_equivalent(db, target, Type::object(db))
|
||||
target.is_equivalent_to(db, Type::object(db))
|
||||
}
|
||||
|
||||
// These clauses handle type variants that include function literals. A function
|
||||
|
@ -1410,6 +1497,15 @@ impl<'db> Type<'db> {
|
|||
false
|
||||
}
|
||||
|
||||
// `TypeIs` is invariant.
|
||||
(Type::TypeIs(left), Type::TypeIs(right)) => {
|
||||
left.return_type(db)
|
||||
.has_relation_to(db, right.return_type(db), relation)
|
||||
&& right
|
||||
.return_type(db)
|
||||
.has_relation_to(db, left.return_type(db), relation)
|
||||
}
|
||||
|
||||
// `TypeIs[T]` is a subtype of `bool`.
|
||||
(Type::TypeIs(_), _) => KnownClass::Bool
|
||||
.to_instance(db)
|
||||
|
@ -1425,11 +1521,7 @@ impl<'db> Type<'db> {
|
|||
true
|
||||
}
|
||||
|
||||
(Type::Callable(_), _) => {
|
||||
// TODO: Implement subtyping between callable types and other types like
|
||||
// function literals, bound methods, class literals, `type[]`, etc.)
|
||||
false
|
||||
}
|
||||
(Type::Callable(_), _) => false,
|
||||
|
||||
(Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => {
|
||||
self_tuple.has_relation_to(db, target_tuple, relation)
|
||||
|
@ -1449,7 +1541,7 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
(Type::Tuple(_), _) => false,
|
||||
|
||||
(Type::BoundSuper(_), Type::BoundSuper(_)) => relation.are_equivalent(db, self, target),
|
||||
(Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target),
|
||||
(Type::BoundSuper(_), _) => KnownClass::Super
|
||||
.to_instance(db)
|
||||
.has_relation_to(db, target, relation),
|
||||
|
@ -1459,15 +1551,17 @@ impl<'db> Type<'db> {
|
|||
(Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
|
||||
.subclass_of()
|
||||
.into_class()
|
||||
.is_none_or(|subclass_of_class| {
|
||||
.map(|subclass_of_class| {
|
||||
ClassType::NonGeneric(class).has_relation_to(db, subclass_of_class, relation)
|
||||
}),
|
||||
})
|
||||
.unwrap_or(relation.is_assignability()),
|
||||
(Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
|
||||
.subclass_of()
|
||||
.into_class()
|
||||
.is_none_or(|subclass_of_class| {
|
||||
.map(|subclass_of_class| {
|
||||
ClassType::Generic(alias).has_relation_to(db, subclass_of_class, relation)
|
||||
}),
|
||||
})
|
||||
.unwrap_or(relation.is_assignability()),
|
||||
|
||||
// This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`?
|
||||
(Type::SubclassOf(self_subclass_ty), Type::SubclassOf(target_subclass_ty)) => {
|
||||
|
@ -1494,25 +1588,20 @@ impl<'db> Type<'db> {
|
|||
.metaclass_instance_type(db)
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// This branch upholds two properties:
|
||||
// - For any type `T` that is assignable to `type`, `T` shall be assignable to `type[Any]`.
|
||||
// - For any type `T` that is assignable to `type`, `type[Any]` shall be assignable to `T`.
|
||||
//
|
||||
// This is really the same as the very first branch in this `match` statement that handles dynamic types.
|
||||
// That branch upholds two properties:
|
||||
// - For any type `S` that is assignable to `object` (which is _all_ types), `S` shall be assignable to `Any`
|
||||
// - For any type `S` that is assignable to `object` (which is _all_ types), `Any` shall be assignable to `S`.
|
||||
//
|
||||
// The only difference between this branch and the first branch is that the first branch deals with the type
|
||||
// `object & Any` (which simplifies to `Any`!) whereas this branch deals with the type `type & Any`.
|
||||
//
|
||||
// See also: <https://github.com/astral-sh/ty/issues/222>
|
||||
(Type::SubclassOf(subclass_of_ty), other)
|
||||
| (other, Type::SubclassOf(subclass_of_ty))
|
||||
if subclass_of_ty.is_dynamic()
|
||||
&& other.has_relation_to(db, KnownClass::Type.to_instance(db), relation) =>
|
||||
// `type[Any]` is a subtype of `type[object]`, and is assignable to any `type[...]`
|
||||
(Type::SubclassOf(subclass_of_ty), other) if subclass_of_ty.is_dynamic() => {
|
||||
KnownClass::Type
|
||||
.to_instance(db)
|
||||
.has_relation_to(db, other, relation)
|
||||
|| (relation.is_assignability()
|
||||
&& other.has_relation_to(db, KnownClass::Type.to_instance(db), relation))
|
||||
}
|
||||
|
||||
// Any `type[...]` type is assignable to `type[Any]`
|
||||
(other, Type::SubclassOf(subclass_of_ty))
|
||||
if subclass_of_ty.is_dynamic() && relation.is_assignability() =>
|
||||
{
|
||||
true
|
||||
other.has_relation_to(db, KnownClass::Type.to_instance(db), relation)
|
||||
}
|
||||
|
||||
// `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses
|
||||
|
@ -1561,43 +1650,7 @@ impl<'db> Type<'db> {
|
|||
|
||||
/// Return true if this type is [equivalent to] type `other`.
|
||||
///
|
||||
/// This method returns `false` if either `self` or `other` is not fully static.
|
||||
///
|
||||
/// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
|
||||
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||
// TODO equivalent but not identical types: TypedDicts, Protocols, type aliases, etc.
|
||||
|
||||
match (self, other) {
|
||||
(Type::Union(left), Type::Union(right)) => left.is_equivalent_to(db, right),
|
||||
(Type::Intersection(left), Type::Intersection(right)) => {
|
||||
left.is_equivalent_to(db, right)
|
||||
}
|
||||
(Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right),
|
||||
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
|
||||
self_function.is_equivalent_to(db, target_function)
|
||||
}
|
||||
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
|
||||
self_method.is_equivalent_to(db, target_method)
|
||||
}
|
||||
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
|
||||
self_method.is_equivalent_to(db, target_method)
|
||||
}
|
||||
(Type::Callable(left), Type::Callable(right)) => left.is_equivalent_to(db, right),
|
||||
(Type::NominalInstance(left), Type::NominalInstance(right)) => {
|
||||
left.is_equivalent_to(db, right)
|
||||
}
|
||||
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
|
||||
left.is_equivalent_to(db, right)
|
||||
}
|
||||
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
|
||||
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
|
||||
n.class.is_object(db) && protocol.normalized(db) == nominal
|
||||
}
|
||||
_ => self == other && self.is_fully_static(db) && other.is_fully_static(db),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this type and `other` are gradual equivalent.
|
||||
/// Two equivalent types represent the same sets of values.
|
||||
///
|
||||
/// > Two gradual types `A` and `B` are equivalent
|
||||
/// > (that is, the same gradual type, not merely consistent with one another)
|
||||
|
@ -1606,10 +1659,8 @@ impl<'db> Type<'db> {
|
|||
/// >
|
||||
/// > — [Summary of type relations]
|
||||
///
|
||||
/// This powers the `assert_type()` directive.
|
||||
///
|
||||
/// [Summary of type relations]: https://typing.python.org/en/latest/spec/concepts.html#summary-of-type-relations
|
||||
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||
/// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
|
||||
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
|
@ -1626,32 +1677,30 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
|
||||
(Type::NominalInstance(first), Type::NominalInstance(second)) => {
|
||||
first.is_gradual_equivalent_to(db, second)
|
||||
first.is_equivalent_to(db, second)
|
||||
}
|
||||
|
||||
(Type::Tuple(first), Type::Tuple(second)) => first.is_gradual_equivalent_to(db, second),
|
||||
(Type::Tuple(first), Type::Tuple(second)) => first.is_equivalent_to(db, second),
|
||||
|
||||
(Type::Union(first), Type::Union(second)) => first.is_gradual_equivalent_to(db, second),
|
||||
(Type::Union(first), Type::Union(second)) => first.is_equivalent_to(db, second),
|
||||
|
||||
(Type::Intersection(first), Type::Intersection(second)) => {
|
||||
first.is_gradual_equivalent_to(db, second)
|
||||
first.is_equivalent_to(db, second)
|
||||
}
|
||||
|
||||
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
|
||||
self_function.is_gradual_equivalent_to(db, target_function)
|
||||
self_function.is_equivalent_to(db, target_function)
|
||||
}
|
||||
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
|
||||
self_method.is_gradual_equivalent_to(db, target_method)
|
||||
self_method.is_equivalent_to(db, target_method)
|
||||
}
|
||||
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
|
||||
self_method.is_gradual_equivalent_to(db, target_method)
|
||||
}
|
||||
(Type::Callable(first), Type::Callable(second)) => {
|
||||
first.is_gradual_equivalent_to(db, second)
|
||||
self_method.is_equivalent_to(db, target_method)
|
||||
}
|
||||
(Type::Callable(first), Type::Callable(second)) => first.is_equivalent_to(db, second),
|
||||
|
||||
(Type::ProtocolInstance(first), Type::ProtocolInstance(second)) => {
|
||||
first.is_gradual_equivalent_to(db, second)
|
||||
first.is_equivalent_to(db, second)
|
||||
}
|
||||
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
|
||||
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
|
||||
|
@ -2125,75 +2174,6 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns true if the type does not contain any gradual forms (as a sub-part).
|
||||
pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Type::Dynamic(_) => false,
|
||||
Type::Never
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::DataclassDecorator(_)
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::ModuleLiteral(..)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::SpecialForm(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::PropertyInstance(_) => true,
|
||||
|
||||
Type::ProtocolInstance(protocol) => protocol.is_fully_static(db),
|
||||
|
||||
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
|
||||
None => true,
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.is_fully_static(db),
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|constraint| constraint.is_fully_static(db)),
|
||||
},
|
||||
|
||||
Type::SubclassOf(subclass_of_ty) => subclass_of_ty.is_fully_static(),
|
||||
Type::BoundSuper(bound_super) => {
|
||||
!matches!(bound_super.pivot_class(db), ClassBase::Dynamic(_))
|
||||
&& !matches!(bound_super.owner(db), SuperOwnerKind::Dynamic(_))
|
||||
}
|
||||
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::NominalInstance(_) => {
|
||||
// TODO: Ideally, we would iterate over the MRO of the class, check if all
|
||||
// bases are fully static, and only return `true` if that is the case.
|
||||
//
|
||||
// This does not work yet, because we currently infer `Unknown` for some
|
||||
// generic base classes that we don't understand yet. For example, `str`
|
||||
// is defined as `class str(Sequence[str])` in typeshed and we currently
|
||||
// compute its MRO as `(str, Unknown, object)`. This would make us think
|
||||
// that `str` is a gradual type, which causes all sorts of downstream
|
||||
// issues because it does not participate in equivalence/subtyping etc.
|
||||
//
|
||||
// Another problem is that we run into problems if we eagerly query the
|
||||
// MRO of class literals here. I have not fully investigated this, but
|
||||
// iterating over the MRO alone, without even acting on it, causes us to
|
||||
// infer `Unknown` for many classes.
|
||||
|
||||
true
|
||||
}
|
||||
Type::Union(union) => union.is_fully_static(db),
|
||||
Type::Intersection(intersection) => intersection.is_fully_static(db),
|
||||
// TODO: Once we support them, make sure that we return `false` for other types
|
||||
// containing gradual forms such as `tuple[Any, ...]`.
|
||||
// Conversely, make sure to return `true` for homogeneous tuples such as
|
||||
// `tuple[int, ...]`, once we add support for them.
|
||||
Type::Tuple(tuple) => tuple.is_fully_static(db),
|
||||
Type::Callable(callable) => callable.is_fully_static(db),
|
||||
Type::TypeIs(type_is) => type_is.return_type(db).is_fully_static(db),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if there is just a single inhabitant for this type.
|
||||
///
|
||||
/// Note: This function aims to have no false positives, but might return `false`
|
||||
|
@ -3744,8 +3724,7 @@ impl<'db> Type<'db> {
|
|||
KnownFunction::IsEquivalentTo
|
||||
| KnownFunction::IsSubtypeOf
|
||||
| KnownFunction::IsAssignableTo
|
||||
| KnownFunction::IsDisjointFrom
|
||||
| KnownFunction::IsGradualEquivalentTo,
|
||||
| KnownFunction::IsDisjointFrom,
|
||||
) => Binding::single(
|
||||
self,
|
||||
Signature::new(
|
||||
|
@ -3762,20 +3741,20 @@ impl<'db> Type<'db> {
|
|||
)
|
||||
.into(),
|
||||
|
||||
Some(
|
||||
KnownFunction::IsFullyStatic
|
||||
| KnownFunction::IsSingleton
|
||||
| KnownFunction::IsSingleValued,
|
||||
) => Binding::single(
|
||||
self,
|
||||
Signature::new(
|
||||
Parameters::new([Parameter::positional_only(Some(Name::new_static("a")))
|
||||
Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => {
|
||||
Binding::single(
|
||||
self,
|
||||
Signature::new(
|
||||
Parameters::new([Parameter::positional_only(Some(Name::new_static(
|
||||
"a",
|
||||
)))
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any())]),
|
||||
Some(KnownClass::Bool.to_instance(db)),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
Some(KnownClass::Bool.to_instance(db)),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
Some(KnownFunction::TopMaterialization | KnownFunction::BottomMaterialization) => {
|
||||
Binding::single(
|
||||
|
@ -6981,34 +6960,7 @@ pub(crate) enum TypeRelation {
|
|||
}
|
||||
|
||||
impl TypeRelation {
|
||||
/// Non-fully-static types do not participate in subtyping, only assignability,
|
||||
/// so the subtyping relation does not even apply to them.
|
||||
///
|
||||
/// Type `A` can only be a subtype of type `B` if the set of possible runtime objects
|
||||
/// that `A` represents is a subset of the set of possible runtime objects that `B` represents.
|
||||
/// But the set of objects described by a non-fully-static type is (either partially or wholly) unknown,
|
||||
/// so the question is simply unanswerable for non-fully-static types.
|
||||
///
|
||||
/// However, the assignability relation applies to all types, even non-fully-static ones.
|
||||
fn applies_to<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool {
|
||||
match self {
|
||||
TypeRelation::Subtyping => type_1.is_fully_static(db) && type_2.is_fully_static(db),
|
||||
TypeRelation::Assignability => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine whether `type_1` and `type_2` are equivalent.
|
||||
///
|
||||
/// Depending on whether the context is a subtyping test or an assignability test,
|
||||
/// this method may call [`Type::is_equivalent_to`] or [`Type::is_assignable_to`].
|
||||
fn are_equivalent<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool {
|
||||
match self {
|
||||
TypeRelation::Subtyping => type_1.is_equivalent_to(db, type_2),
|
||||
TypeRelation::Assignability => type_1.is_gradual_equivalent_to(db, type_2),
|
||||
}
|
||||
}
|
||||
|
||||
const fn applies_to_non_fully_static_types(self) -> bool {
|
||||
pub(crate) const fn is_assignability(self) -> bool {
|
||||
matches!(self, TypeRelation::Assignability)
|
||||
}
|
||||
}
|
||||
|
@ -7147,14 +7099,6 @@ impl<'db> BoundMethodType<'db> {
|
|||
.self_instance(db)
|
||||
.is_equivalent_to(db, self.self_instance(db))
|
||||
}
|
||||
|
||||
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.function(db)
|
||||
.is_gradual_equivalent_to(db, other.function(db))
|
||||
&& other
|
||||
.self_instance(db)
|
||||
.is_gradual_equivalent_to(db, self.self_instance(db))
|
||||
}
|
||||
}
|
||||
|
||||
/// This type represents the set of all callable objects with a certain, possibly overloaded,
|
||||
|
@ -7257,13 +7201,6 @@ impl<'db> CallableType<'db> {
|
|||
self.signatures(db).find_legacy_typevars(db, typevars);
|
||||
}
|
||||
|
||||
/// Check whether this callable type is fully static.
|
||||
///
|
||||
/// See [`Type::is_fully_static`] for more details.
|
||||
fn is_fully_static(self, db: &'db dyn Db) -> bool {
|
||||
self.signatures(db).is_fully_static(db)
|
||||
}
|
||||
|
||||
/// Check whether this callable type has the given relation to another callable type.
|
||||
///
|
||||
/// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details.
|
||||
|
@ -7285,16 +7222,6 @@ impl<'db> CallableType<'db> {
|
|||
.is_equivalent_to(db, other.signatures(db))
|
||||
}
|
||||
|
||||
/// Check whether this callable type is gradual equivalent to another callable type.
|
||||
///
|
||||
/// See [`Type::is_gradual_equivalent_to`] for more details.
|
||||
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.is_function_like(db) == other.is_function_like(db)
|
||||
&& self
|
||||
.signatures(db)
|
||||
.is_gradual_equivalent_to(db, other.signatures(db))
|
||||
}
|
||||
|
||||
/// See [`Type::replace_self_reference`].
|
||||
fn replace_self_reference(self, db: &'db dyn Db, class: ClassLiteral<'db>) -> Self {
|
||||
CallableType::new(
|
||||
|
@ -7391,39 +7318,6 @@ impl<'db> MethodWrapperKind<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
match (self, other) {
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderGet(self_function),
|
||||
MethodWrapperKind::FunctionTypeDunderGet(other_function),
|
||||
) => self_function.is_gradual_equivalent_to(db, other_function),
|
||||
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderCall(self_function),
|
||||
MethodWrapperKind::FunctionTypeDunderCall(other_function),
|
||||
) => self_function.is_gradual_equivalent_to(db, other_function),
|
||||
|
||||
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
|
||||
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))
|
||||
| (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => {
|
||||
self == other
|
||||
}
|
||||
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderGet(_)
|
||||
| MethodWrapperKind::FunctionTypeDunderCall(_)
|
||||
| MethodWrapperKind::PropertyDunderGet(_)
|
||||
| MethodWrapperKind::PropertyDunderSet(_)
|
||||
| MethodWrapperKind::StrStartswith(_),
|
||||
MethodWrapperKind::FunctionTypeDunderGet(_)
|
||||
| MethodWrapperKind::FunctionTypeDunderCall(_)
|
||||
| MethodWrapperKind::PropertyDunderGet(_)
|
||||
| MethodWrapperKind::PropertyDunderSet(_)
|
||||
| MethodWrapperKind::StrStartswith(_),
|
||||
) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized(self, db: &'db dyn Db) -> Self {
|
||||
match self {
|
||||
MethodWrapperKind::FunctionTypeDunderGet(function) => {
|
||||
|
@ -7781,10 +7675,6 @@ impl<'db> UnionType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
|
||||
self.elements(db).iter().all(|ty| ty.is_fully_static(db))
|
||||
}
|
||||
|
||||
/// Create a new union type with the elements normalized.
|
||||
///
|
||||
/// See [`Type::normalized`] for more details.
|
||||
|
@ -7799,15 +7689,8 @@ impl<'db> UnionType<'db> {
|
|||
UnionType::new(db, new_elements.into_boxed_slice())
|
||||
}
|
||||
|
||||
/// Return `true` if `self` represents the exact same set of possible runtime objects as `other`
|
||||
/// Return `true` if `self` represents the exact same sets of possible runtime objects as `other`
|
||||
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
/// Inlined version of [`UnionType::is_fully_static`] to avoid having to lookup
|
||||
/// `self.elements` multiple times in the Salsa db in this single method.
|
||||
#[inline]
|
||||
fn all_fully_static(db: &dyn Db, elements: &[Type]) -> bool {
|
||||
elements.iter().all(|ty| ty.is_fully_static(db))
|
||||
}
|
||||
|
||||
let self_elements = self.elements(db);
|
||||
let other_elements = other.elements(db);
|
||||
|
||||
|
@ -7815,14 +7698,6 @@ impl<'db> UnionType<'db> {
|
|||
return false;
|
||||
}
|
||||
|
||||
if !all_fully_static(db, self_elements) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !all_fully_static(db, other_elements) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
|
@ -7835,39 +7710,6 @@ impl<'db> UnionType<'db> {
|
|||
|
||||
sorted_self == other.normalized(db)
|
||||
}
|
||||
|
||||
/// Return `true` if `self` has exactly the same set of possible static materializations as `other`
|
||||
/// (if `self` represents the same set of possible sets of possible runtime objects as `other`)
|
||||
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: `T | Unknown` should be gradually equivalent to `T | Unknown | Any`,
|
||||
// since they have exactly the same set of possible static materializations
|
||||
// (they represent the same set of possible sets of possible runtime objects)
|
||||
if self.elements(db).len() != other.elements(db).len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let sorted_self = self.normalized(db);
|
||||
|
||||
if sorted_self == other {
|
||||
return true;
|
||||
}
|
||||
|
||||
let sorted_other = other.normalized(db);
|
||||
|
||||
if sorted_self == sorted_other {
|
||||
return true;
|
||||
}
|
||||
|
||||
sorted_self
|
||||
.elements(db)
|
||||
.iter()
|
||||
.zip(sorted_other.elements(db))
|
||||
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::interned(debug)]
|
||||
|
@ -7910,52 +7752,24 @@ impl<'db> IntersectionType<'db> {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
|
||||
self.positive(db).iter().all(|ty| ty.is_fully_static(db))
|
||||
&& self.negative(db).iter().all(|ty| ty.is_fully_static(db))
|
||||
}
|
||||
|
||||
/// Return `true` if `self` represents exactly the same set of possible runtime objects as `other`
|
||||
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
/// Inlined version of [`IntersectionType::is_fully_static`] to avoid having to lookup
|
||||
/// `positive` and `negative` multiple times in the Salsa db in this single method.
|
||||
#[inline]
|
||||
fn all_fully_static(db: &dyn Db, elements: &FxOrderSet<Type>) -> bool {
|
||||
elements.iter().all(|ty| ty.is_fully_static(db))
|
||||
}
|
||||
|
||||
let self_positive = self.positive(db);
|
||||
|
||||
if !all_fully_static(db, self_positive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let other_positive = other.positive(db);
|
||||
|
||||
if self_positive.len() != other_positive.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !all_fully_static(db, other_positive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let self_negative = self.negative(db);
|
||||
|
||||
if !all_fully_static(db, self_negative) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let other_negative = other.negative(db);
|
||||
|
||||
if self_negative.len() != other_negative.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !all_fully_static(db, other_negative) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
|
@ -7969,43 +7783,6 @@ impl<'db> IntersectionType<'db> {
|
|||
sorted_self == other.normalized(db)
|
||||
}
|
||||
|
||||
/// Return `true` if `self` has exactly the same set of possible static materializations as `other`
|
||||
/// (if `self` represents the same set of possible sets of possible runtime objects as `other`)
|
||||
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.positive(db).len() != other.positive(db).len()
|
||||
|| self.negative(db).len() != other.negative(db).len()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let sorted_self = self.normalized(db);
|
||||
|
||||
if sorted_self == other {
|
||||
return true;
|
||||
}
|
||||
|
||||
let sorted_other = other.normalized(db);
|
||||
|
||||
if sorted_self == sorted_other {
|
||||
return true;
|
||||
}
|
||||
|
||||
sorted_self
|
||||
.positive(db)
|
||||
.iter()
|
||||
.zip(sorted_other.positive(db))
|
||||
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
|
||||
&& sorted_self
|
||||
.negative(db)
|
||||
.iter()
|
||||
.zip(sorted_other.negative(db))
|
||||
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
|
||||
}
|
||||
|
||||
pub(crate) fn map_with_boundness(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
|
|
@ -78,6 +78,7 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum UnionElement<'db> {
|
||||
IntLiterals(FxOrderSet<i64>),
|
||||
StringLiterals(FxOrderSet<StringLiteralType<'db>>),
|
||||
|
@ -87,27 +88,26 @@ enum UnionElement<'db> {
|
|||
|
||||
impl<'db> UnionElement<'db> {
|
||||
/// Try reducing this `UnionElement` given the presence in the same union of `other_type`.
|
||||
///
|
||||
/// If this `UnionElement` is a group of literals, filter the literals present if needed and
|
||||
/// return `ReduceResult::KeepIf` with a boolean value indicating whether the remaining group
|
||||
/// of literals should be kept in the union
|
||||
///
|
||||
/// If this `UnionElement` is some other type, return `ReduceResult::Type` so `UnionBuilder`
|
||||
/// can perform more complex checks on it.
|
||||
fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> {
|
||||
match self {
|
||||
UnionElement::IntLiterals(literals) => {
|
||||
if other_type.splits_literals(db, LiteralKind::Int) {
|
||||
let mut collapse = false;
|
||||
let mut ignore = false;
|
||||
let negated = other_type.negate(db);
|
||||
literals.retain(|literal| {
|
||||
let ty = Type::IntLiteral(*literal);
|
||||
if negated.is_subtype_of(db, ty) {
|
||||
collapse = true;
|
||||
}
|
||||
if other_type.is_subtype_of(db, ty) {
|
||||
ignore = true;
|
||||
}
|
||||
!ty.is_subtype_of(db, other_type)
|
||||
});
|
||||
if collapse {
|
||||
if ignore {
|
||||
ReduceResult::Ignore
|
||||
} else if collapse {
|
||||
ReduceResult::CollapseToObject
|
||||
} else {
|
||||
ReduceResult::KeepIf(!literals.is_empty())
|
||||
|
@ -121,15 +121,21 @@ impl<'db> UnionElement<'db> {
|
|||
UnionElement::StringLiterals(literals) => {
|
||||
if other_type.splits_literals(db, LiteralKind::String) {
|
||||
let mut collapse = false;
|
||||
let mut ignore = false;
|
||||
let negated = other_type.negate(db);
|
||||
literals.retain(|literal| {
|
||||
let ty = Type::StringLiteral(*literal);
|
||||
if negated.is_subtype_of(db, ty) {
|
||||
collapse = true;
|
||||
}
|
||||
if other_type.is_subtype_of(db, ty) {
|
||||
ignore = true;
|
||||
}
|
||||
!ty.is_subtype_of(db, other_type)
|
||||
});
|
||||
if collapse {
|
||||
if ignore {
|
||||
ReduceResult::Ignore
|
||||
} else if collapse {
|
||||
ReduceResult::CollapseToObject
|
||||
} else {
|
||||
ReduceResult::KeepIf(!literals.is_empty())
|
||||
|
@ -143,15 +149,21 @@ impl<'db> UnionElement<'db> {
|
|||
UnionElement::BytesLiterals(literals) => {
|
||||
if other_type.splits_literals(db, LiteralKind::Bytes) {
|
||||
let mut collapse = false;
|
||||
let mut ignore = false;
|
||||
let negated = other_type.negate(db);
|
||||
literals.retain(|literal| {
|
||||
let ty = Type::BytesLiteral(*literal);
|
||||
if negated.is_subtype_of(db, ty) {
|
||||
collapse = true;
|
||||
}
|
||||
if other_type.is_subtype_of(db, ty) {
|
||||
ignore = true;
|
||||
}
|
||||
!ty.is_subtype_of(db, other_type)
|
||||
});
|
||||
if collapse {
|
||||
if ignore {
|
||||
ReduceResult::Ignore
|
||||
} else if collapse {
|
||||
ReduceResult::CollapseToObject
|
||||
} else {
|
||||
ReduceResult::KeepIf(!literals.is_empty())
|
||||
|
@ -173,6 +185,8 @@ enum ReduceResult<'db> {
|
|||
KeepIf(bool),
|
||||
/// Collapse this entire union to `object`.
|
||||
CollapseToObject,
|
||||
/// The new element is a subtype of an existing part of the `UnionElement`, ignore it.
|
||||
Ignore,
|
||||
/// The given `Type` can stand-in for the entire `UnionElement` for further union
|
||||
/// simplification checks.
|
||||
Type(Type<'db>),
|
||||
|
@ -229,9 +243,10 @@ impl<'db> UnionBuilder<'db> {
|
|||
// means we shouldn't add it. Otherwise, add a new `UnionElement::StringLiterals`
|
||||
// containing it.
|
||||
Type::StringLiteral(literal) => {
|
||||
let mut found = false;
|
||||
let mut found = None;
|
||||
let mut to_remove = None;
|
||||
let ty_negated = ty.negate(self.db);
|
||||
for element in &mut self.elements {
|
||||
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||
match element {
|
||||
UnionElement::StringLiterals(literals) => {
|
||||
if literals.len() >= MAX_UNION_LITERALS {
|
||||
|
@ -239,14 +254,16 @@ impl<'db> UnionBuilder<'db> {
|
|||
self.add_in_place(replace_with);
|
||||
return;
|
||||
}
|
||||
literals.insert(literal);
|
||||
found = true;
|
||||
break;
|
||||
found = Some(literals);
|
||||
continue;
|
||||
}
|
||||
UnionElement::Type(existing) => {
|
||||
if ty.is_subtype_of(self.db, *existing) {
|
||||
return;
|
||||
}
|
||||
if existing.is_subtype_of(self.db, ty) {
|
||||
to_remove = Some(index);
|
||||
}
|
||||
if ty_negated.is_subtype_of(self.db, *existing) {
|
||||
// The type that includes both this new element, and its negation
|
||||
// (or a supertype of its negation), must be simply `object`.
|
||||
|
@ -257,18 +274,24 @@ impl<'db> UnionBuilder<'db> {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if let Some(found) = found {
|
||||
found.insert(literal);
|
||||
} else {
|
||||
self.elements
|
||||
.push(UnionElement::StringLiterals(FxOrderSet::from_iter([
|
||||
literal,
|
||||
])));
|
||||
}
|
||||
if let Some(index) = to_remove {
|
||||
self.elements.swap_remove(index);
|
||||
}
|
||||
}
|
||||
// Same for bytes literals as for string literals, above.
|
||||
Type::BytesLiteral(literal) => {
|
||||
let mut found = false;
|
||||
let mut found = None;
|
||||
let mut to_remove = None;
|
||||
let ty_negated = ty.negate(self.db);
|
||||
for element in &mut self.elements {
|
||||
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||
match element {
|
||||
UnionElement::BytesLiterals(literals) => {
|
||||
if literals.len() >= MAX_UNION_LITERALS {
|
||||
|
@ -276,14 +299,16 @@ impl<'db> UnionBuilder<'db> {
|
|||
self.add_in_place(replace_with);
|
||||
return;
|
||||
}
|
||||
literals.insert(literal);
|
||||
found = true;
|
||||
break;
|
||||
found = Some(literals);
|
||||
continue;
|
||||
}
|
||||
UnionElement::Type(existing) => {
|
||||
if ty.is_subtype_of(self.db, *existing) {
|
||||
return;
|
||||
}
|
||||
if existing.is_subtype_of(self.db, ty) {
|
||||
to_remove = Some(index);
|
||||
}
|
||||
if ty_negated.is_subtype_of(self.db, *existing) {
|
||||
// The type that includes both this new element, and its negation
|
||||
// (or a supertype of its negation), must be simply `object`.
|
||||
|
@ -294,18 +319,24 @@ impl<'db> UnionBuilder<'db> {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if let Some(found) = found {
|
||||
found.insert(literal);
|
||||
} else {
|
||||
self.elements
|
||||
.push(UnionElement::BytesLiterals(FxOrderSet::from_iter([
|
||||
literal,
|
||||
])));
|
||||
}
|
||||
if let Some(index) = to_remove {
|
||||
self.elements.swap_remove(index);
|
||||
}
|
||||
}
|
||||
// And same for int literals as well.
|
||||
Type::IntLiteral(literal) => {
|
||||
let mut found = false;
|
||||
let mut found = None;
|
||||
let mut to_remove = None;
|
||||
let ty_negated = ty.negate(self.db);
|
||||
for element in &mut self.elements {
|
||||
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||
match element {
|
||||
UnionElement::IntLiterals(literals) => {
|
||||
if literals.len() >= MAX_UNION_LITERALS {
|
||||
|
@ -313,14 +344,16 @@ impl<'db> UnionBuilder<'db> {
|
|||
self.add_in_place(replace_with);
|
||||
return;
|
||||
}
|
||||
literals.insert(literal);
|
||||
found = true;
|
||||
break;
|
||||
found = Some(literals);
|
||||
continue;
|
||||
}
|
||||
UnionElement::Type(existing) => {
|
||||
if ty.is_subtype_of(self.db, *existing) {
|
||||
return;
|
||||
}
|
||||
if existing.is_subtype_of(self.db, ty) {
|
||||
to_remove = Some(index);
|
||||
}
|
||||
if ty_negated.is_subtype_of(self.db, *existing) {
|
||||
// The type that includes both this new element, and its negation
|
||||
// (or a supertype of its negation), must be simply `object`.
|
||||
|
@ -331,10 +364,15 @@ impl<'db> UnionBuilder<'db> {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if let Some(found) = found {
|
||||
found.insert(literal);
|
||||
} else {
|
||||
self.elements
|
||||
.push(UnionElement::IntLiterals(FxOrderSet::from_iter([literal])));
|
||||
}
|
||||
if let Some(index) = to_remove {
|
||||
self.elements.swap_remove(index);
|
||||
}
|
||||
}
|
||||
// Adding `object` to a union results in `object`.
|
||||
ty if ty.is_object(self.db) => {
|
||||
|
@ -347,7 +385,6 @@ impl<'db> UnionBuilder<'db> {
|
|||
None
|
||||
};
|
||||
|
||||
let mut to_add = ty;
|
||||
let mut to_remove = SmallVec::<[usize; 2]>::new();
|
||||
let ty_negated = ty.negate(self.db);
|
||||
|
||||
|
@ -364,20 +401,17 @@ impl<'db> UnionBuilder<'db> {
|
|||
self.collapse_to_object();
|
||||
return;
|
||||
}
|
||||
ReduceResult::Ignore => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
if Some(element_type) == bool_pair {
|
||||
to_add = KnownClass::Bool.to_instance(self.db);
|
||||
to_remove.push(index);
|
||||
// The type we are adding is a BooleanLiteral, which doesn't have any
|
||||
// subtypes. And we just found that the union already contained our
|
||||
// mirror-image BooleanLiteral, so it can't also contain bool or any
|
||||
// supertype of bool. Therefore, we are done.
|
||||
break;
|
||||
self.add_in_place(KnownClass::Bool.to_instance(self.db));
|
||||
return;
|
||||
}
|
||||
|
||||
if ty.is_gradual_equivalent_to(self.db, element_type)
|
||||
if ty.is_equivalent_to(self.db, element_type)
|
||||
|| ty.is_subtype_of(self.db, element_type)
|
||||
|| element_type.is_object(self.db)
|
||||
{
|
||||
return;
|
||||
} else if element_type.is_subtype_of(self.db, ty) {
|
||||
|
@ -397,13 +431,13 @@ impl<'db> UnionBuilder<'db> {
|
|||
}
|
||||
}
|
||||
if let Some((&first, rest)) = to_remove.split_first() {
|
||||
self.elements[first] = UnionElement::Type(to_add);
|
||||
self.elements[first] = UnionElement::Type(ty);
|
||||
// We iterate in descending order to keep remaining indices valid after `swap_remove`.
|
||||
for &index in rest.iter().rev() {
|
||||
self.elements.swap_remove(index);
|
||||
}
|
||||
} else {
|
||||
self.elements.push(UnionElement::Type(to_add));
|
||||
self.elements.push(UnionElement::Type(ty));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -681,7 +715,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||
for (index, existing_positive) in self.positive.iter().enumerate() {
|
||||
// S & T = S if S <: T
|
||||
if existing_positive.is_subtype_of(db, new_positive)
|
||||
|| existing_positive.is_gradual_equivalent_to(db, new_positive)
|
||||
|| existing_positive.is_equivalent_to(db, new_positive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -778,7 +812,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||
for (index, existing_negative) in self.negative.iter().enumerate() {
|
||||
// ~S & ~T = ~T if S <: T
|
||||
if existing_negative.is_subtype_of(db, new_negative)
|
||||
|| existing_negative.is_gradual_equivalent_to(db, new_negative)
|
||||
|| existing_negative.is_equivalent_to(db, new_negative)
|
||||
{
|
||||
to_remove.push(index);
|
||||
}
|
||||
|
|
|
@ -599,21 +599,6 @@ impl<'db> Bindings<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsGradualEquivalentTo) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_gradual_equivalent_to(db, *ty_b),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsFullyStatic) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload
|
||||
.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsSingleton) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
|
||||
|
@ -801,15 +786,13 @@ impl<'db> Bindings<'db> {
|
|||
overload.set_return_type(
|
||||
match instance_ty.static_member(db, attr_name.value(db)) {
|
||||
Place::Type(ty, Boundness::Bound) => {
|
||||
if instance_ty.is_fully_static(db) {
|
||||
ty
|
||||
} else {
|
||||
if ty.is_dynamic() {
|
||||
// Here, we attempt to model the fact that an attribute lookup on
|
||||
// a non-fully static type could fail. This is an approximation,
|
||||
// as there are gradual types like `tuple[Any]`, on which a lookup
|
||||
// of (e.g. of the `index` method) would always succeed.
|
||||
// a dynamic type could fail
|
||||
|
||||
union_with_default(ty)
|
||||
} else {
|
||||
ty
|
||||
}
|
||||
}
|
||||
Place::Type(ty, Boundness::PossiblyUnbound) => {
|
||||
|
@ -1396,7 +1379,7 @@ impl<'db> CallableBinding<'db> {
|
|||
.annotated_type()
|
||||
.unwrap_or(Type::unknown());
|
||||
if let Some(first_parameter_type) = first_parameter_type {
|
||||
if !first_parameter_type.is_gradual_equivalent_to(db, current_parameter_type) {
|
||||
if !first_parameter_type.is_equivalent_to(db, current_parameter_type) {
|
||||
participating_parameter_index = Some(parameter_index);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -379,9 +379,10 @@ impl<'db> ClassType<'db> {
|
|||
) -> bool {
|
||||
self.iter_mro(db).any(|base| {
|
||||
match base {
|
||||
ClassBase::Dynamic(_) => {
|
||||
relation.applies_to_non_fully_static_types() && !other.is_final(db)
|
||||
}
|
||||
ClassBase::Dynamic(_) => match relation {
|
||||
TypeRelation::Subtyping => other.is_object(db),
|
||||
TypeRelation::Assignability => !other.is_final(db),
|
||||
},
|
||||
|
||||
// Protocol and Generic are not represented by a ClassType.
|
||||
ClassBase::Protocol | ClassBase::Generic => false,
|
||||
|
@ -417,20 +418,6 @@ impl<'db> ClassType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
match (self, other) {
|
||||
(ClassType::NonGeneric(this), ClassType::NonGeneric(other)) => this == other,
|
||||
(ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => false,
|
||||
|
||||
(ClassType::Generic(this), ClassType::Generic(other)) => {
|
||||
this.origin(db) == other.origin(db)
|
||||
&& this
|
||||
.specialization(db)
|
||||
.is_gradual_equivalent_to(db, other.specialization(db))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred.
|
||||
pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
|
||||
let (class_literal, specialization) = self.class_literal(db);
|
||||
|
@ -1427,14 +1414,15 @@ impl<'db> ClassLiteral<'db> {
|
|||
continue;
|
||||
}
|
||||
|
||||
// The descriptor handling below is guarded by this fully-static check, because dynamic
|
||||
// types like `Any` are valid (data) descriptors: since they have all possible attributes,
|
||||
// they also have a (callable) `__set__` method. The problem is that we can't determine
|
||||
// the type of the value parameter this way. Instead, we want to use the dynamic type
|
||||
// itself in this case, so we skip the special descriptor handling.
|
||||
if attr_ty.is_fully_static(db) {
|
||||
let dunder_set = attr_ty.class_member(db, "__set__".into());
|
||||
if let Some(dunder_set) = dunder_set.place.ignore_possibly_unbound() {
|
||||
let dunder_set = attr_ty.class_member(db, "__set__".into());
|
||||
if let Place::Type(dunder_set, Boundness::Bound) = dunder_set.place {
|
||||
// The descriptor handling below is guarded by this not-dynamic check, because
|
||||
// dynamic types like `Any` are valid (data) descriptors: since they have all
|
||||
// possible attributes, they also have a (callable) `__set__` method. The
|
||||
// problem is that we can't determine the type of the value parameter this way.
|
||||
// Instead, we want to use the dynamic type itself in this case, so we skip the
|
||||
// special descriptor handling.
|
||||
if !dunder_set.is_dynamic() {
|
||||
// This type of this attribute is a data descriptor. Instead of overwriting the
|
||||
// descriptor attribute, data-classes will (implicitly) call the `__set__` method
|
||||
// of the descriptor. This means that the synthesized `__init__` parameter for
|
||||
|
|
|
@ -148,12 +148,12 @@ declare_lint! {
|
|||
/// ## Examples
|
||||
/// ```python
|
||||
/// from typing import reveal_type
|
||||
/// from ty_extensions import is_fully_static
|
||||
/// from ty_extensions import is_singleton
|
||||
///
|
||||
/// if flag:
|
||||
/// f = repr # Expects a value
|
||||
/// else:
|
||||
/// f = is_fully_static # Expects a type form
|
||||
/// f = is_singleton # Expects a type form
|
||||
///
|
||||
/// f(int) # error
|
||||
/// ```
|
||||
|
|
|
@ -736,9 +736,6 @@ impl<'db> FunctionType<'db> {
|
|||
}
|
||||
let self_signature = self.signature(db);
|
||||
let other_signature = other.signature(db);
|
||||
if !self_signature.is_fully_static(db) || !other_signature.is_fully_static(db) {
|
||||
return false;
|
||||
}
|
||||
self_signature.is_subtype_of(db, other_signature)
|
||||
}
|
||||
|
||||
|
@ -760,19 +757,9 @@ impl<'db> FunctionType<'db> {
|
|||
}
|
||||
let self_signature = self.signature(db);
|
||||
let other_signature = other.signature(db);
|
||||
if !self_signature.is_fully_static(db) || !other_signature.is_fully_static(db) {
|
||||
return false;
|
||||
}
|
||||
self_signature.is_equivalent_to(db, other_signature)
|
||||
}
|
||||
|
||||
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.literal(db) == other.literal(db)
|
||||
&& self
|
||||
.signature(db)
|
||||
.is_gradual_equivalent_to(db, other.signature(db))
|
||||
}
|
||||
|
||||
pub(crate) fn find_legacy_typevars(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
@ -878,10 +865,6 @@ pub enum KnownFunction {
|
|||
IsAssignableTo,
|
||||
/// `ty_extensions.is_disjoint_from`
|
||||
IsDisjointFrom,
|
||||
/// `ty_extensions.is_gradual_equivalent_to`
|
||||
IsGradualEquivalentTo,
|
||||
/// `ty_extensions.is_fully_static`
|
||||
IsFullyStatic,
|
||||
/// `ty_extensions.is_singleton`
|
||||
IsSingleton,
|
||||
/// `ty_extensions.is_single_valued`
|
||||
|
@ -948,8 +931,6 @@ impl KnownFunction {
|
|||
Self::IsAssignableTo
|
||||
| Self::IsDisjointFrom
|
||||
| Self::IsEquivalentTo
|
||||
| Self::IsGradualEquivalentTo
|
||||
| Self::IsFullyStatic
|
||||
| Self::IsSingleValued
|
||||
| Self::IsSingleton
|
||||
| Self::IsSubtypeOf
|
||||
|
@ -1009,12 +990,10 @@ pub(crate) mod tests {
|
|||
| KnownFunction::GenericContext
|
||||
| KnownFunction::DunderAllNames
|
||||
| KnownFunction::StaticAssert
|
||||
| KnownFunction::IsFullyStatic
|
||||
| KnownFunction::IsDisjointFrom
|
||||
| KnownFunction::IsSingleValued
|
||||
| KnownFunction::IsAssignableTo
|
||||
| KnownFunction::IsEquivalentTo
|
||||
| KnownFunction::IsGradualEquivalentTo
|
||||
| KnownFunction::TopMaterialization
|
||||
| KnownFunction::BottomMaterialization
|
||||
| KnownFunction::AllMembers => KnownModule::TyExtensions,
|
||||
|
|
|
@ -463,10 +463,6 @@ impl<'db> Specialization<'db> {
|
|||
.zip(self.types(db))
|
||||
.zip(other.types(db))
|
||||
{
|
||||
if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Equivalence of each type in the specialization depends on the variance of the
|
||||
// corresponding typevar:
|
||||
// - covariant: verify that self_type == other_type
|
||||
|
@ -487,42 +483,6 @@ impl<'db> Specialization<'db> {
|
|||
true
|
||||
}
|
||||
|
||||
pub(crate) fn is_gradual_equivalent_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Specialization<'db>,
|
||||
) -> bool {
|
||||
let generic_context = self.generic_context(db);
|
||||
if generic_context != other.generic_context(db) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ((typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
|
||||
.zip(self.types(db))
|
||||
.zip(other.types(db))
|
||||
{
|
||||
// Equivalence of each type in the specialization depends on the variance of the
|
||||
// corresponding typevar:
|
||||
// - covariant: verify that self_type == other_type
|
||||
// - contravariant: verify that other_type == self_type
|
||||
// - invariant: verify that self_type == other_type
|
||||
// - bivariant: skip, can't make equivalence false
|
||||
let compatible = match typevar.variance(db) {
|
||||
TypeVarVariance::Invariant
|
||||
| TypeVarVariance::Covariant
|
||||
| TypeVarVariance::Contravariant => {
|
||||
self_type.is_gradual_equivalent_to(db, *other_type)
|
||||
}
|
||||
TypeVarVariance::Bivariant => true,
|
||||
};
|
||||
if !compatible {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn find_legacy_typevars(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
|
|
@ -5448,8 +5448,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
if let [Some(actual_ty), Some(asserted_ty)] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
if !actual_ty
|
||||
.is_gradual_equivalent_to(self.db(), *asserted_ty)
|
||||
if !actual_ty.is_equivalent_to(self.db(), *asserted_ty)
|
||||
{
|
||||
if let Some(builder) = self.context.report_lint(
|
||||
&TYPE_ASSERTION_FAILURE,
|
||||
|
@ -5586,14 +5585,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
let db = self.db();
|
||||
let contains_unknown_or_todo = |ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any);
|
||||
if source_type.is_equivalent_to(db, *casted_type)
|
||||
|| (source_type.normalized(db)
|
||||
== casted_type.normalized(db)
|
||||
&& !casted_type.any_over_type(db, &|ty| {
|
||||
contains_unknown_or_todo(ty)
|
||||
})
|
||||
&& !source_type.any_over_type(db, &|ty| {
|
||||
contains_unknown_or_todo(ty)
|
||||
}))
|
||||
&& !casted_type.any_over_type(db, &|ty| {
|
||||
contains_unknown_or_todo(ty)
|
||||
})
|
||||
&& !source_type.any_over_type(db, &|ty| {
|
||||
contains_unknown_or_todo(ty)
|
||||
})
|
||||
{
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
|
|
|
@ -108,10 +108,6 @@ impl<'db> NominalInstanceType<'db> {
|
|||
!self.class.could_coexist_in_mro_with(db, other.class)
|
||||
}
|
||||
|
||||
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.class.is_gradual_equivalent_to(db, other.class)
|
||||
}
|
||||
|
||||
pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool {
|
||||
self.class.known(db).is_some_and(KnownClass::is_singleton)
|
||||
}
|
||||
|
@ -240,11 +236,6 @@ impl<'db> ProtocolInstanceType<'db> {
|
|||
self.inner.interface(db).any_over_type(db, type_fn)
|
||||
}
|
||||
|
||||
/// Return `true` if this protocol type is fully static.
|
||||
pub(super) fn is_fully_static(self, db: &'db dyn Db) -> bool {
|
||||
self.inner.interface(db).is_fully_static(db)
|
||||
}
|
||||
|
||||
/// Return `true` if this protocol type has the given type relation to the protocol `other`.
|
||||
///
|
||||
/// TODO: consider the types of the members as well as their existence
|
||||
|
@ -252,13 +243,9 @@ impl<'db> ProtocolInstanceType<'db> {
|
|||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
_relation: TypeRelation,
|
||||
) -> bool {
|
||||
relation.applies_to(
|
||||
db,
|
||||
Type::ProtocolInstance(self),
|
||||
Type::ProtocolInstance(other),
|
||||
) && other
|
||||
other
|
||||
.inner
|
||||
.interface(db)
|
||||
.is_sub_interface_of(db, self.inner.interface(db))
|
||||
|
@ -268,15 +255,6 @@ impl<'db> ProtocolInstanceType<'db> {
|
|||
///
|
||||
/// TODO: consider the types of the members as well as their existence
|
||||
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.is_fully_static(db)
|
||||
&& other.is_fully_static(db)
|
||||
&& self.normalized(db) == other.normalized(db)
|
||||
}
|
||||
|
||||
/// Return `true` if this protocol type is gradually equivalent to the protocol `other`.
|
||||
///
|
||||
/// TODO: consider the types of the members as well as their existence
|
||||
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.normalized(db) == other.normalized(db)
|
||||
}
|
||||
|
||||
|
|
|
@ -50,9 +50,19 @@ macro_rules! type_property_test {
|
|||
$property
|
||||
}
|
||||
};
|
||||
($test_name:ident, $db:ident, forall fully_static_types $($types:ident),+ . $property:expr) => {
|
||||
#[quickcheck_macros::quickcheck]
|
||||
#[ignore]
|
||||
fn $test_name($($types: crate::types::property_tests::type_generation::FullyStaticTy),+) -> bool {
|
||||
let $db = &crate::types::property_tests::setup::get_cached_db();
|
||||
$(let $types = $types.into_type($db);)+
|
||||
|
||||
$property
|
||||
}
|
||||
};
|
||||
// A property test with a logical implication.
|
||||
($name:ident, $db:ident, forall types $($types:ident),+ . $premise:expr => $conclusion:expr) => {
|
||||
type_property_test!($name, $db, forall types $($types),+ . !($premise) || ($conclusion));
|
||||
($name:ident, $db:ident, forall $typekind:ident $($types:ident),+ . $premise:expr => $conclusion:expr) => {
|
||||
type_property_test!($name, $db, forall $typekind $($types),+ . !($premise) || ($conclusion));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -63,11 +73,10 @@ mod stable {
|
|||
// Reflexivity: `T` is equivalent to itself.
|
||||
type_property_test!(
|
||||
equivalent_to_is_reflexive, db,
|
||||
forall types t. t.is_fully_static(db) => t.is_equivalent_to(db, t)
|
||||
forall types t. t.is_equivalent_to(db, t)
|
||||
);
|
||||
|
||||
// Symmetry: If `S` is equivalent to `T`, then `T` must be equivalent to `S`.
|
||||
// Note that this (trivially) holds true for gradual types as well.
|
||||
type_property_test!(
|
||||
equivalent_to_is_symmetric, db,
|
||||
forall types s, t. s.is_equivalent_to(db, t) => t.is_equivalent_to(db, s)
|
||||
|
@ -79,18 +88,6 @@ mod stable {
|
|||
forall types s, t, u. s.is_equivalent_to(db, t) && t.is_equivalent_to(db, u) => s.is_equivalent_to(db, u)
|
||||
);
|
||||
|
||||
// Symmetry: If `S` is gradual equivalent to `T`, `T` is gradual equivalent to `S`.
|
||||
type_property_test!(
|
||||
gradual_equivalent_to_is_symmetric, db,
|
||||
forall types s, t. s.is_gradual_equivalent_to(db, t) => t.is_gradual_equivalent_to(db, s)
|
||||
);
|
||||
|
||||
// A fully static type `T` is a subtype of itself.
|
||||
type_property_test!(
|
||||
subtype_of_is_reflexive, db,
|
||||
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, t)
|
||||
);
|
||||
|
||||
// `S <: T` and `T <: U` implies that `S <: U`.
|
||||
type_property_test!(
|
||||
subtype_of_is_transitive, db,
|
||||
|
@ -133,28 +130,16 @@ mod stable {
|
|||
forall types t. t.is_singleton(db) => t.is_single_valued(db)
|
||||
);
|
||||
|
||||
// If `T` contains a gradual form, it should not participate in equivalence
|
||||
type_property_test!(
|
||||
non_fully_static_types_do_not_participate_in_equivalence, db,
|
||||
forall types s, t. !s.is_fully_static(db) => !s.is_equivalent_to(db, t) && !t.is_equivalent_to(db, s)
|
||||
);
|
||||
|
||||
// If `T` contains a gradual form, it should not participate in subtyping
|
||||
type_property_test!(
|
||||
non_fully_static_types_do_not_participate_in_subtyping, db,
|
||||
forall types s, t. !s.is_fully_static(db) => !s.is_subtype_of(db, t) && !t.is_subtype_of(db, s)
|
||||
);
|
||||
|
||||
// All types should be assignable to `object`
|
||||
type_property_test!(
|
||||
all_types_assignable_to_object, db,
|
||||
forall types t. t.is_assignable_to(db, Type::object(db))
|
||||
);
|
||||
|
||||
// And for fully static types, they should also be subtypes of `object`
|
||||
// And all types should be subtypes of `object`
|
||||
type_property_test!(
|
||||
all_fully_static_types_subtype_of_object, db,
|
||||
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, Type::object(db))
|
||||
all_types_subtype_of_object, db,
|
||||
forall types t. t.is_subtype_of(db, Type::object(db))
|
||||
);
|
||||
|
||||
// Never should be assignable to every type
|
||||
|
@ -163,54 +148,63 @@ mod stable {
|
|||
forall types t. Type::Never.is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// And it should be a subtype of all fully static types
|
||||
// And it should be a subtype of all types
|
||||
type_property_test!(
|
||||
never_subtype_of_every_fully_static_type, db,
|
||||
forall types t. t.is_fully_static(db) => Type::Never.is_subtype_of(db, t)
|
||||
never_subtype_of_every_type, db,
|
||||
forall types t. Type::Never.is_subtype_of(db, t)
|
||||
);
|
||||
|
||||
// Similar to `Never`, a fully-static "bottom" callable type should be a subtype of all
|
||||
// fully-static callable types
|
||||
// Similar to `Never`, a "bottom" callable type should be a subtype of all callable types
|
||||
type_property_test!(
|
||||
bottom_callable_is_subtype_of_all_fully_static_callable, db,
|
||||
forall types t. t.is_callable_type() && t.is_fully_static(db)
|
||||
bottom_callable_is_subtype_of_all_callable, db,
|
||||
forall types t. t.is_callable_type()
|
||||
=> CallableType::bottom(db).is_subtype_of(db, t)
|
||||
);
|
||||
|
||||
// For any two fully static types, each type in the pair must be a subtype of their union.
|
||||
type_property_test!(
|
||||
all_fully_static_type_pairs_are_subtype_of_their_union, db,
|
||||
forall types s, t.
|
||||
s.is_fully_static(db) && t.is_fully_static(db)
|
||||
=> s.is_subtype_of(db, union(db, [s, t])) && t.is_subtype_of(db, union(db, [s, t]))
|
||||
);
|
||||
|
||||
// A fully static type does not have any materializations.
|
||||
// Thus, two equivalent (fully static) types are also gradual equivalent.
|
||||
type_property_test!(
|
||||
two_equivalent_types_are_also_gradual_equivalent, db,
|
||||
forall types s, t. s.is_equivalent_to(db, t) => s.is_gradual_equivalent_to(db, t)
|
||||
);
|
||||
|
||||
// Two gradual equivalent fully static types are also equivalent.
|
||||
type_property_test!(
|
||||
two_gradual_equivalent_fully_static_types_are_also_equivalent, db,
|
||||
forall types s, t.
|
||||
s.is_fully_static(db) && s.is_gradual_equivalent_to(db, t) => s.is_equivalent_to(db, t)
|
||||
);
|
||||
|
||||
// `T` can be assigned to itself.
|
||||
type_property_test!(
|
||||
assignable_to_is_reflexive, db,
|
||||
forall types t. t.is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// For *any* pair of types, whether fully static or not,
|
||||
// each of the pair should be assignable to the union of the two.
|
||||
// For *any* pair of types, each of the pair should be assignable to the union of the two.
|
||||
type_property_test!(
|
||||
all_type_pairs_are_assignable_to_their_union, db,
|
||||
forall types s, t. s.is_assignable_to(db, union(db, [s, t])) && t.is_assignable_to(db, union(db, [s, t]))
|
||||
);
|
||||
|
||||
// Only `Never` is a subtype of `Any`.
|
||||
type_property_test!(
|
||||
only_never_is_subtype_of_any, db,
|
||||
forall types s. !s.is_equivalent_to(db, Type::Never) => !s.is_subtype_of(db, Type::any())
|
||||
);
|
||||
|
||||
// Only `object` is a supertype of `Any`.
|
||||
type_property_test!(
|
||||
only_object_is_supertype_of_any, db,
|
||||
forall types t. !t.is_equivalent_to(db, Type::object(db)) => !Type::any().is_subtype_of(db, t)
|
||||
);
|
||||
|
||||
// Equivalence is commutative.
|
||||
type_property_test!(
|
||||
equivalent_to_is_commutative, db,
|
||||
forall types s, t. s.is_equivalent_to(db, t) == t.is_equivalent_to(db, s)
|
||||
);
|
||||
|
||||
// A fully static type `T` is a subtype of itself. (This is not true for non-fully-static
|
||||
// types; `Any` is not a subtype of `Any`, only `Never` is.)
|
||||
type_property_test!(
|
||||
subtype_of_is_reflexive_for_fully_static_types, db,
|
||||
forall fully_static_types t. t.is_subtype_of(db, t)
|
||||
);
|
||||
|
||||
// For any two fully static types, each type in the pair must be a subtype of their union.
|
||||
// (This is clearly not true for non-fully-static types, since their subtyping is not
|
||||
// reflexive.)
|
||||
type_property_test!(
|
||||
all_fully_static_type_pairs_are_subtype_of_their_union, db,
|
||||
forall fully_static_types s, t. s.is_subtype_of(db, union(db, [s, t])) && t.is_subtype_of(db, union(db, [s, t]))
|
||||
);
|
||||
}
|
||||
|
||||
/// This module contains property tests that currently lead to many false positives.
|
||||
|
@ -231,21 +225,21 @@ mod flaky {
|
|||
forall types t. t.negate(db).negate(db).is_equivalent_to(db, t)
|
||||
);
|
||||
|
||||
// ~T should be disjoint from T
|
||||
// For any fully static type `T`, `T` should be disjoint from `~T`.
|
||||
// https://github.com/astral-sh/ty/issues/216
|
||||
type_property_test!(
|
||||
negation_is_disjoint, db,
|
||||
forall types t. t.is_fully_static(db) => t.negate(db).is_disjoint_from(db, t)
|
||||
negation_of_fully_static_types_is_disjoint, db,
|
||||
forall fully_static_types t. t.negate(db).is_disjoint_from(db, t)
|
||||
);
|
||||
|
||||
// For two fully static types, their intersection must be a subtype of each type in the pair.
|
||||
// For two types, their intersection must be a subtype of each type in the pair.
|
||||
type_property_test!(
|
||||
all_fully_static_type_pairs_are_supertypes_of_their_intersection, db,
|
||||
all_type_pairs_are_supertypes_of_their_intersection, db,
|
||||
forall types s, t.
|
||||
s.is_fully_static(db) && t.is_fully_static(db)
|
||||
=> intersection(db, [s, t]).is_subtype_of(db, s) && intersection(db, [s, t]).is_subtype_of(db, t)
|
||||
intersection(db, [s, t]).is_subtype_of(db, s) && intersection(db, [s, t]).is_subtype_of(db, t)
|
||||
);
|
||||
|
||||
// And for non-fully-static types, the intersection of a pair of types
|
||||
// And the intersection of a pair of types
|
||||
// should be assignable to both types of the pair.
|
||||
// Currently fails due to https://github.com/astral-sh/ruff/issues/14899
|
||||
type_property_test!(
|
||||
|
@ -258,8 +252,7 @@ mod flaky {
|
|||
type_property_test!(
|
||||
intersection_equivalence_not_order_dependent, db,
|
||||
forall types s, t, u.
|
||||
s.is_fully_static(db) && t.is_fully_static(db) && u.is_fully_static(db)
|
||||
=> [s, t, u]
|
||||
[s, t, u]
|
||||
.into_iter()
|
||||
.permutations(3)
|
||||
.map(|trio_of_types| intersection(db, trio_of_types))
|
||||
|
@ -272,8 +265,7 @@ mod flaky {
|
|||
type_property_test!(
|
||||
union_equivalence_not_order_dependent, db,
|
||||
forall types s, t, u.
|
||||
s.is_fully_static(db) && t.is_fully_static(db) && u.is_fully_static(db)
|
||||
=> [s, t, u]
|
||||
[s, t, u]
|
||||
.into_iter()
|
||||
.permutations(3)
|
||||
.map(|trio_of_types| union(db, trio_of_types))
|
||||
|
|
|
@ -207,16 +207,31 @@ impl Ty {
|
|||
}
|
||||
}
|
||||
|
||||
fn arbitrary_core_type(g: &mut Gen) -> Ty {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct FullyStaticTy(Ty);
|
||||
|
||||
impl FullyStaticTy {
|
||||
pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> {
|
||||
self.0.into_type(db)
|
||||
}
|
||||
}
|
||||
|
||||
fn arbitrary_core_type(g: &mut Gen, fully_static: bool) -> Ty {
|
||||
// We could select a random integer here, but this would make it much less
|
||||
// likely to explore interesting edge cases:
|
||||
let int_lit = Ty::IntLiteral(*g.choose(&[-2, -1, 0, 1, 2]).unwrap());
|
||||
let bool_lit = Ty::BooleanLiteral(bool::arbitrary(g));
|
||||
g.choose(&[
|
||||
Ty::Never,
|
||||
Ty::Unknown,
|
||||
Ty::None,
|
||||
|
||||
// Update this if new non-fully-static types are added below.
|
||||
let fully_static_index = 3;
|
||||
let types = &[
|
||||
Ty::Any,
|
||||
Ty::Unknown,
|
||||
Ty::SubclassOfAny,
|
||||
// Add fully static types below, dynamic types above.
|
||||
// Update `fully_static_index` above if adding new dynamic types!
|
||||
Ty::Never,
|
||||
Ty::None,
|
||||
int_lit,
|
||||
bool_lit,
|
||||
Ty::StringLiteral(""),
|
||||
|
@ -241,7 +256,6 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
|
|||
Ty::BuiltinInstance("type"),
|
||||
Ty::AbcInstance("ABC"),
|
||||
Ty::AbcInstance("ABCMeta"),
|
||||
Ty::SubclassOfAny,
|
||||
Ty::SubclassOfBuiltinClass("object"),
|
||||
Ty::SubclassOfBuiltinClass("str"),
|
||||
Ty::SubclassOfBuiltinClass("type"),
|
||||
|
@ -261,9 +275,13 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
|
|||
class: "int",
|
||||
method: "bit_length",
|
||||
},
|
||||
])
|
||||
.unwrap()
|
||||
.clone()
|
||||
];
|
||||
let types = if fully_static {
|
||||
&types[fully_static_index..]
|
||||
} else {
|
||||
types
|
||||
};
|
||||
g.choose(types).unwrap().clone()
|
||||
}
|
||||
|
||||
/// Constructs an arbitrary type.
|
||||
|
@ -271,53 +289,54 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
|
|||
/// The `size` parameter controls the depth of the type tree. For example,
|
||||
/// a simple type like `int` has a size of 0, `Union[int, str]` has a size
|
||||
/// of 1, `tuple[int, Union[str, bytes]]` has a size of 2, etc.
|
||||
fn arbitrary_type(g: &mut Gen, size: u32) -> Ty {
|
||||
///
|
||||
/// The `fully_static` parameter, if `true`, limits generation to fully static types.
|
||||
fn arbitrary_type(g: &mut Gen, size: u32, fully_static: bool) -> Ty {
|
||||
if size == 0 {
|
||||
arbitrary_core_type(g)
|
||||
arbitrary_core_type(g, fully_static)
|
||||
} else {
|
||||
match u32::arbitrary(g) % 6 {
|
||||
0 => arbitrary_core_type(g),
|
||||
0 => arbitrary_core_type(g, fully_static),
|
||||
1 => Ty::Union(
|
||||
(0..*g.choose(&[2, 3]).unwrap())
|
||||
.map(|_| arbitrary_type(g, size - 1))
|
||||
.map(|_| arbitrary_type(g, size - 1, fully_static))
|
||||
.collect(),
|
||||
),
|
||||
2 => Ty::FixedLengthTuple(
|
||||
(0..*g.choose(&[0, 1, 2]).unwrap())
|
||||
.map(|_| arbitrary_type(g, size - 1))
|
||||
.map(|_| arbitrary_type(g, size - 1, fully_static))
|
||||
.collect(),
|
||||
),
|
||||
3 => Ty::VariableLengthTuple(
|
||||
(0..*g.choose(&[0, 1, 2]).unwrap())
|
||||
.map(|_| arbitrary_type(g, size - 1))
|
||||
.map(|_| arbitrary_type(g, size - 1, fully_static))
|
||||
.collect(),
|
||||
Box::new(arbitrary_type(g, size - 1)),
|
||||
Box::new(arbitrary_type(g, size - 1, fully_static)),
|
||||
(0..*g.choose(&[0, 1, 2]).unwrap())
|
||||
.map(|_| arbitrary_type(g, size - 1))
|
||||
.map(|_| arbitrary_type(g, size - 1, fully_static))
|
||||
.collect(),
|
||||
),
|
||||
4 => Ty::Intersection {
|
||||
pos: (0..*g.choose(&[0, 1, 2]).unwrap())
|
||||
.map(|_| arbitrary_type(g, size - 1))
|
||||
.map(|_| arbitrary_type(g, size - 1, fully_static))
|
||||
.collect(),
|
||||
neg: (0..*g.choose(&[0, 1, 2]).unwrap())
|
||||
.map(|_| arbitrary_type(g, size - 1))
|
||||
.map(|_| arbitrary_type(g, size - 1, fully_static))
|
||||
.collect(),
|
||||
},
|
||||
5 => Ty::Callable {
|
||||
params: match u32::arbitrary(g) % 2 {
|
||||
0 => CallableParams::GradualForm,
|
||||
1 => CallableParams::List(arbitrary_parameter_list(g, size)),
|
||||
_ => unreachable!(),
|
||||
0 if !fully_static => CallableParams::GradualForm,
|
||||
_ => CallableParams::List(arbitrary_parameter_list(g, size, fully_static)),
|
||||
},
|
||||
returns: arbitrary_optional_type(g, size - 1).map(Box::new),
|
||||
returns: arbitrary_annotation(g, size - 1, fully_static).map(Box::new),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec<Param> {
|
||||
fn arbitrary_parameter_list(g: &mut Gen, size: u32, fully_static: bool) -> Vec<Param> {
|
||||
let mut params: Vec<Param> = vec![];
|
||||
let mut used_names = HashSet::new();
|
||||
|
||||
|
@ -369,11 +388,11 @@ fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec<Param> {
|
|||
params.push(Param {
|
||||
kind: next_kind,
|
||||
name,
|
||||
annotated_ty: arbitrary_optional_type(g, size),
|
||||
annotated_ty: arbitrary_annotation(g, size, fully_static),
|
||||
default_ty: if matches!(next_kind, ParamKind::Variadic | ParamKind::KeywordVariadic) {
|
||||
None
|
||||
} else {
|
||||
arbitrary_optional_type(g, size)
|
||||
arbitrary_optional_type(g, size, fully_static)
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -381,10 +400,19 @@ fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec<Param> {
|
|||
params
|
||||
}
|
||||
|
||||
fn arbitrary_optional_type(g: &mut Gen, size: u32) -> Option<Ty> {
|
||||
/// An arbitrary optional type, always `Some` if fully static.
|
||||
fn arbitrary_annotation(g: &mut Gen, size: u32, fully_static: bool) -> Option<Ty> {
|
||||
if fully_static {
|
||||
Some(arbitrary_type(g, size, true))
|
||||
} else {
|
||||
arbitrary_optional_type(g, size, false)
|
||||
}
|
||||
}
|
||||
|
||||
fn arbitrary_optional_type(g: &mut Gen, size: u32, fully_static: bool) -> Option<Ty> {
|
||||
match u32::arbitrary(g) % 2 {
|
||||
0 => None,
|
||||
1 => Some(arbitrary_type(g, size)),
|
||||
1 => Some(arbitrary_type(g, size, fully_static)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
@ -404,7 +432,7 @@ fn arbitrary_optional_name(g: &mut Gen) -> Option<Name> {
|
|||
impl Arbitrary for Ty {
|
||||
fn arbitrary(g: &mut Gen) -> Ty {
|
||||
const MAX_SIZE: u32 = 2;
|
||||
arbitrary_type(g, MAX_SIZE)
|
||||
arbitrary_type(g, MAX_SIZE, false)
|
||||
}
|
||||
|
||||
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
|
||||
|
@ -491,6 +519,17 @@ impl Arbitrary for Ty {
|
|||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for FullyStaticTy {
|
||||
fn arbitrary(g: &mut Gen) -> FullyStaticTy {
|
||||
const MAX_SIZE: u32 = 2;
|
||||
FullyStaticTy(arbitrary_type(g, MAX_SIZE, true))
|
||||
}
|
||||
|
||||
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
|
||||
Box::new(self.0.shrink().map(FullyStaticTy))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn intersection<'db>(
|
||||
db: &'db TestDb,
|
||||
tys: impl IntoIterator<Item = Type<'db>>,
|
||||
|
|
|
@ -135,11 +135,6 @@ impl<'db> ProtocolInterface<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return `true` if all members of this protocol are fully static.
|
||||
pub(super) fn is_fully_static(self, db: &'db dyn Db) -> bool {
|
||||
self.members(db).all(|member| member.ty.is_fully_static(db))
|
||||
}
|
||||
|
||||
/// Return `true` if if all members on `self` are also members of `other`.
|
||||
///
|
||||
/// TODO: this method should consider the types of the members as well as their names.
|
||||
|
|
|
@ -97,15 +97,6 @@ impl<'db> CallableSignature<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Check whether this callable type is fully static.
|
||||
///
|
||||
/// See [`Type::is_fully_static`] for more details.
|
||||
pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool {
|
||||
self.overloads
|
||||
.iter()
|
||||
.all(|signature| signature.is_fully_static(db))
|
||||
}
|
||||
|
||||
pub(crate) fn has_relation_to(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
|
@ -123,9 +114,10 @@ impl<'db> CallableSignature<'db> {
|
|||
/// See [`Type::is_subtype_of`] for more details.
|
||||
pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
Self::has_relation_to_impl(
|
||||
db,
|
||||
&self.overloads,
|
||||
&other.overloads,
|
||||
&|self_signature, other_signature| self_signature.is_subtype_of(db, other_signature),
|
||||
TypeRelation::Subtyping,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -134,54 +126,54 @@ impl<'db> CallableSignature<'db> {
|
|||
/// See [`Type::is_assignable_to`] for more details.
|
||||
pub(crate) fn is_assignable_to(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
Self::has_relation_to_impl(
|
||||
db,
|
||||
&self.overloads,
|
||||
&other.overloads,
|
||||
&|self_signature, other_signature| self_signature.is_assignable_to(db, other_signature),
|
||||
TypeRelation::Assignability,
|
||||
)
|
||||
}
|
||||
|
||||
/// Implementation for the various relation checks between two, possible overloaded, callable
|
||||
/// Implementation of subtyping and assignability between two, possible overloaded, callable
|
||||
/// types.
|
||||
///
|
||||
/// The `check_signature` closure is used to check the relation between two [`Signature`]s.
|
||||
fn has_relation_to_impl<F>(
|
||||
fn has_relation_to_impl(
|
||||
db: &'db dyn Db,
|
||||
self_signatures: &[Signature<'db>],
|
||||
other_signatures: &[Signature<'db>],
|
||||
check_signature: &F,
|
||||
) -> bool
|
||||
where
|
||||
F: Fn(&Signature<'db>, &Signature<'db>) -> bool,
|
||||
{
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
match (self_signatures, other_signatures) {
|
||||
([self_signature], [other_signature]) => {
|
||||
// Base case: both callable types contain a single signature.
|
||||
check_signature(self_signature, other_signature)
|
||||
self_signature.has_relation_to(db, other_signature, relation)
|
||||
}
|
||||
|
||||
// `self` is possibly overloaded while `other` is definitely not overloaded.
|
||||
(_, [_]) => self_signatures.iter().any(|self_signature| {
|
||||
Self::has_relation_to_impl(
|
||||
db,
|
||||
std::slice::from_ref(self_signature),
|
||||
other_signatures,
|
||||
check_signature,
|
||||
relation,
|
||||
)
|
||||
}),
|
||||
|
||||
// `self` is definitely not overloaded while `other` is possibly overloaded.
|
||||
([_], _) => other_signatures.iter().all(|other_signature| {
|
||||
Self::has_relation_to_impl(
|
||||
db,
|
||||
self_signatures,
|
||||
std::slice::from_ref(other_signature),
|
||||
check_signature,
|
||||
relation,
|
||||
)
|
||||
}),
|
||||
|
||||
// `self` is definitely overloaded while `other` is possibly overloaded.
|
||||
(_, _) => other_signatures.iter().all(|other_signature| {
|
||||
Self::has_relation_to_impl(
|
||||
db,
|
||||
self_signatures,
|
||||
std::slice::from_ref(other_signature),
|
||||
check_signature,
|
||||
relation,
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
@ -197,14 +189,7 @@ impl<'db> CallableSignature<'db> {
|
|||
// equivalence check instead of delegating it to the subtype check.
|
||||
self_signature.is_equivalent_to(db, other_signature)
|
||||
}
|
||||
(self_signatures, other_signatures) => {
|
||||
if !self_signatures
|
||||
.iter()
|
||||
.chain(other_signatures.iter())
|
||||
.all(|signature| signature.is_fully_static(db))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
(_, _) => {
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
|
@ -213,21 +198,6 @@ impl<'db> CallableSignature<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Check whether this callable type is gradual equivalent to another callable type.
|
||||
///
|
||||
/// See [`Type::is_gradual_equivalent_to`] for more details.
|
||||
pub(crate) fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
match (self.overloads.as_slice(), other.overloads.as_slice()) {
|
||||
([self_signature], [other_signature]) => {
|
||||
self_signature.is_gradual_equivalent_to(db, other_signature)
|
||||
}
|
||||
_ => {
|
||||
// TODO: overloads
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replace_self_reference(&self, db: &'db dyn Db, class: ClassLiteral<'db>) -> Self {
|
||||
Self {
|
||||
overloads: self
|
||||
|
@ -435,61 +405,19 @@ impl<'db> Signature<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a fully static signature.
|
||||
///
|
||||
/// A signature is fully static if all of its parameters and return type are fully static and
|
||||
/// if it does not use gradual form (`...`) for its parameters.
|
||||
pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool {
|
||||
if self.parameters.is_gradual() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.parameters.iter().any(|parameter| {
|
||||
parameter
|
||||
.annotated_type()
|
||||
.is_none_or(|annotated_type| !annotated_type.is_fully_static(db))
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.return_ty
|
||||
.is_some_and(|return_type| return_type.is_fully_static(db))
|
||||
}
|
||||
|
||||
/// Return `true` if `self` has exactly the same set of possible static materializations as
|
||||
/// `other` (if `self` represents the same set of possible sets of possible runtime objects as
|
||||
/// `other`).
|
||||
pub(crate) fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool {
|
||||
self.is_equivalent_to_impl(other, |self_type, other_type| {
|
||||
pub(crate) fn is_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool {
|
||||
let check_types = |self_type: Option<Type<'db>>, other_type: Option<Type<'db>>| {
|
||||
self_type
|
||||
.unwrap_or(Type::unknown())
|
||||
.is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown()))
|
||||
})
|
||||
}
|
||||
.is_equivalent_to(db, other_type.unwrap_or(Type::unknown()))
|
||||
};
|
||||
|
||||
/// Return `true` if `self` represents the exact same set of possible runtime objects as `other`.
|
||||
pub(crate) fn is_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool {
|
||||
self.is_equivalent_to_impl(other, |self_type, other_type| {
|
||||
match (self_type, other_type) {
|
||||
(Some(self_type), Some(other_type)) => self_type.is_equivalent_to(db, other_type),
|
||||
// We need the catch-all case here because it's not guaranteed that this is a fully
|
||||
// static type.
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Implementation for the [`is_equivalent_to`] and [`is_gradual_equivalent_to`] for signature.
|
||||
///
|
||||
/// [`is_equivalent_to`]: Self::is_equivalent_to
|
||||
/// [`is_gradual_equivalent_to`]: Self::is_gradual_equivalent_to
|
||||
fn is_equivalent_to_impl<F>(&self, other: &Signature<'db>, check_types: F) -> bool
|
||||
where
|
||||
F: Fn(Option<Type<'db>>, Option<Type<'db>>) -> bool,
|
||||
{
|
||||
// N.B. We don't need to explicitly check for the use of gradual form (`...`) in the
|
||||
// parameters because it is internally represented by adding `*Any` and `**Any` to the
|
||||
// parameter list.
|
||||
if self.parameters.is_gradual() != other.parameters.is_gradual() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.parameters.len() != other.parameters.len() {
|
||||
return false;
|
||||
|
@ -554,38 +482,13 @@ impl<'db> Signature<'db> {
|
|||
true
|
||||
}
|
||||
|
||||
/// Return `true` if a callable with signature `self` is assignable to a callable with
|
||||
/// signature `other`.
|
||||
pub(crate) fn is_assignable_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool {
|
||||
self.is_assignable_to_impl(other, |type1, type2| {
|
||||
// In the context of a callable type, the `None` variant represents an `Unknown` type.
|
||||
type1
|
||||
.unwrap_or(Type::unknown())
|
||||
.is_assignable_to(db, type2.unwrap_or(Type::unknown()))
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if a callable with signature `self` is a subtype of a callable with signature
|
||||
/// `other`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `self` or `other` is not a fully static signature.
|
||||
pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool {
|
||||
self.is_assignable_to_impl(other, |type1, type2| {
|
||||
// SAFETY: Subtype relation is only checked for fully static types.
|
||||
type1.unwrap().is_subtype_of(db, type2.unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
/// Implementation for the [`is_assignable_to`] and [`is_subtype_of`] for signature.
|
||||
///
|
||||
/// [`is_assignable_to`]: Self::is_assignable_to
|
||||
/// [`is_subtype_of`]: Self::is_subtype_of
|
||||
fn is_assignable_to_impl<F>(&self, other: &Signature<'db>, check_types: F) -> bool
|
||||
where
|
||||
F: Fn(Option<Type<'db>>, Option<Type<'db>>) -> bool,
|
||||
{
|
||||
/// Implementation of subtyping and assignability for signature.
|
||||
fn has_relation_to(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
other: &Signature<'db>,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
/// A helper struct to zip two slices of parameters together that provides control over the
|
||||
/// two iterators individually. It also keeps track of the current parameter in each
|
||||
/// iterator.
|
||||
|
@ -647,17 +550,40 @@ impl<'db> Signature<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
let check_types = |type1: Option<Type<'db>>, type2: Option<Type<'db>>| {
|
||||
type1.unwrap_or(Type::unknown()).has_relation_to(
|
||||
db,
|
||||
type2.unwrap_or(Type::unknown()),
|
||||
relation,
|
||||
)
|
||||
};
|
||||
|
||||
// Return types are covariant.
|
||||
if !check_types(self.return_ty, other.return_ty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.parameters.is_gradual() || other.parameters.is_gradual() {
|
||||
// If either of the parameter lists contains a gradual form (`...`), then it is
|
||||
// assignable / subtype to and from any other callable type.
|
||||
// A gradual parameter list is a supertype of the "bottom" parameter list (*args: object,
|
||||
// **kwargs: object).
|
||||
if other.parameters.is_gradual()
|
||||
&& self
|
||||
.parameters
|
||||
.variadic()
|
||||
.is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object(db)))
|
||||
&& self
|
||||
.parameters
|
||||
.keyword_variadic()
|
||||
.is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object(db)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If either of the parameter lists is gradual (`...`), then it is assignable to and from
|
||||
// any other parameter list, but not a subtype or supertype of any other parameter list.
|
||||
if self.parameters.is_gradual() || other.parameters.is_gradual() {
|
||||
return relation.is_assignability();
|
||||
}
|
||||
|
||||
let mut parameters = ParametersZip {
|
||||
current_self: None,
|
||||
current_other: None,
|
||||
|
@ -979,23 +905,36 @@ pub(crate) struct Parameters<'db> {
|
|||
/// Whether this parameter list represents a gradual form using `...` as the only parameter.
|
||||
///
|
||||
/// If this is `true`, the `value` will still contain the variadic and keyword-variadic
|
||||
/// parameters. This flag is used to distinguish between an explicit `...` in the callable type
|
||||
/// as in `Callable[..., int]` and the variadic arguments in `lambda` expression as in
|
||||
/// `lambda *args, **kwargs: None`.
|
||||
/// parameters.
|
||||
///
|
||||
/// Per [the typing specification], any signature with a variadic and a keyword-variadic
|
||||
/// argument, both annotated (explicitly or implicitly) as `Any` or `Unknown`, is considered
|
||||
/// equivalent to `...`.
|
||||
///
|
||||
/// The display implementation utilizes this flag to use `...` instead of displaying the
|
||||
/// individual variadic and keyword-variadic parameters.
|
||||
///
|
||||
/// Note: This flag is also used to indicate invalid forms of `Callable` annotations.
|
||||
/// Note: This flag can also result from invalid forms of `Callable` annotations.
|
||||
///
|
||||
/// TODO: the spec also allows signatures like `Concatenate[int, ...]`, which have some number
|
||||
/// of required positional parameters followed by a gradual form. Our representation will need
|
||||
/// some adjustments to represent that.
|
||||
///
|
||||
/// [the typing specification]: https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable
|
||||
is_gradual: bool,
|
||||
}
|
||||
|
||||
impl<'db> Parameters<'db> {
|
||||
pub(crate) fn new(parameters: impl IntoIterator<Item = Parameter<'db>>) -> Self {
|
||||
Self {
|
||||
value: parameters.into_iter().collect(),
|
||||
is_gradual: false,
|
||||
}
|
||||
let value: Vec<Parameter<'db>> = parameters.into_iter().collect();
|
||||
let is_gradual = value.len() == 2
|
||||
&& value
|
||||
.iter()
|
||||
.any(|p| p.is_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic()))
|
||||
&& value.iter().any(|p| {
|
||||
p.is_keyword_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic())
|
||||
});
|
||||
Self { value, is_gradual }
|
||||
}
|
||||
|
||||
/// Create an empty parameter list.
|
||||
|
|
|
@ -73,10 +73,6 @@ impl<'db> SubclassOfType<'db> {
|
|||
subclass_of.is_dynamic()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_fully_static(self) -> bool {
|
||||
!self.is_dynamic()
|
||||
}
|
||||
|
||||
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> {
|
||||
match self.subclass_of {
|
||||
SubclassOfInner::Dynamic(_) => match variance {
|
||||
|
@ -146,9 +142,13 @@ impl<'db> SubclassOfType<'db> {
|
|||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
match (self.subclass_of, other.subclass_of) {
|
||||
(SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => {
|
||||
relation.applies_to_non_fully_static_types()
|
||||
(SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => {
|
||||
relation.is_assignability()
|
||||
}
|
||||
(SubclassOfInner::Dynamic(_), SubclassOfInner::Class(other_class)) => {
|
||||
other_class.is_object(db) || relation.is_assignability()
|
||||
}
|
||||
(SubclassOfInner::Class(_), SubclassOfInner::Dynamic(_)) => relation.is_assignability(),
|
||||
|
||||
// For example, `type[bool]` describes all possible runtime subclasses of the class `bool`,
|
||||
// and `type[int]` describes all possible runtime subclasses of the class `int`.
|
||||
|
|
|
@ -137,18 +137,10 @@ impl<'db> TupleType<'db> {
|
|||
self.tuple(db).is_equivalent_to(db, other.tuple(db))
|
||||
}
|
||||
|
||||
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.tuple(db).is_gradual_equivalent_to(db, other.tuple(db))
|
||||
}
|
||||
|
||||
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.tuple(db).is_disjoint_from(db, other.tuple(db))
|
||||
}
|
||||
|
||||
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
|
||||
self.tuple(db).is_fully_static(db)
|
||||
}
|
||||
|
||||
pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool {
|
||||
self.tuple(db).is_single_valued(db)
|
||||
}
|
||||
|
@ -292,17 +284,6 @@ impl<'db> FixedLengthTupleSpec<'db> {
|
|||
.all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty))
|
||||
}
|
||||
|
||||
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
self.0.len() == other.0.len()
|
||||
&& (self.0.iter())
|
||||
.zip(&other.0)
|
||||
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
|
||||
}
|
||||
|
||||
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
|
||||
self.0.iter().all(|ty| ty.is_fully_static(db))
|
||||
}
|
||||
|
||||
fn is_single_valued(&self, db: &'db dyn Db) -> bool {
|
||||
self.0.iter().all(|ty| ty.is_single_valued(db))
|
||||
}
|
||||
|
@ -667,32 +648,6 @@ impl<'db> VariableLengthTupleSpec<'db> {
|
|||
EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
self.variable.is_gradual_equivalent_to(db, other.variable)
|
||||
&& (self.prenormalized_prefix_elements(db, None))
|
||||
.zip_longest(other.prenormalized_prefix_elements(db, None))
|
||||
.all(|pair| match pair {
|
||||
EitherOrBoth::Both(self_ty, other_ty) => {
|
||||
self_ty.is_gradual_equivalent_to(db, other_ty)
|
||||
}
|
||||
EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false,
|
||||
})
|
||||
&& (self.prenormalized_suffix_elements(db, None))
|
||||
.zip_longest(other.prenormalized_suffix_elements(db, None))
|
||||
.all(|pair| match pair {
|
||||
EitherOrBoth::Both(self_ty, other_ty) => {
|
||||
self_ty.is_gradual_equivalent_to(db, other_ty)
|
||||
}
|
||||
EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
|
||||
self.variable.is_fully_static(db)
|
||||
&& self.prefix_elements().all(|ty| ty.is_fully_static(db))
|
||||
&& self.suffix_elements().all(|ty| ty.is_fully_static(db))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> PyIndex<'db> for &VariableLengthTupleSpec<'db> {
|
||||
|
@ -873,19 +828,6 @@ impl<'db> TupleSpec<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &TupleSpec<'db>) -> bool {
|
||||
match (self, other) {
|
||||
(TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => {
|
||||
self_tuple.is_gradual_equivalent_to(db, other_tuple)
|
||||
}
|
||||
(TupleSpec::Variable(self_tuple), TupleSpec::Variable(other_tuple)) => {
|
||||
self_tuple.is_gradual_equivalent_to(db, other_tuple)
|
||||
}
|
||||
(TupleSpec::Fixed(_), TupleSpec::Variable(_))
|
||||
| (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_disjoint_from(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
// Two tuples with an incompatible number of required elements must always be disjoint.
|
||||
let (self_min, self_max) = self.size_hint();
|
||||
|
@ -950,13 +892,6 @@ impl<'db> TupleSpec<'db> {
|
|||
false
|
||||
}
|
||||
|
||||
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
TupleSpec::Fixed(tuple) => tuple.is_fully_static(db),
|
||||
TupleSpec::Variable(tuple) => tuple.is_fully_static(db),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_single_valued(&self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
TupleSpec::Fixed(tuple) => tuple.is_single_valued(db),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue