[ty] Add Top[] and Bottom[] special forms, replacing top_materialization_of() function (#20054)
Some checks failed
CI / mkdocs (push) Has been cancelled
CI / Determine changes (push) Has been cancelled
CI / cargo fmt (push) Has been cancelled
CI / cargo build (release) (push) Has been cancelled
CI / python package (push) Has been cancelled
CI / pre-commit (push) Has been cancelled
[ty Playground] Release / publish (push) Has been cancelled
CI / cargo clippy (push) Has been cancelled
CI / cargo test (linux) (push) Has been cancelled
CI / cargo test (linux, release) (push) Has been cancelled
CI / cargo test (windows) (push) Has been cancelled
CI / cargo test (wasm) (push) Has been cancelled
CI / formatter instabilities and black similarity (push) Has been cancelled
CI / cargo build (msrv) (push) Has been cancelled
CI / cargo fuzz build (push) Has been cancelled
CI / fuzz parser (push) Has been cancelled
CI / test scripts (push) Has been cancelled
CI / ecosystem (push) Has been cancelled
CI / Fuzz for new ty panics (push) Has been cancelled
CI / cargo shear (push) Has been cancelled
CI / test ruff-lsp (push) Has been cancelled
CI / check playground (push) Has been cancelled
CI / benchmarks-instrumented (push) Has been cancelled
CI / benchmarks-walltime (push) Has been cancelled

Part of astral-sh/ty#994

## Summary

Add new special forms to `ty_extensions`, `Top[T]` and `Bottom[T]`.
Remove `ty_extensions.top_materialization` and
`ty_extensions.bottom_materialization`.

## Test Plan

Converted the existing `materialization.md` mdtest to the new syntax.
Added some tests for invalid use of the new special form.
This commit is contained in:
Jelle Zijlstra 2025-08-23 11:20:56 -07:00 committed by GitHub
parent e7237652a9
commit ec86a4e960
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 288 additions and 182 deletions

View file

@ -29,22 +29,25 @@ The dynamic type at the top-level is replaced with `object`.
```py ```py
from typing import Any, Callable from typing import Any, Callable
from ty_extensions import Unknown, top_materialization from ty_extensions import Unknown, Top
reveal_type(top_materialization(Any)) # revealed: object def _(top_any: Top[Any], top_unknown: Top[Unknown]):
reveal_type(top_materialization(Unknown)) # revealed: object reveal_type(top_any) # revealed: object
reveal_type(top_unknown) # revealed: object
``` ```
The contravariant position is replaced with `Never`. The contravariant position is replaced with `Never`.
```py ```py
reveal_type(top_materialization(Callable[[Any], None])) # revealed: (Never, /) -> None def _(top_callable: Top[Callable[[Any], None]]):
reveal_type(top_callable) # revealed: (Never, /) -> None
``` ```
The invariant position is replaced with an unresolved type variable. The invariant position is replaced with an unresolved type variable.
```py ```py
reveal_type(top_materialization(list[Any])) # revealed: list[T_all] def _(top_list: Top[list[Any]]):
reveal_type(top_list) # revealed: list[T_all]
``` ```
### Bottom materialization ### Bottom materialization
@ -53,24 +56,26 @@ The dynamic type at the top-level is replaced with `Never`.
```py ```py
from typing import Any, Callable from typing import Any, Callable
from ty_extensions import Unknown, bottom_materialization from ty_extensions import Unknown, Bottom
reveal_type(bottom_materialization(Any)) # revealed: Never def _(bottom_any: Bottom[Any], bottom_unknown: Bottom[Unknown]):
reveal_type(bottom_materialization(Unknown)) # revealed: Never reveal_type(bottom_any) # revealed: Never
reveal_type(bottom_unknown) # revealed: Never
``` ```
The contravariant position is replaced with `object`. The contravariant position is replaced with `object`.
```py ```py
# revealed: (object, object, /) -> None def _(bottom_callable: Bottom[Callable[[Any, Unknown], None]]):
reveal_type(bottom_materialization(Callable[[Any, Unknown], None])) reveal_type(bottom_callable) # revealed: (object, object, /) -> None
``` ```
The invariant position is replaced in the same way as the top materialization, with an unresolved The invariant position is replaced in the same way as the top materialization, with an unresolved
type variable. type variable.
```py ```py
reveal_type(bottom_materialization(list[Any])) # revealed: list[T_all] def _(bottom_list: Bottom[list[Any]]):
reveal_type(bottom_list) # revealed: list[T_all]
``` ```
## Fully static types ## Fully static types
@ -79,30 +84,30 @@ The top / bottom (and only) materialization of any fully static type is just its
```py ```py
from typing import Any, Literal from typing import Any, Literal
from ty_extensions import TypeOf, bottom_materialization, top_materialization from ty_extensions import TypeOf, Bottom, Top, is_equivalent_to, static_assert
from enum import Enum from enum import Enum
class Answer(Enum): class Answer(Enum):
NO = 0 NO = 0
YES = 1 YES = 1
reveal_type(top_materialization(int)) # revealed: int static_assert(is_equivalent_to(Top[int], int))
reveal_type(bottom_materialization(int)) # revealed: int static_assert(is_equivalent_to(Bottom[int], int))
reveal_type(top_materialization(Literal[1])) # revealed: Literal[1] static_assert(is_equivalent_to(Top[Literal[1]], Literal[1]))
reveal_type(bottom_materialization(Literal[1])) # revealed: Literal[1] static_assert(is_equivalent_to(Bottom[Literal[1]], Literal[1]))
reveal_type(top_materialization(Literal[True])) # revealed: Literal[True] static_assert(is_equivalent_to(Top[Literal[True]], Literal[True]))
reveal_type(bottom_materialization(Literal[True])) # revealed: Literal[True] static_assert(is_equivalent_to(Bottom[Literal[True]], Literal[True]))
reveal_type(top_materialization(Literal["abc"])) # revealed: Literal["abc"] static_assert(is_equivalent_to(Top[Literal["abc"]], Literal["abc"]))
reveal_type(bottom_materialization(Literal["abc"])) # revealed: Literal["abc"] static_assert(is_equivalent_to(Bottom[Literal["abc"]], Literal["abc"]))
reveal_type(top_materialization(Literal[Answer.YES])) # revealed: Literal[Answer.YES] static_assert(is_equivalent_to(Top[Literal[Answer.YES]], Literal[Answer.YES]))
reveal_type(bottom_materialization(Literal[Answer.YES])) # revealed: Literal[Answer.YES] static_assert(is_equivalent_to(Bottom[Literal[Answer.YES]], Literal[Answer.YES]))
reveal_type(top_materialization(int | str)) # revealed: int | str static_assert(is_equivalent_to(Top[int | str], int | str))
reveal_type(bottom_materialization(int | str)) # revealed: int | str static_assert(is_equivalent_to(Bottom[int | str], int | str))
``` ```
We currently treat function literals as fully static types, so they remain unchanged even though the We currently treat function literals as fully static types, so they remain unchanged even though the
@ -114,11 +119,17 @@ def function(x: Any) -> None: ...
class A: class A:
def method(self, x: Any) -> None: ... def method(self, x: Any) -> None: ...
reveal_type(top_materialization(TypeOf[function])) # revealed: def function(x: Any) -> None def _(
reveal_type(bottom_materialization(TypeOf[function])) # revealed: def function(x: Any) -> None top_func: Top[TypeOf[function]],
bottom_func: Bottom[TypeOf[function]],
top_meth: Top[TypeOf[A().method]],
bottom_meth: Bottom[TypeOf[A().method]],
):
reveal_type(top_func) # revealed: def function(x: Any) -> None
reveal_type(bottom_func) # revealed: def function(x: Any) -> None
reveal_type(top_materialization(TypeOf[A().method])) # revealed: bound method A.method(x: Any) -> None reveal_type(top_meth) # revealed: bound method A.method(x: Any) -> None
reveal_type(bottom_materialization(TypeOf[A().method])) # revealed: bound method A.method(x: Any) -> None reveal_type(bottom_meth) # revealed: bound method A.method(x: Any) -> None
``` ```
## Callable ## Callable
@ -126,27 +137,30 @@ reveal_type(bottom_materialization(TypeOf[A().method])) # revealed: bound metho
For a callable, the parameter types are in a contravariant position, and the return type is in a For a callable, the parameter types are in a contravariant position, and the return type is in a
covariant position. covariant position.
```toml
[environment]
python-version = "3.12"
```
```py ```py
from typing import Any, Callable from typing import Any, Callable
from ty_extensions import TypeOf, Unknown, bottom_materialization, top_materialization from ty_extensions import TypeOf, Unknown, Bottom, Top
def _(callable: Callable[[Any, Unknown], Any]) -> None: type C1 = Callable[[Any, Unknown], Any]
# revealed: (Never, Never, /) -> object
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (object, object, /) -> Never def _(top: Top[C1], bottom: Bottom[C1]) -> None:
reveal_type(bottom_materialization(TypeOf[callable])) reveal_type(top) # revealed: (Never, Never, /) -> object
reveal_type(bottom) # revealed: (object, object, /) -> Never
``` ```
The parameter types in a callable inherits the contravariant position. The parameter types in a callable inherits the contravariant position.
```py ```py
def _(callable: Callable[[int, tuple[int | Any]], tuple[Any]]) -> None: type C2 = Callable[[int, tuple[int | Any]], tuple[Any]]
# revealed: (int, tuple[int], /) -> tuple[object]
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (int, tuple[object], /) -> Never def _(top: Top[C2], bottom: Bottom[C2]) -> None:
reveal_type(bottom_materialization(TypeOf[callable])) reveal_type(top) # revealed: (int, tuple[int], /) -> tuple[object]
reveal_type(bottom) # revealed: (int, tuple[object], /) -> Never
``` ```
But, if the callable itself is in a contravariant position, then the variance is flipped i.e., if But, if the callable itself is in a contravariant position, then the variance is flipped i.e., if
@ -154,30 +168,37 @@ the outer variance is covariant, it's flipped to contravariant, and if it's cont
flipped to covariant, invariant remains invariant. flipped to covariant, invariant remains invariant.
```py ```py
def _(callable: Callable[[Any, Callable[[Unknown], Any]], Callable[[Any, int], Any]]) -> None: type C3 = Callable[[Any, Callable[[Unknown], Any]], Callable[[Any, int], Any]]
def _(top: Top[C3], bottom: Bottom[C3]) -> None:
# revealed: (Never, (object, /) -> Never, /) -> (Never, int, /) -> object # revealed: (Never, (object, /) -> Never, /) -> (Never, int, /) -> object
reveal_type(top_materialization(TypeOf[callable])) reveal_type(top)
# revealed: (object, (Never, /) -> object, /) -> (object, int, /) -> Never # revealed: (object, (Never, /) -> object, /) -> (object, int, /) -> Never
reveal_type(bottom_materialization(TypeOf[callable])) reveal_type(bottom)
``` ```
## Tuple ## Tuple
All positions in a tuple are covariant. All positions in a tuple are covariant.
```toml
[environment]
python-version = "3.12"
```
```py ```py
from typing import Any from typing import Any, Never
from ty_extensions import Unknown, bottom_materialization, top_materialization from ty_extensions import Unknown, Bottom, Top, is_equivalent_to, static_assert
reveal_type(top_materialization(tuple[Any, int])) # revealed: tuple[object, int] static_assert(is_equivalent_to(Top[tuple[Any, int]], tuple[object, int]))
reveal_type(bottom_materialization(tuple[Any, int])) # revealed: Never static_assert(is_equivalent_to(Bottom[tuple[Any, int]], Never))
reveal_type(top_materialization(tuple[Unknown, int])) # revealed: tuple[object, int] static_assert(is_equivalent_to(Top[tuple[Unknown, int]], tuple[object, int]))
reveal_type(bottom_materialization(tuple[Unknown, int])) # revealed: Never static_assert(is_equivalent_to(Bottom[tuple[Unknown, int]], Never))
reveal_type(top_materialization(tuple[Any, int, Unknown])) # revealed: tuple[object, int, object] static_assert(is_equivalent_to(Top[tuple[Any, int, Unknown]], tuple[object, int, object]))
reveal_type(bottom_materialization(tuple[Any, int, Unknown])) # revealed: Never static_assert(is_equivalent_to(Bottom[tuple[Any, int, Unknown]], Never))
``` ```
Except for when the tuple itself is in a contravariant position, then all positions in the tuple Except for when the tuple itself is in a contravariant position, then all positions in the tuple
@ -187,43 +208,59 @@ inherit the contravariant position.
from typing import Callable from typing import Callable
from ty_extensions import TypeOf from ty_extensions import TypeOf
def _(callable: Callable[[tuple[Any, int], tuple[str, Unknown]], None]) -> None: type C = Callable[[tuple[Any, int], tuple[str, Unknown]], None]
# revealed: (Never, Never, /) -> None
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (tuple[object, int], tuple[str, object], /) -> None def _(top: Top[C], bottom: Bottom[C]) -> None:
reveal_type(bottom_materialization(TypeOf[callable])) reveal_type(top) # revealed: (Never, Never, /) -> None
reveal_type(bottom) # revealed: (tuple[object, int], tuple[str, object], /) -> None
``` ```
And, similarly for an invariant position. And, similarly for an invariant position.
```py ```py
reveal_type(top_materialization(list[tuple[Any, int]])) # revealed: list[tuple[T_all, int]] type LTAnyInt = list[tuple[Any, int]]
reveal_type(bottom_materialization(list[tuple[Any, int]])) # revealed: list[tuple[T_all, int]] type LTStrUnknown = list[tuple[str, Unknown]]
type LTAnyIntUnknown = list[tuple[Any, int, Unknown]]
reveal_type(top_materialization(list[tuple[str, Unknown]])) # revealed: list[tuple[str, T_all]] def _(
reveal_type(bottom_materialization(list[tuple[str, Unknown]])) # revealed: list[tuple[str, T_all]] top_ai: Top[LTAnyInt],
bottom_ai: Bottom[LTAnyInt],
top_su: Top[LTStrUnknown],
bottom_su: Bottom[LTStrUnknown],
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_materialization(list[tuple[Any, int, Unknown]])) # revealed: list[tuple[T_all, int, T_all]] reveal_type(top_su) # revealed: list[tuple[str, T_all]]
reveal_type(bottom_materialization(list[tuple[Any, int, Unknown]])) # revealed: list[tuple[T_all, int, T_all]] reveal_type(bottom_su) # revealed: list[tuple[str, T_all]]
reveal_type(top_aiu) # revealed: list[tuple[T_all, int, T_all]]
reveal_type(bottom_aiu) # revealed: list[tuple[T_all, int, T_all]]
``` ```
## Union ## Union
All positions in a union are covariant. All positions in a union are covariant.
```toml
[environment]
python-version = "3.12"
```
```py ```py
from typing import Any from typing import Any
from ty_extensions import Unknown, bottom_materialization, top_materialization from ty_extensions import Unknown, Bottom, Top, static_assert, is_equivalent_to
reveal_type(top_materialization(Any | int)) # revealed: object static_assert(is_equivalent_to(Top[Any | int], object))
reveal_type(bottom_materialization(Any | int)) # revealed: int static_assert(is_equivalent_to(Bottom[Any | int], int))
reveal_type(top_materialization(Unknown | int)) # revealed: object static_assert(is_equivalent_to(Top[Unknown | int], object))
reveal_type(bottom_materialization(Unknown | int)) # revealed: int static_assert(is_equivalent_to(Bottom[Unknown | int], int))
reveal_type(top_materialization(int | str | Any)) # revealed: object static_assert(is_equivalent_to(Top[int | str | Any], object))
reveal_type(bottom_materialization(int | str | Any)) # revealed: int | str static_assert(is_equivalent_to(Bottom[int | str | Any], int | str))
``` ```
Except for when the union itself is in a contravariant position, then all positions in the union Except for when the union itself is in a contravariant position, then all positions in the union
@ -234,24 +271,29 @@ from typing import Callable
from ty_extensions import TypeOf from ty_extensions import TypeOf
def _(callable: Callable[[Any | int, str | Unknown], None]) -> None: def _(callable: Callable[[Any | int, str | Unknown], None]) -> None:
# revealed: (int, str, /) -> None static_assert(is_equivalent_to(Top[TypeOf[callable]], Callable[[int, str], None]))
reveal_type(top_materialization(TypeOf[callable])) static_assert(is_equivalent_to(Bottom[TypeOf[callable]], Callable[[object, object], None]))
# revealed: (object, object, /) -> None
reveal_type(bottom_materialization(TypeOf[callable]))
``` ```
And, similarly for an invariant position. And, similarly for an invariant position.
```py ```py
reveal_type(top_materialization(list[Any | int])) # revealed: list[T_all | int] def _(
reveal_type(bottom_materialization(list[Any | int])) # revealed: list[T_all | int] top_ai: Top[list[Any | int]],
bottom_ai: Bottom[list[Any | int]],
top_su: Top[list[str | Unknown]],
bottom_su: Bottom[list[str | Unknown]],
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_materialization(list[str | Unknown])) # revealed: list[str | T_all] reveal_type(top_su) # revealed: list[str | T_all]
reveal_type(bottom_materialization(list[str | Unknown])) # revealed: list[str | T_all] reveal_type(bottom_su) # revealed: list[str | T_all]
reveal_type(top_materialization(list[Any | int | Unknown])) # revealed: list[T_all | int] reveal_type(top_aiu) # revealed: list[T_all | int]
reveal_type(bottom_materialization(list[Any | int | Unknown])) # revealed: list[T_all | int] reveal_type(bottom_aiu) # revealed: list[T_all | int]
``` ```
## Intersection ## Intersection
@ -260,24 +302,26 @@ All positions in an intersection are covariant.
```py ```py
from typing import Any from typing import Any
from ty_extensions import Intersection, Unknown, bottom_materialization, top_materialization from typing_extensions import Never
from ty_extensions import Intersection, Unknown, Bottom, Top, static_assert, is_equivalent_to
reveal_type(top_materialization(Intersection[Any, int])) # revealed: int static_assert(is_equivalent_to(Top[Intersection[Any, int]], int))
reveal_type(bottom_materialization(Intersection[Any, int])) # revealed: Never static_assert(is_equivalent_to(Bottom[Intersection[Any, int]], Never))
# Here, the top materialization of `Any | int` is `object` and the intersection of it with tuple # Here, the top materialization of `Any | int` is `object` and the intersection of it with tuple
# revealed: tuple[str, object] static_assert(is_equivalent_to(Top[Intersection[Any | int, tuple[str, Unknown]]], tuple[str, object]))
reveal_type(top_materialization(Intersection[Any | int, tuple[str, Unknown]])) static_assert(is_equivalent_to(Bottom[Intersection[Any | int, tuple[str, Unknown]]], Never))
# revealed: Never
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str, Unknown]]))
class Foo: ... class Foo: ...
# revealed: Foo & tuple[str] static_assert(is_equivalent_to(Bottom[Intersection[Any | Foo, tuple[str]]], Intersection[Foo, tuple[str]]))
reveal_type(bottom_materialization(Intersection[Any | Foo, tuple[str]]))
reveal_type(top_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int] def _(
reveal_type(bottom_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int] 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]
``` ```
## Negation (via `Not`) ## Negation (via `Not`)
@ -286,38 +330,44 @@ All positions in a negation are contravariant.
```py ```py
from typing import Any from typing import Any
from ty_extensions import Not, Unknown, bottom_materialization, top_materialization from typing_extensions import Never
from ty_extensions import Not, Unknown, Bottom, Top, static_assert, is_equivalent_to
# ~Any is still Any, so the top materialization is object # ~Any is still Any, so the top materialization is object
reveal_type(top_materialization(Not[Any])) # revealed: object static_assert(is_equivalent_to(Top[Not[Any]], object))
reveal_type(bottom_materialization(Not[Any])) # revealed: Never static_assert(is_equivalent_to(Bottom[Not[Any]], Never))
# tuple[Any, int] is in a contravariant position, so the # tuple[Any, int] is in a contravariant position, so the
# top materialization is Never and the negation of it # top materialization is Never and the negation of it
# revealed: object static_assert(is_equivalent_to(Top[Not[tuple[Any, int]]], object))
reveal_type(top_materialization(Not[tuple[Any, int]])) static_assert(is_equivalent_to(Bottom[Not[tuple[Any, int]]], Not[tuple[object, int]]))
# revealed: ~tuple[object, int]
reveal_type(bottom_materialization(Not[tuple[Any, int]]))
``` ```
## `type` ## `type`
```toml
[environment]
python-version = "3.12"
```
```py ```py
from typing import Any from typing import Any
from ty_extensions import Unknown, bottom_materialization, top_materialization from typing_extensions import Never
from ty_extensions import Unknown, Bottom, Top, static_assert, is_equivalent_to
reveal_type(top_materialization(type[Any])) # revealed: type static_assert(is_equivalent_to(Top[type[Any]], type))
reveal_type(bottom_materialization(type[Any])) # revealed: Never static_assert(is_equivalent_to(Bottom[type[Any]], Never))
reveal_type(top_materialization(type[Unknown])) # revealed: type static_assert(is_equivalent_to(Top[type[Unknown]], type))
reveal_type(bottom_materialization(type[Unknown])) # revealed: Never static_assert(is_equivalent_to(Bottom[type[Unknown]], Never))
reveal_type(top_materialization(type[int | Any])) # revealed: type static_assert(is_equivalent_to(Top[type[int | Any]], type))
reveal_type(bottom_materialization(type[int | Any])) # revealed: type[int] static_assert(is_equivalent_to(Bottom[type[int | Any]], type[int]))
# Here, `T` has an upper bound of `type` # Here, `T` has an upper bound of `type`
reveal_type(top_materialization(list[type[Any]])) # revealed: list[T_all] def _(top: Top[list[type[Any]]], bottom: Bottom[list[type[Any]]]):
reveal_type(bottom_materialization(list[type[Any]])) # revealed: list[T_all] reveal_type(top) # revealed: list[T_all]
reveal_type(bottom) # revealed: list[T_all]
``` ```
## Type variables ## Type variables
@ -329,26 +379,19 @@ python-version = "3.12"
```py ```py
from typing import Any, Never, TypeVar from typing import Any, Never, TypeVar
from ty_extensions import ( from ty_extensions import Unknown, Bottom, Top, static_assert, is_subtype_of
TypeOf,
Unknown,
bottom_materialization,
top_materialization,
static_assert,
is_subtype_of,
)
def bounded_by_gradual[T: Any](t: T) -> None: def bounded_by_gradual[T: Any](t: T) -> None:
# Top materialization of `T: Any` is `T: object` # Top materialization of `T: Any` is `T: object`
# Bottom materialization of `T: Any` is `T: Never` # Bottom materialization of `T: Any` is `T: Never`
static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], Never)) static_assert(is_subtype_of(Bottom[T], Never))
def constrained_by_gradual[T: (int, Any)](t: T) -> None: def constrained_by_gradual[T: (int, Any)](t: T) -> None:
# Top materialization of `T: (int, Any)` is `T: (int, object)` # Top materialization of `T: (int, Any)` is `T: (int, object)`
# Bottom materialization of `T: (int, Any)` is `T: (int, Never)` # Bottom materialization of `T: (int, Any)` is `T: (int, Never)`
static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], int)) static_assert(is_subtype_of(Bottom[T], int))
``` ```
## Generics ## Generics
@ -361,9 +404,14 @@ variable itself.
- If the type variable is contravariant, the materialization happens as per the surrounding - If the type variable is contravariant, the materialization happens as per the surrounding
variance, but the variance is flipped variance, but the variance is flipped
```toml
[environment]
python-version = "3.12"
```
```py ```py
from typing import Any, Generic, TypeVar from typing import Any, Generic, TypeVar, Never
from ty_extensions import bottom_materialization, top_materialization from ty_extensions import Bottom, Top, static_assert, is_equivalent_to
T = TypeVar("T") T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True) T_co = TypeVar("T_co", covariant=True)
@ -378,14 +426,15 @@ class GenericCovariant(Generic[T_co]):
class GenericContravariant(Generic[T_contra]): class GenericContravariant(Generic[T_contra]):
pass pass
reveal_type(top_materialization(GenericInvariant[Any])) # revealed: GenericInvariant[T_all] def _(top: Top[GenericInvariant[Any]], bottom: Bottom[GenericInvariant[Any]]):
reveal_type(bottom_materialization(GenericInvariant[Any])) # revealed: GenericInvariant[T_all] reveal_type(top) # revealed: GenericInvariant[T_all]
reveal_type(bottom) # revealed: GenericInvariant[T_all]
reveal_type(top_materialization(GenericCovariant[Any])) # revealed: GenericCovariant[object] static_assert(is_equivalent_to(Top[GenericCovariant[Any]], GenericCovariant[object]))
reveal_type(bottom_materialization(GenericCovariant[Any])) # revealed: GenericCovariant[Never] static_assert(is_equivalent_to(Bottom[GenericCovariant[Any]], GenericCovariant[Never]))
reveal_type(top_materialization(GenericContravariant[Any])) # revealed: GenericContravariant[Never] static_assert(is_equivalent_to(Top[GenericContravariant[Any]], GenericContravariant[Never]))
reveal_type(bottom_materialization(GenericContravariant[Any])) # revealed: GenericContravariant[object] static_assert(is_equivalent_to(Bottom[GenericContravariant[Any]], GenericContravariant[object]))
``` ```
Parameters in callable are contravariant, so the variance should be flipped: Parameters in callable are contravariant, so the variance should be flipped:
@ -394,24 +443,52 @@ Parameters in callable are contravariant, so the variance should be flipped:
from typing import Callable from typing import Callable
from ty_extensions import TypeOf from ty_extensions import TypeOf
def invariant(callable: Callable[[GenericInvariant[Any]], None]) -> None: type InvariantCallable = Callable[[GenericInvariant[Any]], None]
# revealed: (GenericInvariant[T_all], /) -> None type CovariantCallable = Callable[[GenericCovariant[Any]], None]
reveal_type(top_materialization(TypeOf[callable])) type ContravariantCallable = Callable[[GenericContravariant[Any]], None]
# revealed: (GenericInvariant[T_all], /) -> None def invariant(top: Top[InvariantCallable], bottom: Bottom[InvariantCallable]) -> None:
reveal_type(bottom_materialization(TypeOf[callable])) reveal_type(top) # revealed: (GenericInvariant[T_all], /) -> None
reveal_type(bottom) # revealed: (GenericInvariant[T_all], /) -> None
def covariant(callable: Callable[[GenericCovariant[Any]], None]) -> None: def covariant(top: Top[CovariantCallable], bottom: Bottom[CovariantCallable]) -> None:
# revealed: (GenericCovariant[Never], /) -> None reveal_type(top) # revealed: (GenericCovariant[Never], /) -> None
reveal_type(top_materialization(TypeOf[callable])) reveal_type(bottom) # revealed: (GenericCovariant[object], /) -> None
# revealed: (GenericCovariant[object], /) -> None def contravariant(top: Top[ContravariantCallable], bottom: Bottom[ContravariantCallable]) -> None:
reveal_type(bottom_materialization(TypeOf[callable])) reveal_type(top) # revealed: (GenericContravariant[object], /) -> None
reveal_type(bottom) # revealed: (GenericContravariant[Never], /) -> None
def contravariant(callable: Callable[[GenericContravariant[Any]], None]) -> None: ```
# revealed: (GenericContravariant[object], /) -> None
reveal_type(top_materialization(TypeOf[callable])) ## Invalid use
# revealed: (GenericContravariant[Never], /) -> None `Top[]` and `Bottom[]` are special forms that take a single argument.
reveal_type(bottom_materialization(TypeOf[callable]))
It is invalid to use them without a type argument.
```py
from ty_extensions import Bottom, Top
def _(
just_top: Top, # error: [invalid-type-form]
just_bottom: Bottom, # error: [invalid-type-form]
): ...
```
It is also invalid to use multiple arguments:
```py
def _(
top_two: Top[int, str], # error: [invalid-type-form]
bottom_two: Bottom[int, str], # error: [invalid-type-form]
): ...
```
The argument must be a type expression:
```py
def _(
top_1: Top[1], # error: [invalid-type-form]
bottom_1: Bottom[1], # error: [invalid-type-form]
): ...
``` ```

