Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The invariant position is replaced with an unresolved type variable.

```py
def _(top_list: Top[list[Any]]):
reveal_type(top_list) # revealed: list[T_all]
reveal_type(top_list) # revealed: Top[list[Any]]
```

### Bottom materialization
Expand Down Expand Up @@ -75,7 +75,7 @@ type variable.

```py
def _(bottom_list: Bottom[list[Any]]):
reveal_type(bottom_list) # revealed: list[T_all]
reveal_type(bottom_list) # revealed: Bottom[list[Any]]
```

## Fully static types
Expand Down Expand Up @@ -230,14 +230,14 @@ def _(
top_aiu: Top[LTAnyIntUnknown],
bottom_aiu: Bottom[LTAnyIntUnknown],
):
reveal_type(top_ai) # revealed: list[tuple[T_all, int]]
reveal_type(bottom_ai) # revealed: list[tuple[T_all, int]]
reveal_type(top_ai) # revealed: Top[list[tuple[Any, int]]]
reveal_type(bottom_ai) # revealed: Bottom[list[tuple[Any, int]]]

reveal_type(top_su) # revealed: list[tuple[str, T_all]]
reveal_type(bottom_su) # revealed: list[tuple[str, T_all]]
reveal_type(top_su) # revealed: Top[list[tuple[str, Unknown]]]
reveal_type(bottom_su) # revealed: Bottom[list[tuple[str, Unknown]]]

reveal_type(top_aiu) # revealed: list[tuple[T_all, int, T_all]]
reveal_type(bottom_aiu) # revealed: list[tuple[T_all, int, T_all]]
reveal_type(top_aiu) # revealed: Top[list[tuple[Any, int, Unknown]]]
reveal_type(bottom_aiu) # revealed: Bottom[list[tuple[Any, int, Unknown]]]
```

## Union
Expand Down Expand Up @@ -286,14 +286,14 @@ def _(
top_aiu: Top[list[Any | int | Unknown]],
bottom_aiu: Bottom[list[Any | int | Unknown]],
):
reveal_type(top_ai) # revealed: list[T_all | int]
reveal_type(bottom_ai) # revealed: list[T_all | int]
reveal_type(top_ai) # revealed: Top[list[Any | int]]
reveal_type(bottom_ai) # revealed: Bottom[list[Any | int]]

reveal_type(top_su) # revealed: list[str | T_all]
reveal_type(bottom_su) # revealed: list[str | T_all]
reveal_type(top_su) # revealed: Top[list[str | Unknown]]
reveal_type(bottom_su) # revealed: Bottom[list[str | Unknown]]

reveal_type(top_aiu) # revealed: list[T_all | int]
reveal_type(bottom_aiu) # revealed: list[T_all | int]
reveal_type(top_aiu) # revealed: Top[list[Any | int]]
reveal_type(bottom_aiu) # revealed: Bottom[list[Any | int]]
```

## Intersection
Expand All @@ -320,8 +320,10 @@ def _(
top: Top[Intersection[list[Any], list[int]]],
bottom: Bottom[Intersection[list[Any], list[int]]],
):
reveal_type(top) # revealed: list[T_all] & list[int]
reveal_type(bottom) # revealed: list[T_all] & list[int]
# Top[list[Any] & list[int]] = Top[list[Any]] & list[int] = list[int]
reveal_type(top) # revealed: list[int]
# Bottom[list[Any] & list[int]] = Bottom[list[Any]] & list[int] = Bottom[list[Any]]
reveal_type(bottom) # revealed: Bottom[list[Any]]
```

## Negation (via `Not`)
Expand Down Expand Up @@ -366,8 +368,8 @@ static_assert(is_equivalent_to(Bottom[type[int | Any]], type[int]))

# Here, `T` has an upper bound of `type`
def _(top: Top[list[type[Any]]], bottom: Bottom[list[type[Any]]]):
reveal_type(top) # revealed: list[T_all]
reveal_type(bottom) # revealed: list[T_all]
reveal_type(top) # revealed: Top[list[type[Any]]]
reveal_type(bottom) # revealed: Bottom[list[type[Any]]]
```

