[ty] eliminate is_fully_static (#18799)

## Summary

Having a recursive type method to check whether a type is fully static
is inefficient, unnecessary, and makes us overly strict about subtyping
relations.

It's inefficient because we end up re-walking the same types many times
to check for fully-static-ness.

It's unnecessary because we can check relations involving the dynamic
type appropriately, depending whether the relation is subtyping or
assignability.

We use the subtyping relation to simplify unions and intersections. We
can usefully consider that `S <: T` for gradual types also, as long as
it remains true that `S | T` is equivalent to `T` and `S & T` is
equivalent to `S`.

One conservative definition (implemented here) that satisfies this
requirement is that we consider `S <: T` if, for every possible pair of
materializations `S'` and `T'`, `S' <: T'`. Or put differently the top
materialization of `S` (`S+` -- the union of all possible
materializations of `S`) is a subtype of the bottom materialization of
`T` (`T-` -- the intersection of all possible materializations of `T`).
In the most basic cases we can usefully say that `Any <: object` and
that `Never <: Any`, and we can handle more complex cases inductively
from there.

This definition of subtyping for gradual subtypes is not reflexive
(`Any` is not a subtype of `Any`).

As a corollary, we also remove `is_gradual_equivalent_to` --
`is_equivalent_to` now has the meaning that `is_gradual_equivalent_to`
used to have. If necessary, we could restore an
`is_fully_static_equivalent_to` or similar (which would not do an
`is_fully_static` pre-check of the types, but would instead pass a
relation-kind enum down through a recursive equivalence check, similar
to `has_relation_to`), but so far this doesn't appear to be necessary.

Credit to @JelleZijlstra for the observation that `is_fully_static` is
unnecessary and overly restrictive on subtyping.

There is another possible definition of gradual subtyping: instead of
requiring that `S+ <: T-`, we could instead require that `S+ <: T+` and
`S- <: T-`. In other words, instead of requiring all materializations of
`S` to be a subtype of every materialization of `T`, we just require
that every materialization of `S` be a subtype of _some_ materialization
of `T`, and that every materialization of `T` be a supertype of some
materialization of `S`. This definition also preserves the core
invariant that `S <: T` implies that `S | T = T` and `S & T = S`, and it
restores reflexivity: under this definition, `Any` is a subtype of
`Any`, and for any equivalent types `S` and `T`, `S <: T` and `T <: S`.
But unfortunately, this definition breaks transitivity of subtyping,
because nominal subclasses in Python use assignability ("consistent
subtyping") to define acceptable overrides. This means that we may have
a class `A` with `def method(self) -> Any` and a subtype `B(A)` with
`def method(self) -> int`, since `int` is assignable to `Any`. This
means that if we have a protocol `P` with `def method(self) -> Any`, we
would have `B <: A` (from nominal subtyping) and `A <: P` (`Any` is a
subtype of `Any`), but not `B <: P` (`int` is not a subtype of `Any`).
Breaking transitivity of subtyping is not tenable, so we don't use this
definition of subtyping.

## Test Plan

Existing tests (modified in some cases to account for updated
semantics.)

Stable property tests pass at a million iterations:
`QUICKCHECK_TESTS=1000000 cargo test -p ty_python_semantic -- --ignored
types::property_tests::stable`

### Changes to property test type generation

Since we no longer have a method of categorizing built types as
fully-static or not-fully-static, I had to add a previously-discussed
feature to the property tests so that some tests can build types that
are known by construction to be fully static, because there are still
properties that only apply to fully-static types (for example,
reflexiveness of subtyping.)

## Changes to handling of `*args, **kwargs` signatures

This PR "discovered" that, once we allow non-fully-static types to
participate in subtyping under the above definitions, `(*args: Any,
**kwargs: Any) -> Any` is now a subtype of `() -> object`. This is true,
if we take a literal interpretation of the former signature: all
materializations of the parameters `*args: Any, **kwargs: Any` can
accept zero arguments, making the former signature a subtype of the
latter. But the spec actually says that `*args: Any, **kwargs: Any`
should be interpreted as equivalent to `...`, and that makes a
difference here: `(...) -> Any` is not a subtype of `() -> object`,
because (unlike a literal reading of `(*args: Any, **kwargs: Any)`),
`...` can materialize to _any_ signature, including a signature with
required positional arguments.

This matters for this PR because it makes the "any two types are both
assignable to their union" property test fail if we don't implement the
equivalence to `...`. Because `FunctionType.__call__` has the signature
`(*args: Any, **kwargs: Any) -> Any`, and if we take that at face value
it's a subtype of `() -> object`, making `FunctionType` a subtype of `()
-> object)` -- but then a function with a required argument is also a
subtype of `FunctionType`, but not a subtype of `() -> object`. So I
went ahead and implemented the equivalence to `...` in this PR.

## Ecosystem analysis

* Most of the ecosystem report are cases of improved union/intersection
simplification. For example, we can now simplify a union like `bool |
(bool & Unknown) | Unknown` to simply `bool | Unknown`, because we can
now observe that every possible materialization of `bool & Unknown` is
still a subtype of `bool` (whereas before we would set aside `bool &
Unknown` as a not-fully-static type.) This is clearly an improvement.
* The `possibly-unresolved-reference` errors in sockeye, pymongo,
ignite, scrapy and others are true positives for conditional imports
that were formerly silenced by bogus conflicting-declarations (which we
currently don't issue a diagnostic for), because we considered two
different declarations of `Unknown` to be conflicting (we used
`is_equivalent_to` not `is_gradual_equivalent_to`). In this PR that
distinction disappears and all equivalence is gradual, so a declaration
of `Unknown` no longer conflicts with a declaration of `Unknown`, which
then results in us surfacing the possibly-unbound error.
* We will now issue "redundant cast" for casting from a typevar with a
gradual bound to the same typevar (the hydra-zen diagnostic). This seems
like an improvement.
* The new diagnostics in bandersnatch are interesting. For some reason
primer in CI seems to be checking bandersnatch on Python 3.10 (not yet
sure why; this doesn't happen when I run it locally). But bandersnatch
uses `enum.StrEnum`, which doesn't exist on 3.10. That makes the `class
SimpleDigest(StrEnum)` a class that inherits from `Unknown` (and
bypasses our current TODO handling for accessing attributes on enum
classes, since we don't recognize it as an enum class at all). This PR
improves our understanding of assignability to classes that inherit from
`Any` / `Unknown`, and we now recognize that a string literal is not
assignable to a class inheriting `Any` or `Unknown`.
This commit is contained in:
Carl Meyer 2025-06-24 18:02:05 -07:00 committed by GitHub
parent eee5a5a3d6
commit 62975b3ab2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 957 additions and 1633 deletions

View file

@ -313,7 +313,7 @@ len([], 1)
### Type API predicates
```py
from ty_extensions import is_subtype_of, is_fully_static
from ty_extensions import is_subtype_of
# error: [missing-argument]
is_subtype_of()
@ -326,10 +326,4 @@ is_subtype_of(int, int, int)
# error: [too-many-positional-arguments]
is_subtype_of(int, int, int, int)
# error: [missing-argument]
is_fully_static()
# error: [too-many-positional-arguments]
is_fully_static(int, int)
```

View file

@ -144,8 +144,7 @@ from typing import Any
def _(a: Any, tuple_of_any: tuple[Any]):
reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"]
# TODO: Ideally, this would just be `def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int`
# revealed: (def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int) | Literal["default"]
# revealed: def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default"))
```

View file

@ -203,15 +203,15 @@ def _(
## Cannot use an argument as both a value and a type form
```py
from ty_extensions import is_fully_static
from ty_extensions import is_singleton
def _(flag: bool):
if flag:
f = repr
else:
f = is_fully_static
f = is_singleton
# error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call"
reveal_type(f(int)) # revealed: str | Literal[True]
reveal_type(f(int)) # revealed: str | Literal[False]
```
## Size limit on unions of literals

View file

@ -90,11 +90,12 @@ from typing import Any
@dataclass
class C:
w: type[Any]
x: Any
y: int | Any
z: tuple[int, Any]
reveal_type(C.__init__) # revealed: (self: C, x: Any, y: int | Any, z: tuple[int, Any]) -> None
reveal_type(C.__init__) # revealed: (self: C, w: type[Any], x: Any, y: int | Any, z: tuple[int, Any]) -> None
```
Variables without annotations are ignored:

View file

@ -22,7 +22,7 @@ Types that "produce" data on demand are covariant in their typevar. If you expec
get from the sequence is a valid `int`.
```py
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any, Generic, TypeVar
class A: ...
@ -53,11 +53,13 @@ static_assert(is_assignable_to(D[Any], C[A]))
static_assert(is_assignable_to(D[Any], C[B]))
static_assert(is_subtype_of(C[B], C[A]))
static_assert(is_subtype_of(C[A], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(not is_subtype_of(C[Any], C[Any]))
static_assert(is_subtype_of(D[B], C[A]))
static_assert(not is_subtype_of(D[A], C[B]))
@ -84,27 +86,11 @@ static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(is_equivalent_to(C[Any], C[Any]))
static_assert(is_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
static_assert(not is_equivalent_to(D[Any], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
```
## Contravariance
@ -117,7 +103,7 @@ Types that "consume" data are contravariant in their typevar. If you expect a co
that you pass into the consumer is a valid `int`.
```py
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any, Generic, TypeVar
class A: ...
@ -178,27 +164,11 @@ static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(is_equivalent_to(C[Any], C[Any]))
static_assert(is_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
static_assert(not is_equivalent_to(D[Any], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
```
## Invariance
@ -224,7 +194,7 @@ In the end, if you expect a mutable list, you must always be given a list of exa
since we can't know in advance which of the allowed methods you'll want to use.
```py
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any, Generic, TypeVar
class A: ...
@ -287,27 +257,11 @@ static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(is_equivalent_to(C[Any], C[Any]))
static_assert(is_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
static_assert(not is_equivalent_to(D[Any], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
```
## Bivariance

View file

@ -113,33 +113,6 @@ class C[T]:
reveal_type(x) # revealed: T
```
## Fully static typevars
We consider a typevar to be fully static unless it has a non-fully-static bound or constraint. This
is true even though a fully static typevar might be specialized to a gradual form like `Any`. (This
is similar to how you can assign an expression whose type is not fully static to a target whose type
is.)
```py
from ty_extensions import is_fully_static, static_assert
from typing import Any
def unbounded_unconstrained[T](t: T) -> None:
static_assert(is_fully_static(T))
def bounded[T: int](t: T) -> None:
static_assert(is_fully_static(T))
def bounded_by_gradual[T: Any](t: T) -> None:
static_assert(not is_fully_static(T))
def constrained[T: (int, str)](t: T) -> None:
static_assert(is_fully_static(T))
def constrained_by_gradual[T: (int, Any)](t: T) -> None:
static_assert(not is_fully_static(T))
```
## Subtyping and assignability
(Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static
@ -372,14 +345,14 @@ def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
## Equivalence
A fully static `TypeVar` is always equivalent to itself, but never to another `TypeVar`, since there
is no guarantee that they will be specialized to the same type. (This is true even if both typevars
are bounded by the same final class, since you can specialize the typevars to `Never` in addition to
A `TypeVar` is always equivalent to itself, but never to another `TypeVar`, since there is no
guarantee that they will be specialized to the same type. (This is true even if both typevars are
bounded by the same final class, since you can specialize the typevars to `Never` in addition to
that final class.)
```py
from typing import final
from ty_extensions import is_equivalent_to, static_assert, is_gradual_equivalent_to
from ty_extensions import is_equivalent_to, static_assert
@final
class FinalClass: ...
@ -395,28 +368,16 @@ def f[A, B, C: FinalClass, D: FinalClass, E: (FinalClass, SecondFinalClass), F:
static_assert(is_equivalent_to(E, E))
static_assert(is_equivalent_to(F, F))
static_assert(is_gradual_equivalent_to(A, A))
static_assert(is_gradual_equivalent_to(B, B))
static_assert(is_gradual_equivalent_to(C, C))
static_assert(is_gradual_equivalent_to(D, D))
static_assert(is_gradual_equivalent_to(E, E))
static_assert(is_gradual_equivalent_to(F, F))
static_assert(not is_equivalent_to(A, B))
static_assert(not is_equivalent_to(C, D))
static_assert(not is_equivalent_to(E, F))
static_assert(not is_gradual_equivalent_to(A, B))
static_assert(not is_gradual_equivalent_to(C, D))
static_assert(not is_gradual_equivalent_to(E, F))
```
TypeVars which have non-fully-static bounds or constraints do not participate in equivalence
relations, but do participate in gradual equivalence relations.
TypeVars which have non-fully-static bounds or constraints are also self-equivalent.
```py
from typing import final, Any
from ty_extensions import is_equivalent_to, static_assert, is_gradual_equivalent_to
from ty_extensions import is_equivalent_to, static_assert
# fmt: off
@ -426,15 +387,10 @@ def f[
C: (tuple[Any], tuple[Any, Any]),
D: (tuple[Any], tuple[Any, Any])
]():
static_assert(not is_equivalent_to(A, A))
static_assert(not is_equivalent_to(B, B))
static_assert(not is_equivalent_to(C, C))
static_assert(not is_equivalent_to(D, D))
static_assert(is_gradual_equivalent_to(A, A))
static_assert(is_gradual_equivalent_to(B, B))
static_assert(is_gradual_equivalent_to(C, C))
static_assert(is_gradual_equivalent_to(D, D))
static_assert(is_equivalent_to(A, A))
static_assert(is_equivalent_to(B, B))
static_assert(is_equivalent_to(C, C))
static_assert(is_equivalent_to(D, D))
# fmt: on
```

View file

@ -27,7 +27,7 @@ Types that "produce" data on demand are covariant in their typevar. If you expec
get from the sequence is a valid `int`.
```py
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
@ -94,27 +94,11 @@ static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(is_equivalent_to(C[Any], C[Any]))
static_assert(is_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
static_assert(not is_equivalent_to(D[Any], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
```
## Contravariance
@ -127,7 +111,7 @@ Types that "consume" data are contravariant in their typevar. If you expect a co
that you pass into the consumer is a valid `int`.
```py
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
@ -193,27 +177,11 @@ static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(is_equivalent_to(C[Any], C[Any]))
static_assert(is_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
static_assert(not is_equivalent_to(D[Any], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
```
## Invariance
@ -239,7 +207,7 @@ In the end, if you expect a mutable list, you must always be given a list of exa
since we can't know in advance which of the allowed methods you'll want to use.
```py
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
@ -299,27 +267,11 @@ static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
static_assert(is_equivalent_to(C[Any], C[Any]))
static_assert(is_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
static_assert(not is_equivalent_to(D[Any], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
```
## Bivariance
@ -333,7 +285,7 @@ at all. (If it did, it would have to be covariant, contravariant, or invariant,
the typevar was used.)
```py
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
@ -359,6 +311,7 @@ static_assert(is_assignable_to(C[Any], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(D[B], C[A]))
static_assert(is_subtype_of(C[A], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(D[A], C[B]))
@ -377,6 +330,7 @@ static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(not is_subtype_of(C[Any], C[Any]))
# TODO: no error
# error: [static-assert-error]
@ -397,10 +351,18 @@ static_assert(is_equivalent_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[A], C[B]))
static_assert(not is_equivalent_to(C[A], C[Any]))
static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[A], C[Any]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[B], C[Any]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[Any], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[Any], C[B]))
static_assert(not is_equivalent_to(D[A], C[A]))
static_assert(not is_equivalent_to(D[B], C[B]))
@ -411,39 +373,11 @@ static_assert(not is_equivalent_to(D[B], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[A]))
static_assert(not is_equivalent_to(D[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[A], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[A], C[Any]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[B], C[Any]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[Any], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[Any], C[B]))
static_assert(is_equivalent_to(C[Any], C[Any]))
static_assert(is_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
static_assert(not is_equivalent_to(D[Any], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
```
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance

View file

@ -717,6 +717,18 @@ def never(
reveal_type(d) # revealed: Never
```
Regression tests for complex nested simplifications:
```py
from typing_extensions import Any, assert_type
def _(x: Intersection[bool, Not[Intersection[Any, Not[AlwaysTruthy], Not[AlwaysFalsy]]]]):
assert_type(x, bool)
def _(x: Intersection[bool, Any] | Literal[True] | Literal[False]):
assert_type(x, bool)
```
## Simplification of `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy`
Similarly, intersections between `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy` can be

View file

@ -1346,107 +1346,6 @@ def h(obj: InstanceAttrBool):
reveal_type(bool(obj)) # revealed: bool
```
## Fully static protocols; gradual protocols
A protocol is only fully static if all of its members are fully static:
```py
from typing import Protocol, Any
from ty_extensions import is_fully_static, static_assert
class FullyStatic(Protocol):
x: int
class NotFullyStatic(Protocol):
x: Any
static_assert(is_fully_static(FullyStatic))
static_assert(not is_fully_static(NotFullyStatic))
```
Non-fully-static protocols do not participate in subtyping or equivalence, only assignability and
gradual equivalence:
```py
from ty_extensions import is_subtype_of, is_assignable_to, is_equivalent_to, is_gradual_equivalent_to
class NominalWithX:
x: int = 42
static_assert(is_assignable_to(NominalWithX, FullyStatic))
static_assert(is_assignable_to(NominalWithX, NotFullyStatic))
static_assert(not is_subtype_of(FullyStatic, NotFullyStatic))
static_assert(is_assignable_to(FullyStatic, NotFullyStatic))
static_assert(not is_subtype_of(NotFullyStatic, FullyStatic))
static_assert(is_assignable_to(NotFullyStatic, FullyStatic))
static_assert(not is_subtype_of(NominalWithX, NotFullyStatic))
static_assert(is_assignable_to(NominalWithX, NotFullyStatic))
static_assert(is_subtype_of(NominalWithX, FullyStatic))
static_assert(is_equivalent_to(FullyStatic, FullyStatic))
static_assert(not is_equivalent_to(NotFullyStatic, NotFullyStatic))
static_assert(is_gradual_equivalent_to(FullyStatic, FullyStatic))
static_assert(is_gradual_equivalent_to(NotFullyStatic, NotFullyStatic))
class AlsoNotFullyStatic(Protocol):
x: Any
static_assert(not is_equivalent_to(NotFullyStatic, AlsoNotFullyStatic))
static_assert(is_gradual_equivalent_to(NotFullyStatic, AlsoNotFullyStatic))
```
Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to
the nominal type `object` (as described above):
```py
class Empty(Protocol): ...
static_assert(is_fully_static(Empty))
```
A method member is only considered fully static if all its parameter annotations and its return
annotation are fully static:
```py
class FullyStaticMethodMember(Protocol):
def method(self, x: int) -> str: ...
class DynamicParameter(Protocol):
def method(self, x: Any) -> str: ...
class DynamicReturn(Protocol):
def method(self, x: int) -> Any: ...
static_assert(is_fully_static(FullyStaticMethodMember))
# TODO: these should pass
static_assert(not is_fully_static(DynamicParameter)) # error: [static-assert-error]
static_assert(not is_fully_static(DynamicReturn)) # error: [static-assert-error]
```
The [typing spec][spec_protocol_members] states:
> If any parameters of a protocol method are not annotated, then their types are assumed to be `Any`
Thus, a partially unannotated method member can also not be considered to be fully static:
```py
class NoParameterAnnotation(Protocol):
def method(self, x) -> str: ...
class NoReturnAnnotation(Protocol):
def method(self, x: int): ...
# TODO: these should pass
static_assert(not is_fully_static(NoParameterAnnotation)) # error: [static-assert-error]
static_assert(not is_fully_static(NoReturnAnnotation)) # error: [static-assert-error]
```
## Callable protocols
An instance of a protocol type is callable if the protocol defines a `__call__` method:
@ -1560,7 +1459,7 @@ def two(some_list: list, some_tuple: tuple[int, str], some_sized: Sized):
from __future__ import annotations
from typing import Protocol, Any
from ty_extensions import is_fully_static, static_assert, is_assignable_to, is_subtype_of, is_equivalent_to
from ty_extensions import static_assert, is_assignable_to, is_subtype_of, is_equivalent_to
class RecursiveFullyStatic(Protocol):
parent: RecursiveFullyStatic
@ -1570,11 +1469,9 @@ class RecursiveNonFullyStatic(Protocol):
parent: RecursiveNonFullyStatic
x: Any
static_assert(is_fully_static(RecursiveFullyStatic))
static_assert(not is_fully_static(RecursiveNonFullyStatic))
static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic))
static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic))
# TODO: these should pass, once we take into account types of members
static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic)) # error: [static-assert-error]
static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic)) # error: [static-assert-error]
static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveNonFullyStatic))
static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic))
@ -1589,8 +1486,6 @@ static_assert(is_equivalent_to(AlsoRecursiveFullyStatic, RecursiveFullyStatic))
class RecursiveOptionalParent(Protocol):
parent: RecursiveOptionalParent | None
static_assert(is_fully_static(RecursiveOptionalParent))
static_assert(is_assignable_to(RecursiveOptionalParent, RecursiveOptionalParent))
static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveOptionalParent))
@ -1635,7 +1530,7 @@ python-version = "3.12"
from __future__ import annotations
from typing import Protocol, Callable
from ty_extensions import Intersection, Not, is_fully_static, is_assignable_to, is_equivalent_to, static_assert
from ty_extensions import Intersection, Not, is_assignable_to, is_equivalent_to, static_assert
class C: ...
@ -1663,7 +1558,6 @@ class Recursive(Protocol):
nested: Recursive | Callable[[Recursive | Recursive, tuple[Recursive, Recursive]], Recursive | Recursive]
static_assert(is_fully_static(Recursive))
static_assert(is_equivalent_to(Recursive, Recursive))
static_assert(is_assignable_to(Recursive, Recursive))

View file

@ -40,7 +40,7 @@ error[parameter-already-assigned]: Multiple values provided for parameter `name`
| ^^^^^^^^^^
|
info: Union variant `def f1(name: str) -> int` is incompatible with this call site
info: Attempted to call union type `(def f1(name: str) -> int) | (def any(*args, **kwargs) -> int)`
info: Attempted to call union type `(def f1(name: str) -> int) | (def any(...) -> int)`
info: rule `parameter-already-assigned` is enabled by default
```
@ -55,7 +55,7 @@ error[unknown-argument]: Argument `unknown` does not match any known parameter o
| ^^^^^^^^^^^^^^
|
info: Union variant `def f1(name: str) -> int` is incompatible with this call site
info: Attempted to call union type `(def f1(name: str) -> int) | (def any(*args, **kwargs) -> int)`
info: Attempted to call union type `(def f1(name: str) -> int) | (def any(...) -> int)`
info: rule `unknown-argument` is enabled by default
```

View file

@ -91,13 +91,11 @@ The `Unknown` type is a special type that we use to represent actually unknown t
annotation), as opposed to `Any` which represents an explicitly unknown type.
```py
from ty_extensions import Unknown, static_assert, is_assignable_to, is_fully_static
from ty_extensions import Unknown, static_assert, is_assignable_to
static_assert(is_assignable_to(Unknown, int))
static_assert(is_assignable_to(int, Unknown))
static_assert(not is_fully_static(Unknown))
def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None:
reveal_type(x) # revealed: Unknown
reveal_type(y) # revealed: tuple[str, Unknown]
@ -333,19 +331,6 @@ static_assert(is_disjoint_from(None, int))
static_assert(not is_disjoint_from(Literal[2] | str, int))
```
### Fully static types
```py
from ty_extensions import is_fully_static, static_assert
from typing import Any
static_assert(is_fully_static(int | str))
static_assert(is_fully_static(type[int]))
static_assert(not is_fully_static(int | Any))
static_assert(not is_fully_static(type[Any]))
```
### Singleton types
```py

View file

@ -2,24 +2,13 @@
## Introduction
The type `Any` is the dynamic type in Python's gradual type system. It represents an unknown
fully-static type, which means that it represents an *unknown* set of runtime values.
```py
from ty_extensions import static_assert, is_fully_static
from typing import Any
```
`Any` is a dynamic type:
```py
static_assert(not is_fully_static(Any))
```
The type `Any` is the dynamic type in Python's gradual type system. It represents an unknown static
type, which means that it represents an *unknown* set of runtime values.
## Every type is assignable to `Any`, and `Any` is assignable to every type
```py
from ty_extensions import static_assert, is_fully_static, is_assignable_to
from ty_extensions import static_assert, is_assignable_to
from typing_extensions import Never, Any
class C: ...

View file

@ -113,8 +113,8 @@ static_assert(is_equivalent_to(Not[Intersection[P, Q]], Not[P] | Not[Q]))
The two gradual types are equivalent:
```py
from ty_extensions import static_assert, is_gradual_equivalent_to, Not
from ty_extensions import static_assert, is_equivalent_to, Not
from typing import Any
static_assert(is_gradual_equivalent_to(Not[Any], Any))
static_assert(is_equivalent_to(Not[Any], Any))
```

View file

@ -36,8 +36,7 @@ static_assert(not is_assignable_to(Child1, Child2))
### Gradual types
Gradual types do not participate in subtyping, but can still be assignable to other types (and
static types can be assignable to gradual types):
The dynamic type is assignable to or from any type.
```py
from ty_extensions import static_assert, is_assignable_to, Unknown
@ -47,13 +46,6 @@ static_assert(is_assignable_to(Unknown, Literal[1]))
static_assert(is_assignable_to(Any, Literal[1]))
static_assert(is_assignable_to(Literal[1], Unknown))
static_assert(is_assignable_to(Literal[1], Any))
class SubtypeOfAny(Any): ...
static_assert(is_assignable_to(SubtypeOfAny, Any))
static_assert(is_assignable_to(SubtypeOfAny, int))
static_assert(is_assignable_to(Any, SubtypeOfAny))
static_assert(not is_assignable_to(int, SubtypeOfAny))
```
## Literal types
@ -239,7 +231,9 @@ from ty_extensions import is_assignable_to, static_assert
static_assert(not is_assignable_to(type[Any], None))
```
## Class-literals that inherit from `Any`
## Inheriting `Any`
### Class-literal types
Class-literal types that inherit from `Any` are assignable to any type `T` where `T` is assignable
to `type`:
@ -267,6 +261,39 @@ def test(x: Any):
This is because the `Any` element in the MRO could materialize to any subtype of `type`.
### Nominal instance and subclass-of types
Instances of classes that inherit `Any` are assignable to any non-final type.
```py
from ty_extensions import is_assignable_to, static_assert
from typing_extensions import Any, final
class InheritsAny(Any):
pass
class Arbitrary:
pass
@final
class FinalClass:
pass
static_assert(is_assignable_to(InheritsAny, Arbitrary))
static_assert(is_assignable_to(InheritsAny, Any))
static_assert(is_assignable_to(InheritsAny, object))
static_assert(not is_assignable_to(InheritsAny, FinalClass))
```
Similar for subclass-of types:
```py
static_assert(is_assignable_to(type[Any], type[Any]))
static_assert(is_assignable_to(type[object], type[Any]))
static_assert(is_assignable_to(type[Any], type[Arbitrary]))
static_assert(is_assignable_to(type[Any], type[object]))
```
## Heterogeneous tuple types
```py

View file

@ -193,8 +193,8 @@ static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3]))
## Intersections
```py
from typing_extensions import Literal, final, Any
from ty_extensions import Intersection, is_disjoint_from, static_assert, Not
from typing_extensions import Literal, final, Any, LiteralString
from ty_extensions import Intersection, is_disjoint_from, static_assert, Not, AlwaysFalsy
@final
class P: ...
@ -249,6 +249,9 @@ static_assert(not is_disjoint_from(Intersection[Any, Not[Y]], Intersection[Any,
static_assert(is_disjoint_from(Intersection[int, Any], Not[int]))
static_assert(is_disjoint_from(Not[int], Intersection[int, Any]))
# TODO https://github.com/astral-sh/ty/issues/216
static_assert(is_disjoint_from(AlwaysFalsy, LiteralString & ~Literal[""])) # error: [static-assert-error]
```
## Special types