View file

@ -4143,21 +4143,6 @@ impl<'db> Type<'db> {
.into() .into()
} }
Some(KnownFunction::TopMaterialization | KnownFunction::BottomMaterialization) => {
Binding::single(
self,
Signature::new(
Parameters::new([Parameter::positional_only(Some(Name::new_static(
"type",
)))
.type_form()
.with_annotated_type(Type::any())]),
Some(Type::any()),
),
)
.into()
}
Some(KnownFunction::AssertType) => Binding::single( Some(KnownFunction::AssertType) => Binding::single(
self, self,
Signature::new( Signature::new(
@ -5741,6 +5726,8 @@ impl<'db> Type<'db> {
SpecialFormType::Optional SpecialFormType::Optional
| SpecialFormType::Not | SpecialFormType::Not
| SpecialFormType::Top
| SpecialFormType::Bottom
| SpecialFormType::TypeOf | SpecialFormType::TypeOf
| SpecialFormType::TypeIs | SpecialFormType::TypeIs
| SpecialFormType::TypeGuard | SpecialFormType::TypeGuard

View file

@ -726,18 +726,6 @@ impl<'db> Bindings<'db> {
} }
} }
Some(KnownFunction::TopMaterialization) => {
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(ty.top_materialization(db));
}
}
Some(KnownFunction::BottomMaterialization) => {
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(ty.bottom_materialization(db));
}
}
Some(KnownFunction::Len) => { Some(KnownFunction::Len) => {
if let [Some(first_arg)] = overload.parameter_types() { if let [Some(first_arg)] = overload.parameter_types() {
if let Some(len_ty) = first_arg.len(db) { if let Some(len_ty) = first_arg.len(db) {

View file

@ -192,6 +192,8 @@ impl<'db> ClassBase<'db> {
| SpecialFormType::ReadOnly | SpecialFormType::ReadOnly
| SpecialFormType::Optional | SpecialFormType::Optional
| SpecialFormType::Not | SpecialFormType::Not
| SpecialFormType::Top
| SpecialFormType::Bottom
| SpecialFormType::Intersection | SpecialFormType::Intersection
| SpecialFormType::TypeOf | SpecialFormType::TypeOf
| SpecialFormType::CallableTypeOf | SpecialFormType::CallableTypeOf

View file

@ -1168,10 +1168,6 @@ pub enum KnownFunction {
AllMembers, AllMembers,
/// `ty_extensions.has_member` /// `ty_extensions.has_member`
HasMember, HasMember,
/// `ty_extensions.top_materialization`
TopMaterialization,
/// `ty_extensions.bottom_materialization`
BottomMaterialization,
/// `ty_extensions.reveal_protocol_interface` /// `ty_extensions.reveal_protocol_interface`
RevealProtocolInterface, RevealProtocolInterface,
} }
@ -1232,8 +1228,6 @@ impl KnownFunction {
| Self::IsSingleValued | Self::IsSingleValued
| Self::IsSingleton | Self::IsSingleton
| Self::IsSubtypeOf | Self::IsSubtypeOf
| Self::TopMaterialization
| Self::BottomMaterialization
| Self::GenericContext | Self::GenericContext
| Self::DunderAllNames | Self::DunderAllNames
| Self::EnumMembers | Self::EnumMembers
@ -1569,8 +1563,6 @@ pub(crate) mod tests {
| KnownFunction::IsSingleValued | KnownFunction::IsSingleValued
| KnownFunction::IsAssignableTo | KnownFunction::IsAssignableTo
| KnownFunction::IsEquivalentTo | KnownFunction::IsEquivalentTo
| KnownFunction::TopMaterialization
| KnownFunction::BottomMaterialization
| KnownFunction::HasMember | KnownFunction::HasMember
| KnownFunction::RevealProtocolInterface | KnownFunction::RevealProtocolInterface
| KnownFunction::AllMembers => KnownModule::TyExtensions, | KnownFunction::AllMembers => KnownModule::TyExtensions,

View file

@ -10599,6 +10599,54 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
ty ty
} }
SpecialFormType::Top => {
let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice {
&*tuple.elts
} else {
std::slice::from_ref(arguments_slice)
};
let num_arguments = arguments.len();
let arg = if num_arguments == 1 {
self.infer_type_expression(&arguments[0])
} else {
for argument in arguments {
self.infer_type_expression(argument);
}
report_invalid_argument_number_to_special_form(
&self.context,
subscript,
special_form,
num_arguments,
1,
);
Type::unknown()
};
arg.top_materialization(db)
}
SpecialFormType::Bottom => {
let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice {
&*tuple.elts
} else {
std::slice::from_ref(arguments_slice)
};
let num_arguments = arguments.len();
let arg = if num_arguments == 1 {
self.infer_type_expression(&arguments[0])
} else {
for argument in arguments {
self.infer_type_expression(argument);
}
report_invalid_argument_number_to_special_form(
&self.context,
subscript,
special_form,
num_arguments,
1,
);
Type::unknown()
};
arg.bottom_materialization(db)
}
SpecialFormType::TypeOf => { SpecialFormType::TypeOf => {
let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice {
&*tuple.elts &*tuple.elts

View file

@ -77,6 +77,10 @@ pub enum SpecialFormType {
TypeOf, TypeOf,
/// The symbol `ty_extensions.CallableTypeOf` /// The symbol `ty_extensions.CallableTypeOf`
CallableTypeOf, CallableTypeOf,
/// The symbol `ty_extensions.Top`
Top,
/// The symbol `ty_extensions.Bottom`
Bottom,
/// The symbol `typing.Callable` /// The symbol `typing.Callable`
/// (which can also be found as `typing_extensions.Callable` or as `collections.abc.Callable`) /// (which can also be found as `typing_extensions.Callable` or as `collections.abc.Callable`)
Callable, Callable,
@ -151,6 +155,8 @@ impl SpecialFormType {
| Self::TypeIs | Self::TypeIs
| Self::TypeOf | Self::TypeOf
| Self::Not | Self::Not
| Self::Top
| Self::Bottom
| Self::Intersection | Self::Intersection
| Self::CallableTypeOf | Self::CallableTypeOf
| Self::Protocol // actually `_ProtocolMeta` at runtime but this is what typeshed says | Self::Protocol // actually `_ProtocolMeta` at runtime but this is what typeshed says
@ -247,6 +253,8 @@ impl SpecialFormType {
| Self::AlwaysTruthy | Self::AlwaysTruthy
| Self::AlwaysFalsy | Self::AlwaysFalsy
| Self::Not | Self::Not
| Self::Top
| Self::Bottom
| Self::Intersection | Self::Intersection
| Self::TypeOf | Self::TypeOf
| Self::CallableTypeOf => module.is_ty_extensions(), | Self::CallableTypeOf => module.is_ty_extensions(),
@ -291,6 +299,8 @@ impl SpecialFormType {
| Self::AlwaysTruthy | Self::AlwaysTruthy
| Self::AlwaysFalsy | Self::AlwaysFalsy
| Self::Not | Self::Not
| Self::Top
| Self::Bottom
| Self::Intersection | Self::Intersection
| Self::TypeOf | Self::TypeOf
| Self::CallableTypeOf | Self::CallableTypeOf
@ -352,6 +362,8 @@ impl SpecialFormType {
SpecialFormType::Intersection => "ty_extensions.Intersection", SpecialFormType::Intersection => "ty_extensions.Intersection",
SpecialFormType::TypeOf => "ty_extensions.TypeOf", SpecialFormType::TypeOf => "ty_extensions.TypeOf",
SpecialFormType::CallableTypeOf => "ty_extensions.CallableTypeOf", SpecialFormType::CallableTypeOf => "ty_extensions.CallableTypeOf",
SpecialFormType::Top => "ty_extensions.Top",
SpecialFormType::Bottom => "ty_extensions.Bottom",
SpecialFormType::Protocol => "typing.Protocol", SpecialFormType::Protocol => "typing.Protocol",
SpecialFormType::Generic => "typing.Generic", SpecialFormType::Generic => "typing.Generic",
SpecialFormType::NamedTuple => "typing.NamedTuple", SpecialFormType::NamedTuple => "typing.NamedTuple",

View file

@ -24,6 +24,12 @@ Not: _SpecialForm
Intersection: _SpecialForm Intersection: _SpecialForm
TypeOf: _SpecialForm TypeOf: _SpecialForm
CallableTypeOf: _SpecialForm CallableTypeOf: _SpecialForm
# Top[T] evaluates to the top materialization of T, a type that is a supertype
# of every materialization of T.
Top: _SpecialForm
# Bottom[T] evaluates to the bottom materialization of T, a type that is a subtype
# of every materialization of T.
Bottom: _SpecialForm
# ty treats annotations of `float` to mean `float | int`, and annotations of `complex` # ty treats annotations of `float` to mean `float | int`, and annotations of `complex`
# to mean `complex | float | int`. This is to support a typing-system special case [1]. # to mean `complex | float | int`. This is to support a typing-system special case [1].
@ -56,12 +62,6 @@ def dunder_all_names(module: Any) -> Any: ...
# List all members of an enum. # List all members of an enum.
def enum_members[E: type[Enum]](enum: E) -> tuple[str, ...]: ... def enum_members[E: type[Enum]](enum: E) -> tuple[str, ...]: ...
# Returns the type that's an upper bound of materializing the given (gradual) type.
def top_materialization(type: Any) -> Any: ...
# Returns the type that's a lower bound of materializing the given (gradual) type.
def bottom_materialization(type: Any) -> Any: ...
# Returns a tuple of all members of the given object, similar to `dir(obj)` and # Returns a tuple of all members of the given object, similar to `dir(obj)` and
# `inspect.getmembers(obj)`, with at least the following differences: # `inspect.getmembers(obj)`, with at least the following differences:
# #