## Type variables
Expand Down Expand Up @@ -427,8 +429,8 @@ class GenericContravariant(Generic[T_contra]):
pass

def _(top: Top[GenericInvariant[Any]], bottom: Bottom[GenericInvariant[Any]]):
reveal_type(top) # revealed: GenericInvariant[T_all]
reveal_type(bottom) # revealed: GenericInvariant[T_all]
reveal_type(top) # revealed: Top[GenericInvariant[Any]]
reveal_type(bottom) # revealed: Bottom[GenericInvariant[Any]]

static_assert(is_equivalent_to(Top[GenericCovariant[Any]], GenericCovariant[object]))
static_assert(is_equivalent_to(Bottom[GenericCovariant[Any]], GenericCovariant[Never]))
Expand All @@ -448,8 +450,8 @@ type CovariantCallable = Callable[[GenericCovariant[Any]], None]
type ContravariantCallable = Callable[[GenericContravariant[Any]], None]

def invariant(top: Top[InvariantCallable], bottom: Bottom[InvariantCallable]) -> None:
reveal_type(top) # revealed: (GenericInvariant[T_all], /) -> None
reveal_type(bottom) # revealed: (GenericInvariant[T_all], /) -> None
reveal_type(top) # revealed: (Bottom[GenericInvariant[Any]], /) -> None
reveal_type(bottom) # revealed: (Top[GenericInvariant[Any]], /) -> None

def covariant(top: Top[CovariantCallable], bottom: Bottom[CovariantCallable]) -> None:
reveal_type(top) # revealed: (GenericCovariant[Never], /) -> None
Expand Down Expand Up @@ -492,3 +494,207 @@ def _(
bottom_1: Bottom[1], # error: [invalid-type-form]
): ...
```

## Nested use

`Top[T]` and `Bottom[T]` are always fully static types. Therefore, they have only one
materialization (themselves) and applying `Top` or `Bottom` again does nothing.

```py
from typing import Any
from ty_extensions import Top, Bottom, static_assert, is_equivalent_to

static_assert(is_equivalent_to(Top[Top[list[Any]]], Top[list[Any]]))
static_assert(is_equivalent_to(Bottom[Top[list[Any]]], Top[list[Any]]))

static_assert(is_equivalent_to(Bottom[Bottom[list[Any]]], Bottom[list[Any]]))
static_assert(is_equivalent_to(Top[Bottom[list[Any]]], Bottom[list[Any]]))
```

## Subtyping

Any `list[T]` is a subtype of `Top[list[Any]]`, but with more restrictive gradual types, not all
other specializations are subtypes.

```py
from typing import Any, Literal
from ty_extensions import is_subtype_of, static_assert, Top, Intersection, Bottom