View file

@ -1,41 +1,76 @@
# Equivalence relation
`is_equivalent_to` implements [the equivalence relation] for fully static types.
`is_equivalent_to` implements [the equivalence relation] on types.
Two types `A` and `B` are equivalent iff `A` is a subtype of `B` and `B` is a subtype of `A`.
For fully static types, two types `A` and `B` are equivalent iff `A` is a subtype of `B` and `B` is
a subtype of `A` (that is, the two types represent the same set of values).
Two gradual types `A` and `B` are equivalent if all [materializations] of `A` are also
materializations of `B`, and all materializations of `B` are also materializations of `A`.
## Basic
### Fully static
```py
from typing import Any
from typing_extensions import Literal
from ty_extensions import Unknown, is_equivalent_to, static_assert
from typing_extensions import Literal, LiteralString, Never
from ty_extensions import Unknown, is_equivalent_to, static_assert, TypeOf, AlwaysTruthy, AlwaysFalsy
static_assert(is_equivalent_to(Literal[1, 2], Literal[1, 2]))
static_assert(is_equivalent_to(type[object], type))
static_assert(not is_equivalent_to(Any, Any))
static_assert(not is_equivalent_to(Unknown, Unknown))
static_assert(not is_equivalent_to(Any, None))
static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 0]))
static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 2, 3]))
```
## Equivalence is commutative
```py
from typing_extensions import Literal
from ty_extensions import is_equivalent_to, static_assert
static_assert(is_equivalent_to(type, type[object]))
static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 0]))
static_assert(not is_equivalent_to(Literal[1, 0], Literal[1, 2]))
static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 2, 3]))
static_assert(not is_equivalent_to(Literal[1, 2, 3], Literal[1, 2]))
static_assert(is_equivalent_to(Never, Never))
static_assert(is_equivalent_to(AlwaysTruthy, AlwaysTruthy))
static_assert(is_equivalent_to(AlwaysFalsy, AlwaysFalsy))
static_assert(is_equivalent_to(LiteralString, LiteralString))
static_assert(is_equivalent_to(Literal[True], Literal[True]))
static_assert(is_equivalent_to(Literal[False], Literal[False]))
static_assert(is_equivalent_to(TypeOf[0:1:2], TypeOf[0:1:2]))
static_assert(is_equivalent_to(TypeOf[str], TypeOf[str]))
static_assert(is_equivalent_to(type, type[object]))
```
## Differently ordered intersections and unions are equivalent
### Gradual
```py
from ty_extensions import is_equivalent_to, static_assert, Intersection, Not
from typing import Any
from typing_extensions import Literal, LiteralString, Never
from ty_extensions import Unknown, is_equivalent_to, static_assert
static_assert(is_equivalent_to(Any, Any))
static_assert(is_equivalent_to(Unknown, Unknown))
static_assert(is_equivalent_to(Any, Unknown))
static_assert(not is_equivalent_to(Any, None))
static_assert(not is_equivalent_to(type, type[Any]))
static_assert(not is_equivalent_to(type[object], type[Any]))
```
## Unions and intersections
```py
from typing import Any
from ty_extensions import Intersection, Not, Unknown, is_equivalent_to, static_assert
static_assert(is_equivalent_to(str | int, str | int))
static_assert(is_equivalent_to(str | int | Any, str | int | Unknown))
static_assert(is_equivalent_to(str | int, int | str))
static_assert(is_equivalent_to(Intersection[str, int, Not[bytes], Not[None]], Intersection[int, str, Not[None], Not[bytes]]))
static_assert(is_equivalent_to(Intersection[str | int, Not[type[Any]]], Intersection[int | str, Not[type[Unknown]]]))
static_assert(not is_equivalent_to(str | int, int | str | bytes))
static_assert(not is_equivalent_to(str | int | bytes, int | str | dict))
static_assert(is_equivalent_to(Unknown, Unknown | Any))
static_assert(is_equivalent_to(Unknown, Intersection[Unknown, Any]))
class P: ...
class Q: ...
@ -66,6 +101,18 @@ static_assert(is_equivalent_to(Intersection[Q, R, Not[P]], Intersection[Not[P],
static_assert(is_equivalent_to(Intersection[Q | R, Not[P | S]], Intersection[Not[S | P], R | Q]))
```
## Tuples
```py
from ty_extensions import Unknown, is_equivalent_to, static_assert
from typing import Any
static_assert(is_equivalent_to(tuple[str, Any], tuple[str, Unknown]))
static_assert(not is_equivalent_to(tuple[str, int], tuple[str, int, bytes]))
static_assert(not is_equivalent_to(tuple[str, int], tuple[int, str]))
```
## Tuples containing equivalent but differently ordered unions/intersections are equivalent
```py
@ -193,21 +240,14 @@ def f2(a: int, b: int) -> None: ...
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
```
When either of the callable types uses a gradual form for the parameters:
```py
static_assert(not is_equivalent_to(Callable[..., None], Callable[[int], None]))
static_assert(not is_equivalent_to(Callable[[int], None], Callable[..., None]))
```
When the return types are not equivalent or absent in one or both of the callable types:
When the return types are not equivalent in one or both of the callable types:
```py
def f3(): ...
def f4() -> None: ...
static_assert(not is_equivalent_to(Callable[[], int], Callable[[], None]))
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3]))
static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3]))
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
static_assert(not is_equivalent_to(CallableTypeOf[f4], CallableTypeOf[f3]))
```
@ -247,7 +287,7 @@ def f11(a) -> None: ...
static_assert(not is_equivalent_to(CallableTypeOf[f9], CallableTypeOf[f10]))
static_assert(not is_equivalent_to(CallableTypeOf[f10], CallableTypeOf[f11]))
static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f10]))
static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f11]))
static_assert(is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f11]))
```
When the default value for a parameter is present only in one of the callable type:
@ -334,10 +374,9 @@ static_assert(is_equivalent_to(CallableTypeOf[pg], CallableTypeOf[cpg]))
static_assert(is_equivalent_to(CallableTypeOf[cpg], CallableTypeOf[pg]))
```
## Function-literal types and bound-method types
### Function-literal types and bound-method types
Function-literal types and bound-method types are always considered self-equivalent, even if they
have unannotated parameters, or parameters with not-fully-static annotations.
Function-literal types and bound-method types are always considered self-equivalent.
```toml
[environment]
@ -360,4 +399,94 @@ type X = TypeOf[A.method]
static_assert(is_equivalent_to(X, X))
```
### Non-fully-static callable types
The examples provided below are only a subset of the possible cases and only include the ones with
gradual types. The cases with fully static types and using different combinations of parameter kinds
are covered above.
```py
from ty_extensions import Unknown, CallableTypeOf, is_equivalent_to, static_assert
from typing import Any, Callable
static_assert(is_equivalent_to(Callable[..., int], Callable[..., int]))
static_assert(is_equivalent_to(Callable[..., Any], Callable[..., Unknown]))
static_assert(is_equivalent_to(Callable[[int, Any], None], Callable[[int, Unknown], None]))
static_assert(not is_equivalent_to(Callable[[int, Any], None], Callable[[Any, int], None]))
static_assert(not is_equivalent_to(Callable[[int, str], None], Callable[[int, str, bytes], None]))
static_assert(not is_equivalent_to(Callable[..., None], Callable[[], None]))
```
A function with no explicit return type should be gradual equivalent to a callable with a return
type of `Any`.
```py
def f1():
return
static_assert(is_equivalent_to(CallableTypeOf[f1], Callable[[], Any]))
```
And, similarly for parameters with no annotations.
```py
def f2(a, b, /) -> None:
return
static_assert(is_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None]))
```
Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs`
parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable
with `...` as the parameter type.
```py
def variadic_without_annotation(*args, **kwargs):
return
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
return
static_assert(is_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any]))
static_assert(is_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any]))
```
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
callable with `...` as the parameter type.
```py
def variadic_args(*args):
return
def variadic_kwargs(**kwargs):
return
static_assert(not is_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any]))
static_assert(not is_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any]))
```
Parameter names, default values, and it's kind should also be considered when checking for gradual
equivalence.
```py
def f1(a): ...
def f2(b): ...
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
def f3(a=1): ...
def f4(a=2): ...
def f5(a): ...
static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
static_assert(is_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3]))
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5]))
def f6(a, /): ...
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
```
[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent

View file

@ -1,133 +0,0 @@
# Fully-static types
A type is fully static iff it does not contain any gradual forms.
## Fully-static
```py
from typing_extensions import Literal, LiteralString, Never, Callable
from ty_extensions import Intersection, Not, TypeOf, is_fully_static, static_assert
static_assert(is_fully_static(Never))
static_assert(is_fully_static(None))
static_assert(is_fully_static(Literal[1]))
static_assert(is_fully_static(Literal[True]))
static_assert(is_fully_static(Literal["abc"]))
static_assert(is_fully_static(Literal[b"abc"]))
static_assert(is_fully_static(LiteralString))
static_assert(is_fully_static(str))
static_assert(is_fully_static(object))
static_assert(is_fully_static(type))
static_assert(is_fully_static(TypeOf[str]))
static_assert(is_fully_static(TypeOf[Literal]))
static_assert(is_fully_static(str | None))
static_assert(is_fully_static(Intersection[str, Not[LiteralString]]))
static_assert(is_fully_static(tuple[()]))
static_assert(is_fully_static(tuple[int, object]))
static_assert(is_fully_static(type[str]))
static_assert(is_fully_static(type[object]))
```
## Non-fully-static
```py
from typing_extensions import Any, Literal, LiteralString, Callable
from ty_extensions import Intersection, Not, TypeOf, Unknown, is_fully_static, static_assert
static_assert(not is_fully_static(Any))
static_assert(not is_fully_static(Unknown))
static_assert(not is_fully_static(Any | str))
static_assert(not is_fully_static(str | Unknown))
static_assert(not is_fully_static(Intersection[Any, Not[LiteralString]]))
static_assert(not is_fully_static(tuple[Any, ...]))
static_assert(not is_fully_static(tuple[int, Any]))
static_assert(not is_fully_static(type[Any]))
```
## Callable
```py
from typing_extensions import Callable, Any
from ty_extensions import Unknown, is_fully_static, static_assert
static_assert(is_fully_static(Callable[[], int]))
static_assert(is_fully_static(Callable[[int, str], int]))
static_assert(not is_fully_static(Callable[..., int]))
static_assert(not is_fully_static(Callable[[], Any]))
static_assert(not is_fully_static(Callable[[int, Unknown], int]))
```
The invalid forms of `Callable` annotation are never fully static because we represent them with the
`(...) -> Unknown` signature.
```py
static_assert(not is_fully_static(Callable))
# error: [invalid-type-form]
static_assert(not is_fully_static(Callable[int, int]))
```
Using function literals, we can check more variations of callable types as it allows us to define
parameters without annotations and no return type.
```py
from ty_extensions import CallableTypeOf, is_fully_static, static_assert
def f00() -> None: ...
def f01(a: int, b: str) -> None: ...
def f11(): ...
def f12(a, b): ...
def f13(a, b: int): ...
def f14(a, b: int) -> None: ...
def f15(a, b) -> None: ...
static_assert(is_fully_static(CallableTypeOf[f00]))
static_assert(is_fully_static(CallableTypeOf[f01]))
static_assert(not is_fully_static(CallableTypeOf[f11]))
static_assert(not is_fully_static(CallableTypeOf[f12]))
static_assert(not is_fully_static(CallableTypeOf[f13]))
static_assert(not is_fully_static(CallableTypeOf[f14]))
static_assert(not is_fully_static(CallableTypeOf[f15]))
```
## Overloads
`overloaded.pyi`:
```pyi
from typing import Any, overload
@overload
def gradual() -> None: ...
@overload
def gradual(a: Any) -> None: ...
@overload
def static() -> None: ...
@overload
def static(x: int) -> None: ...
@overload
def static(x: str) -> str: ...
```
```py
from ty_extensions import CallableTypeOf, TypeOf, is_fully_static, static_assert
from overloaded import gradual, static
static_assert(is_fully_static(TypeOf[gradual]))
static_assert(is_fully_static(TypeOf[static]))
static_assert(not is_fully_static(CallableTypeOf[gradual]))
static_assert(is_fully_static(CallableTypeOf[static]))
```

View file

@ -1,159 +0,0 @@
# Gradual equivalence relation
Two gradual types `A` and `B` are equivalent if all [materializations] of `A` are also
materializations of `B`, and all materializations of `B` are also materializations of `A`.
## Basic
```py
from typing import Any
from typing_extensions import Literal, LiteralString, Never
from ty_extensions import AlwaysFalsy, AlwaysTruthy, TypeOf, Unknown, is_gradual_equivalent_to, static_assert
static_assert(is_gradual_equivalent_to(Any, Any))
static_assert(is_gradual_equivalent_to(Unknown, Unknown))
static_assert(is_gradual_equivalent_to(Any, Unknown))
static_assert(is_gradual_equivalent_to(Never, Never))
static_assert(is_gradual_equivalent_to(AlwaysTruthy, AlwaysTruthy))
static_assert(is_gradual_equivalent_to(AlwaysFalsy, AlwaysFalsy))
static_assert(is_gradual_equivalent_to(LiteralString, LiteralString))
static_assert(is_gradual_equivalent_to(Literal[True], Literal[True]))
static_assert(is_gradual_equivalent_to(Literal[False], Literal[False]))
static_assert(is_gradual_equivalent_to(TypeOf[0:1:2], TypeOf[0:1:2]))
static_assert(is_gradual_equivalent_to(TypeOf[str], TypeOf[str]))
static_assert(is_gradual_equivalent_to(type, type[object]))
static_assert(not is_gradual_equivalent_to(type, type[Any]))
static_assert(not is_gradual_equivalent_to(type[object], type[Any]))
```
## Unions and intersections
```py
from typing import Any
from ty_extensions import Intersection, Not, Unknown, is_gradual_equivalent_to, static_assert
static_assert(is_gradual_equivalent_to(str | int, str | int))
static_assert(is_gradual_equivalent_to(str | int | Any, str | int | Unknown))
static_assert(is_gradual_equivalent_to(str | int, int | str))
static_assert(
is_gradual_equivalent_to(Intersection[str, int, Not[bytes], Not[None]], Intersection[int, str, Not[None], Not[bytes]])
)
static_assert(is_gradual_equivalent_to(Intersection[str | int, Not[type[Any]]], Intersection[int | str, Not[type[Unknown]]]))
static_assert(not is_gradual_equivalent_to(str | int, int | str | bytes))
static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict))
static_assert(is_gradual_equivalent_to(Unknown, Unknown | Any))
static_assert(is_gradual_equivalent_to(Unknown, Intersection[Unknown, Any]))
```
## Tuples
```py
from ty_extensions import Unknown, is_gradual_equivalent_to, static_assert
from typing import Any
static_assert(is_gradual_equivalent_to(tuple[str, Any], tuple[str, Unknown]))
static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[str, int, bytes]))
static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str]))
```
## Callable
The examples provided below are only a subset of the possible cases and only include the ones with
gradual types. The cases with fully static types and using different combinations of parameter kinds
are covered in the [equivalence tests](./is_equivalent_to.md#callable).
```py
from ty_extensions import Unknown, CallableTypeOf, is_gradual_equivalent_to, static_assert
from typing import Any, Callable
static_assert(is_gradual_equivalent_to(Callable[..., int], Callable[..., int]))
static_assert(is_gradual_equivalent_to(Callable[..., Any], Callable[..., Unknown]))
static_assert(is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[int, Unknown], None]))
static_assert(not is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[Any, int], None]))
static_assert(not is_gradual_equivalent_to(Callable[[int, str], None], Callable[[int, str, bytes], None]))
static_assert(not is_gradual_equivalent_to(Callable[..., None], Callable[[], None]))
```
A function with no explicit return type should be gradual equivalent to a callable with a return
type of `Any`.
```py
def f1():
return
static_assert(is_gradual_equivalent_to(CallableTypeOf[f1], Callable[[], Any]))
```
And, similarly for parameters with no annotations.
```py
def f2(a, b, /) -> None:
return
static_assert(is_gradual_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None]))
```
Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs`
parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable
with `...` as the parameter type.
```py
def variadic_without_annotation(*args, **kwargs):
return
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
return
static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any]))
static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any]))
```
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
callable with `...` as the parameter type.
```py
def variadic_args(*args):
return
def variadic_kwargs(**kwargs):
return
static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any]))
static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any]))
```
Parameter names, default values, and it's kind should also be considered when checking for gradual
equivalence.
```py
def f1(a): ...
def f2(b): ...
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
def f3(a=1): ...
def f4(a=2): ...
def f5(a): ...
static_assert(is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
static_assert(
is_gradual_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3])
)
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5]))
def f6(a, /): ...
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
```
TODO: Overloads
[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize

View file

@ -10,6 +10,10 @@ The `is_subtype_of(S, T)` relation below checks if type `S` is a subtype of type
A fully static type `S` is a subtype of another fully static type `T` iff the set of values
represented by `S` is a subset of the set of values represented by `T`.
A non fully static type `S` can also be safely considered a subtype of a non fully static type `T`,
if all possible materializations of `S` represent sets of values that are a subset of every possible
set of values represented by a materialization of `T`.
See the [typing documentation] for more information.
## Basic builtin types
@ -316,12 +320,13 @@ static_assert(
python-version = "3.12"
```
As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type.
However, the special-case behavior of assignability does not also apply to subtyping, since gradual
types to not participate in subtyping.
As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type, not
only in the type of its elements, but also in its length.
Its subtyping follows the general rule for subtyping of gradual types.
```py
from typing import Any
from typing import Any, Never
from ty_extensions import static_assert, is_subtype_of
static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, ...]))
@ -330,9 +335,11 @@ static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, Any]))
static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, ...]))
static_assert(not is_subtype_of(tuple[Any, ...], tuple[int]))
static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, int]))
static_assert(is_subtype_of(tuple[Any, ...], tuple[object, ...]))
static_assert(is_subtype_of(tuple[Never, ...], tuple[Any, ...]))
```
Subtyping also does not apply when `tuple[Any, ...]` is unpacked into a mixed tuple.
Same applies when `tuple[Any, ...]` is unpacked into a mixed tuple.
```py
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[Any, ...]]))
@ -363,9 +370,9 @@ static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, int]))
```
Subtyping does apply to unbounded homogeneous tuples of a fully static type. However, such tuples
are defined to be the _union_ of all tuple lengths, not the _gradual choice_ of them, so no
variable-length tuples are a subtyping of _any_ fixed-length tuple.
Unbounded homogeneous tuples of a non-Any type are defined to be the _union_ of all tuple lengths,
not the _gradual choice_ of them, so no variable-length tuples are a subtype of _any_ fixed-length
tuple.
```py
static_assert(not is_subtype_of(tuple[int, ...], tuple[Any, ...]))
@ -642,7 +649,7 @@ static_assert(not is_subtype_of(_SpecialForm, TypeOf[Literal]))
### Basic
```py
from typing import _SpecialForm
from typing import _SpecialForm, Any
from typing_extensions import Literal, assert_type
from ty_extensions import TypeOf, is_subtype_of, static_assert
@ -674,6 +681,8 @@ static_assert(not is_subtype_of(LiteralBool, bool))
static_assert(not is_subtype_of(type, type[bool]))
static_assert(not is_subtype_of(LiteralBool, type[Any]))
# int
static_assert(is_subtype_of(LiteralInt, LiteralInt))
@ -687,7 +696,9 @@ static_assert(not is_subtype_of(LiteralInt, int))
static_assert(not is_subtype_of(type, type[int]))
# LiteralString
static_assert(not is_subtype_of(LiteralInt, type[Any]))
# str
static_assert(is_subtype_of(LiteralStr, type[str]))
static_assert(is_subtype_of(LiteralStr, type))
@ -695,6 +706,8 @@ static_assert(is_subtype_of(LiteralStr, type[object]))
static_assert(not is_subtype_of(type[str], LiteralStr))
static_assert(not is_subtype_of(LiteralStr, type[Any]))
# custom metaclasses
type LiteralHasCustomMetaclass = TypeOf[HasCustomMetaclass]
@ -704,6 +717,18 @@ static_assert(is_subtype_of(Meta, type[object]))
static_assert(is_subtype_of(Meta, type))
static_assert(not is_subtype_of(Meta, type[type]))
static_assert(not is_subtype_of(Meta, type[Any]))
# generics
type LiteralListOfInt = TypeOf[list[int]]
assert_type(list[int], LiteralListOfInt)
static_assert(is_subtype_of(LiteralListOfInt, type))
static_assert(not is_subtype_of(LiteralListOfInt, type[Any]))
```
### Unions of class literals
@ -740,7 +765,9 @@ static_assert(is_subtype_of(LiteralBase | LiteralUnrelated, object))
## Non-fully-static types
`Any`, `Unknown`, `Todo` and derivatives thereof do not participate in subtyping.
A non-fully-static type can be considered a subtype of another type if all possible materializations
of the first type represent sets of values that are a subset of every possible set of values
represented by a materialization of the second type.
```py
from ty_extensions import Unknown, is_subtype_of, static_assert, Intersection
@ -749,25 +776,58 @@ from typing_extensions import Any
static_assert(not is_subtype_of(Any, Any))
static_assert(not is_subtype_of(Any, int))
static_assert(not is_subtype_of(int, Any))
static_assert(not is_subtype_of(Any, object))
static_assert(is_subtype_of(Any, object))
static_assert(not is_subtype_of(object, Any))
static_assert(not is_subtype_of(int, Any | int))
static_assert(not is_subtype_of(Intersection[Any, int], int))
static_assert(is_subtype_of(int, Any | int))
static_assert(is_subtype_of(Intersection[Any, int], int))
static_assert(not is_subtype_of(tuple[int, int], tuple[int, Any]))
```
# The same for `Unknown`:
The same for `Unknown`:
```py
static_assert(not is_subtype_of(Unknown, Unknown))
static_assert(not is_subtype_of(Unknown, int))
static_assert(not is_subtype_of(int, Unknown))
static_assert(not is_subtype_of(Unknown, object))
static_assert(is_subtype_of(Unknown, object))
static_assert(not is_subtype_of(object, Unknown))
static_assert(not is_subtype_of(int, Unknown | int))
static_assert(not is_subtype_of(Intersection[Unknown, int], int))
static_assert(is_subtype_of(int, Unknown | int))
static_assert(is_subtype_of(Intersection[Unknown, int], int))
static_assert(not is_subtype_of(tuple[int, int], tuple[int, Unknown]))
```
Instances of classes that inherit `Any` are not subtypes of some other `Arbitrary` class, because
the `Any` they inherit from could materialize to something (e.g. `object`) that is not a subclass of
that class.
Similarly, they are not subtypes of `Any`, because there are possible materializations of `Any` that
would not satisfy the subtype relation.
They are subtypes of `object`.
```py
class InheritsAny(Any):
pass
class Arbitrary:
pass
static_assert(not is_subtype_of(InheritsAny, Arbitrary))
static_assert(not is_subtype_of(InheritsAny, Any))
static_assert(is_subtype_of(InheritsAny, object))
```
Similar for subclass-of types:
```py
static_assert(not is_subtype_of(type[Any], type[Any]))
static_assert(not is_subtype_of(type[object], type[Any]))
static_assert(not is_subtype_of(type[Any], type[Arbitrary]))
static_assert(is_subtype_of(type[Any], type[object]))
```
## Callable
The general principle is that a callable type is a subtype of another if it's more flexible in what
@ -1389,10 +1449,45 @@ static_assert(is_subtype_of(TypeOf[C.foo], object))
static_assert(not is_subtype_of(object, TypeOf[C.foo]))
```
#### Gradual form
A callable type with `...` parameters can be considered a supertype of a callable type that accepts
any arguments of any type, but otherwise is not a subtype or supertype of any callable type.
```py
from typing import Callable, Never
from ty_extensions import CallableTypeOf, is_subtype_of, static_assert
def bottom(*args: object, **kwargs: object) -> Never:
raise Exception()
type BottomCallable = CallableTypeOf[bottom]
static_assert(is_subtype_of(BottomCallable, Callable[..., Never]))
static_assert(is_subtype_of(BottomCallable, Callable[..., int]))
static_assert(not is_subtype_of(Callable[[], object], Callable[..., object]))
static_assert(not is_subtype_of(Callable[..., object], Callable[[], object]))
```
According to the spec, `*args: Any, **kwargs: Any` is equivalent to `...`. This is a subtle but
important distinction. No materialization of the former signature (if taken literally) can have any
required arguments, but `...` can materialize to a signature with required arguments. The below test
would not pass if we didn't handle this special case.
```py
from typing import Callable, Any
from ty_extensions import is_subtype_of, static_assert, CallableTypeOf
def f(*args: Any, **kwargs: Any) -> Any: ...
static_assert(not is_subtype_of(CallableTypeOf[f], Callable[[], object]))
```
### Classes with `__call__`
```py
from typing import Callable
from typing import Callable, Any
from ty_extensions import TypeOf, is_subtype_of, static_assert, is_assignable_to
class A:
@ -1404,6 +1499,8 @@ a = A()
static_assert(is_subtype_of(A, Callable[[int], int]))
static_assert(not is_subtype_of(A, Callable[[], int]))
static_assert(not is_subtype_of(Callable[[int], int], A))
static_assert(not is_subtype_of(A, Callable[[Any], int]))
static_assert(not is_subtype_of(A, Callable[[int], Any]))
def f(fn: Callable[[int], int]) -> None: ...