# None and Top
static_assert(is_subtype_of(list[int], Top[list[Any]]))
static_assert(not is_subtype_of(Top[list[Any]], list[int]))
static_assert(is_subtype_of(list[bool], Top[list[Intersection[int, Any]]]))
static_assert(is_subtype_of(list[int], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(list[int | str], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(list[object], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(list[str], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(list[str | bool], Top[list[Intersection[int, Any]]]))

# Top and Top
static_assert(is_subtype_of(Top[list[int | Any]], Top[list[Any]]))
static_assert(not is_subtype_of(Top[list[Any]], Top[list[int | Any]]))
static_assert(is_subtype_of(Top[list[Intersection[int, Any]]], Top[list[Any]]))
static_assert(not is_subtype_of(Top[list[Any]], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(Top[list[Intersection[int, Any]]], Top[list[int | Any]]))
static_assert(not is_subtype_of(Top[list[int | Any]], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(Top[list[str | Any]], Top[list[int | Any]]))
static_assert(is_subtype_of(Top[list[str | int | Any]], Top[list[int | Any]]))
static_assert(not is_subtype_of(Top[list[int | Any]], Top[list[str | int | Any]]))

# Bottom and Top
static_assert(is_subtype_of(Bottom[list[Any]], Top[list[Any]]))
static_assert(is_subtype_of(Bottom[list[Any]], Top[list[int | Any]]))
static_assert(is_subtype_of(Bottom[list[int | Any]], Top[list[Any]]))
static_assert(is_subtype_of(Bottom[list[int | Any]], Top[list[int | str]]))
static_assert(is_subtype_of(Bottom[list[Intersection[int, Any]]], Top[list[Intersection[str, Any]]]))
static_assert(not is_subtype_of(Bottom[list[Intersection[int, bool | Any]]], Bottom[list[Intersection[str, Literal["x"] | Any]]]))

# None and None
static_assert(not is_subtype_of(list[int], list[Any]))
static_assert(not is_subtype_of(list[Any], list[int]))
static_assert(is_subtype_of(list[int], list[int]))
static_assert(not is_subtype_of(list[int], list[object]))
static_assert(not is_subtype_of(list[object], list[int]))

# Top and None
static_assert(not is_subtype_of(Top[list[Any]], list[Any]))
static_assert(not is_subtype_of(Top[list[Any]], list[int]))
static_assert(is_subtype_of(Top[list[int]], list[int]))

# Bottom and None
static_assert(is_subtype_of(Bottom[list[Any]], list[object]))
static_assert(is_subtype_of(Bottom[list[int | Any]], list[str | int]))
static_assert(not is_subtype_of(Bottom[list[str | Any]], list[Intersection[int, bool | Any]]))

# None and Bottom
static_assert(not is_subtype_of(list[int], Bottom[list[Any]]))
static_assert(not is_subtype_of(list[int], Bottom[list[int | Any]]))
static_assert(is_subtype_of(list[int], Bottom[list[int]]))

# Top and Bottom
static_assert(not is_subtype_of(Top[list[Any]], Bottom[list[Any]]))
static_assert(not is_subtype_of(Top[list[int | Any]], Bottom[list[int | Any]]))
static_assert(is_subtype_of(Top[list[int]], Bottom[list[int]]))

# Bottom and Bottom
static_assert(is_subtype_of(Bottom[list[Any]], Bottom[list[int | str | Any]]))
static_assert(is_subtype_of(Bottom[list[int | Any]], Bottom[list[int | str | Any]]))
static_assert(is_subtype_of(Bottom[list[bool | Any]], Bottom[list[int | Any]]))
static_assert(not is_subtype_of(Bottom[list[int | Any]], Bottom[list[bool | Any]]))
static_assert(not is_subtype_of(Bottom[list[int | Any]], Bottom[list[Any]]))
```

## Assignability

### General

Assignability is the same as subtyping for top and bottom materializations, because those are fully
static types, but some gradual types are assignable even if they are not subtypes.

```py
from typing import Any, Literal
from ty_extensions import is_assignable_to, static_assert, Top, Intersection, Bottom

# None and Top
static_assert(is_assignable_to(list[Any], Top[list[Any]]))
static_assert(is_assignable_to(list[int], Top[list[Any]]))
static_assert(not is_assignable_to(Top[list[Any]], list[int]))
static_assert(is_assignable_to(list[bool], Top[list[Intersection[int, Any]]]))
static_assert(is_assignable_to(list[int], Top[list[Intersection[int, Any]]]))
static_assert(is_assignable_to(list[Any], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(list[int | str], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(list[object], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(list[str], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(list[str | bool], Top[list[Intersection[int, Any]]]))

# Top and Top
static_assert(is_assignable_to(Top[list[int | Any]], Top[list[Any]]))
static_assert(not is_assignable_to(Top[list[Any]], Top[list[int | Any]]))
static_assert(is_assignable_to(Top[list[Intersection[int, Any]]], Top[list[Any]]))
static_assert(not is_assignable_to(Top[list[Any]], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(Top[list[Intersection[int, Any]]], Top[list[int | Any]]))
static_assert(not is_assignable_to(Top[list[int | Any]], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(Top[list[str | Any]], Top[list[int | Any]]))
static_assert(is_assignable_to(Top[list[str | int | Any]], Top[list[int | Any]]))
static_assert(not is_assignable_to(Top[list[int | Any]], Top[list[str | int | Any]]))

# Bottom and Top
static_assert(is_assignable_to(Bottom[list[Any]], Top[list[Any]]))
static_assert(is_assignable_to(Bottom[list[Any]], Top[list[int | Any]]))
static_assert(is_assignable_to(Bottom[list[int | Any]], Top[list[Any]]))
static_assert(is_assignable_to(Bottom[list[Intersection[int, Any]]], Top[list[Intersection[str, Any]]]))
static_assert(
not is_assignable_to(Bottom[list[Intersection[int, bool | Any]]], Bottom[list[Intersection[str, Literal["x"] | Any]]])
)

# None and None
static_assert(is_assignable_to(list[int], list[Any]))
static_assert(is_assignable_to(list[Any], list[int]))
static_assert(is_assignable_to(list[int], list[int]))
static_assert(not is_assignable_to(list[int], list[object]))
static_assert(not is_assignable_to(list[object], list[int]))

# Top and None
static_assert(is_assignable_to(Top[list[Any]], list[Any]))
static_assert(not is_assignable_to(Top[list[Any]], list[int]))
static_assert(is_assignable_to(Top[list[int]], list[int]))

# Bottom and None
static_assert(is_assignable_to(Bottom[list[Any]], list[object]))
static_assert(is_assignable_to(Bottom[list[int | Any]], Top[list[str | int]]))
static_assert(not is_assignable_to(Bottom[list[str | Any]], list[Intersection[int, bool | Any]]))

# None and Bottom
static_assert(is_assignable_to(list[Any], Bottom[list[Any]]))
static_assert(not is_assignable_to(list[int], Bottom[list[Any]]))
static_assert(not is_assignable_to(list[int], Bottom[list[int | Any]]))
static_assert(is_assignable_to(list[int], Bottom[list[int]]))

# Top and Bottom
static_assert(not is_assignable_to(Top[list[Any]], Bottom[list[Any]]))
static_assert(not is_assignable_to(Top[list[int | Any]], Bottom[list[int | Any]]))
static_assert(is_assignable_to(Top[list[int]], Bottom[list[int]]))

# Bottom and Bottom
static_assert(is_assignable_to(Bottom[list[Any]], Bottom[list[int | str | Any]]))
static_assert(is_assignable_to(Bottom[list[int | Any]], Bottom[list[int | str | Any]]))
static_assert(is_assignable_to(Bottom[list[bool | Any]], Bottom[list[int | Any]]))
static_assert(not is_assignable_to(Bottom[list[int | Any]], Bottom[list[bool | Any]]))
static_assert(not is_assignable_to(Bottom[list[int | Any]], Bottom[list[Any]]))
```

### Subclasses with different variance

We need to take special care when an invariant class inherits from a covariant or contravariant one.
This comes up frequently in practice because `list` (invariant) inherits from `Sequence` and a
number of other covariant ABCs, but we'll use a synthetic example.

```py
from typing import Generic, TypeVar, Any
from ty_extensions import static_assert, is_assignable_to, is_equivalent_to, Top

class A:
pass

class B(A):
pass

T_co = TypeVar("T_co", covariant=True)
T = TypeVar("T")

class CovariantBase(Generic[T_co]):
def get(self) -> T_co:
raise NotImplementedError

class InvariantChild(CovariantBase[T]):
def push(self, obj: T) -> None: ...

static_assert(is_assignable_to(InvariantChild[A], CovariantBase[A]))
static_assert(is_assignable_to(InvariantChild[B], CovariantBase[A]))
static_assert(not is_assignable_to(InvariantChild[A], CovariantBase[B]))
static_assert(not is_assignable_to(InvariantChild[B], InvariantChild[A]))
static_assert(is_equivalent_to(Top[CovariantBase[Any]], CovariantBase[object]))
static_assert(is_assignable_to(InvariantChild[Any], CovariantBase[A]))

static_assert(not is_assignable_to(Top[InvariantChild[Any]], CovariantBase[A]))
```
Loading
Loading