View file

@ -326,29 +326,20 @@ from ty_extensions import (
Unknown,
bottom_materialization,
top_materialization,
is_fully_static,
static_assert,
is_subtype_of,
)
def bounded_by_gradual[T: Any](t: T) -> None:
static_assert(not is_fully_static(T))
# Top materialization of `T: Any` is `T: object`
static_assert(is_fully_static(TypeOf[top_materialization(T)]))
# Bottom materialization of `T: Any` is `T: Never`
static_assert(is_fully_static(TypeOf[bottom_materialization(T)]))
static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], Never))
def constrained_by_gradual[T: (int, Any)](t: T) -> None:
static_assert(not is_fully_static(T))
# Top materialization of `T: (int, Any)` is `T: (int, object)`
static_assert(is_fully_static(TypeOf[top_materialization(T)]))
# Bottom materialization of `T: (int, Any)` is `T: (int, Never)`
static_assert(is_fully_static(TypeOf[bottom_materialization(T)]))
static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], int))
```

View file

@ -175,8 +175,8 @@ python-version = "3.12"
```
```py
from typing import Literal
from ty_extensions import AlwaysTruthy, AlwaysFalsy
from typing import Literal, Union
from ty_extensions import AlwaysTruthy, AlwaysFalsy, is_equivalent_to, static_assert
type strings = Literal["foo", ""]
type ints = Literal[0, 1]
@ -213,4 +213,61 @@ def _(
reveal_type(bytes_or_falsy) # revealed: Literal[b"foo"] | AlwaysFalsy
reveal_type(falsy_or_bytes) # revealed: AlwaysFalsy | Literal[b"foo"]
type SA = Union[Literal[""], AlwaysTruthy, Literal["foo"]]
static_assert(is_equivalent_to(SA, Literal[""] | AlwaysTruthy))
type SD = Union[Literal[""], AlwaysTruthy, Literal["foo"], AlwaysFalsy, AlwaysTruthy, int]
static_assert(is_equivalent_to(SD, AlwaysTruthy | AlwaysFalsy | int))
type BA = Union[Literal[b""], AlwaysTruthy, Literal[b"foo"]]
static_assert(is_equivalent_to(BA, Literal[b""] | AlwaysTruthy))
type BD = Union[Literal[b""], AlwaysTruthy, Literal[b"foo"], AlwaysFalsy, AlwaysTruthy, int]
static_assert(is_equivalent_to(BD, AlwaysTruthy | AlwaysFalsy | int))
type IA = Union[Literal[0], AlwaysTruthy, Literal[1]]
static_assert(is_equivalent_to(IA, Literal[0] | AlwaysTruthy))
type ID = Union[Literal[0], AlwaysTruthy, Literal[1], AlwaysFalsy, AlwaysTruthy, str]
static_assert(is_equivalent_to(ID, AlwaysTruthy | AlwaysFalsy | str))
```
## Unions with intersections of literals and Any
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Any, Literal
from ty_extensions import Intersection
type SA = Literal[""]
type SB = Intersection[Literal[""], Any]
type SC = SA | SB
type SD = SB | SA
def _(c: SC, d: SD):
reveal_type(c) # revealed: Literal[""]
reveal_type(d) # revealed: Literal[""]
type IA = Literal[0]
type IB = Intersection[Literal[0], Any]
type IC = IA | IB
type ID = IB | IA
def _(c: IC, d: ID):
reveal_type(c) # revealed: Literal[0]
reveal_type(d) # revealed: Literal[0]
type BA = Literal[b""]
type BB = Intersection[Literal[b""], Any]
type BC = BA | BB
type BD = BB | BA
def _(c: BC, d: BD):
reveal_type(c) # revealed: Literal[b""]
reveal_type(d) # revealed: Literal[b""]
```

View file

@ -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> {
/// >
/// > &mdash; [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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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