[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
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
reveal_type(top_materialization(Unknown)) # revealed: object
def _(top_any: Top[Any], top_unknown: Top[Unknown]):
reveal_type(top_any) # revealed: object
reveal_type(top_unknown) # revealed: object
```
The contravariant position is replaced with `Never`.
```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.
```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
@ -53,24 +56,26 @@ The dynamic type at the top-level is replaced with `Never`.
```py
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
reveal_type(bottom_materialization(Unknown)) # revealed: Never
def _(bottom_any: Bottom[Any], bottom_unknown: Bottom[Unknown]):
reveal_type(bottom_any) # revealed: Never
reveal_type(bottom_unknown) # revealed: Never
```
The contravariant position is replaced with `object`.
```py
# revealed: (object, object, /) -> None
reveal_type(bottom_materialization(Callable[[Any, Unknown], None]))
def _(bottom_callable: Bottom[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
type variable.
```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
@ -79,30 +84,30 @@ The top / bottom (and only) materialization of any fully static type is just its
```py
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
class Answer(Enum):
NO = 0
YES = 1
reveal_type(top_materialization(int)) # revealed: int
reveal_type(bottom_materialization(int)) # revealed: int
static_assert(is_equivalent_to(Top[int], int))
static_assert(is_equivalent_to(Bottom[int], int))
reveal_type(top_materialization(Literal[1])) # revealed: Literal[1]
reveal_type(bottom_materialization(Literal[1])) # revealed: Literal[1]
static_assert(is_equivalent_to(Top[Literal[1]], Literal[1]))
static_assert(is_equivalent_to(Bottom[Literal[1]], Literal[1]))
reveal_type(top_materialization(Literal[True])) # revealed: Literal[True]
reveal_type(bottom_materialization(Literal[True])) # revealed: Literal[True]
static_assert(is_equivalent_to(Top[Literal[True]], Literal[True]))
static_assert(is_equivalent_to(Bottom[Literal[True]], Literal[True]))
reveal_type(top_materialization(Literal["abc"])) # revealed: Literal["abc"]
reveal_type(bottom_materialization(Literal["abc"])) # revealed: Literal["abc"]
static_assert(is_equivalent_to(Top[Literal["abc"]], Literal["abc"]))
static_assert(is_equivalent_to(Bottom[Literal["abc"]], Literal["abc"]))
reveal_type(top_materialization(Literal[Answer.YES])) # revealed: Literal[Answer.YES]
reveal_type(bottom_materialization(Literal[Answer.YES])) # revealed: Literal[Answer.YES]
static_assert(is_equivalent_to(Top[Literal[Answer.YES]], Literal[Answer.YES]))
static_assert(is_equivalent_to(Bottom[Literal[Answer.YES]], Literal[Answer.YES]))
reveal_type(top_materialization(int | str)) # revealed: int | str
reveal_type(bottom_materialization(int | str)) # revealed: int | str
static_assert(is_equivalent_to(Top[int | str], 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
@ -114,11 +119,17 @@ def function(x: Any) -> None: ...
class A:
def method(self, x: Any) -> None: ...
reveal_type(top_materialization(TypeOf[function])) # revealed: def function(x: Any) -> None
reveal_type(bottom_materialization(TypeOf[function])) # revealed: def function(x: Any) -> None
def _(
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(bottom_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_meth) # revealed: bound method A.method(x: Any) -> None
```
## 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
covariant position.
```toml
[environment]
python-version = "3.12"
```
```py
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:
# revealed: (Never, Never, /) -> object
reveal_type(top_materialization(TypeOf[callable]))
type C1 = Callable[[Any, Unknown], Any]
# revealed: (object, object, /) -> Never
reveal_type(bottom_materialization(TypeOf[callable]))
def _(top: Top[C1], bottom: Bottom[C1]) -> None:
reveal_type(top) # revealed: (Never, Never, /) -> object
reveal_type(bottom) # revealed: (object, object, /) -> Never
```
The parameter types in a callable inherits the contravariant position.
```py
def _(callable: Callable[[int, tuple[int | Any]], tuple[Any]]) -> None:
# revealed: (int, tuple[int], /) -> tuple[object]
reveal_type(top_materialization(TypeOf[callable]))
type C2 = Callable[[int, tuple[int | Any]], tuple[Any]]
# revealed: (int, tuple[object], /) -> Never
reveal_type(bottom_materialization(TypeOf[callable]))
def _(top: Top[C2], bottom: Bottom[C2]) -> None:
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
@ -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.
```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
reveal_type(top_materialization(TypeOf[callable]))
reveal_type(top)
# revealed: (object, (Never, /) -> object, /) -> (object, int, /) -> Never
reveal_type(bottom_materialization(TypeOf[callable]))
reveal_type(bottom)
```
## Tuple
All positions in a tuple are covariant.
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Any
from ty_extensions import Unknown, bottom_materialization, top_materialization
from typing import Any, Never
from ty_extensions import Unknown, Bottom, Top, is_equivalent_to, static_assert
reveal_type(top_materialization(tuple[Any, int])) # revealed: tuple[object, int]
reveal_type(bottom_materialization(tuple[Any, int])) # revealed: Never
static_assert(is_equivalent_to(Top[tuple[Any, int]], tuple[object, int]))
static_assert(is_equivalent_to(Bottom[tuple[Any, int]], Never))
reveal_type(top_materialization(tuple[Unknown, int])) # revealed: tuple[object, int]
reveal_type(bottom_materialization(tuple[Unknown, int])) # revealed: Never
static_assert(is_equivalent_to(Top[tuple[Unknown, int]], tuple[object, int]))
static_assert(is_equivalent_to(Bottom[tuple[Unknown, int]], Never))
reveal_type(top_materialization(tuple[Any, int, Unknown])) # revealed: tuple[object, int, object]
reveal_type(bottom_materialization(tuple[Any, int, Unknown])) # revealed: Never
static_assert(is_equivalent_to(Top[tuple[Any, int, Unknown]], tuple[object, int, object]))
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
@ -187,43 +208,59 @@ inherit the contravariant position.
from typing import Callable
from ty_extensions import TypeOf
def _(callable: Callable[[tuple[Any, int], tuple[str, Unknown]], None]) -> None:
# revealed: (Never, Never, /) -> None
reveal_type(top_materialization(TypeOf[callable]))
type C = Callable[[tuple[Any, int], tuple[str, Unknown]], None]
# revealed: (tuple[object, int], tuple[str, object], /) -> None
reveal_type(bottom_materialization(TypeOf[callable]))
def _(top: Top[C], bottom: Bottom[C]) -> None:
reveal_type(top) # revealed: (Never, Never, /) -> None
reveal_type(bottom) # revealed: (tuple[object, int], tuple[str, object], /) -> None
```
And, similarly for an invariant position.
```py
reveal_type(top_materialization(list[tuple[Any, int]])) # revealed: list[tuple[T_all, int]]
reveal_type(bottom_materialization(list[tuple[Any, int]])) # revealed: list[tuple[T_all, int]]
type LTAnyInt = list[tuple[Any, 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]]
reveal_type(bottom_materialization(list[tuple[str, Unknown]])) # revealed: list[tuple[str, T_all]]
def _(
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(bottom_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_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
All positions in a union are covariant.
```toml
[environment]
python-version = "3.12"
```
```py
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
reveal_type(bottom_materialization(Any | int)) # revealed: int
static_assert(is_equivalent_to(Top[Any | int], object))
static_assert(is_equivalent_to(Bottom[Any | int], int))
reveal_type(top_materialization(Unknown | int)) # revealed: object
reveal_type(bottom_materialization(Unknown | int)) # revealed: int
static_assert(is_equivalent_to(Top[Unknown | int], object))
static_assert(is_equivalent_to(Bottom[Unknown | int], int))
reveal_type(top_materialization(int | str | Any)) # revealed: object
reveal_type(bottom_materialization(int | str | Any)) # revealed: int | str
static_assert(is_equivalent_to(Top[int | str | Any], object))
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
@ -234,24 +271,29 @@ from typing import Callable
from ty_extensions import TypeOf
def _(callable: Callable[[Any | int, str | Unknown], None]) -> None:
# revealed: (int, str, /) -> None
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (object, object, /) -> None
reveal_type(bottom_materialization(TypeOf[callable]))
static_assert(is_equivalent_to(Top[TypeOf[callable]], Callable[[int, str], None]))
static_assert(is_equivalent_to(Bottom[TypeOf[callable]], Callable[[object, object], None]))
```
And, similarly for an invariant position.
```py
reveal_type(top_materialization(list[Any | int])) # revealed: list[T_all | int]
reveal_type(bottom_materialization(list[Any | int])) # revealed: list[T_all | int]
def _(
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(bottom_materialization(list[str | Unknown])) # revealed: list[str | T_all]
reveal_type(top_su) # 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(bottom_materialization(list[Any | int | Unknown])) # revealed: list[T_all | int]
reveal_type(top_aiu) # revealed: list[T_all | int]
reveal_type(bottom_aiu) # revealed: list[T_all | int]
```
## Intersection
@ -260,24 +302,26 @@ All positions in an intersection are covariant.
```py
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
reveal_type(bottom_materialization(Intersection[Any, int])) # revealed: Never
static_assert(is_equivalent_to(Top[Intersection[Any, int]], int))
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
# revealed: tuple[str, object]
reveal_type(top_materialization(Intersection[Any | int, tuple[str, Unknown]]))
# revealed: Never
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str, Unknown]]))
static_assert(is_equivalent_to(Top[Intersection[Any | int, tuple[str, Unknown]]], tuple[str, object]))
static_assert(is_equivalent_to(Bottom[Intersection[Any | int, tuple[str, Unknown]]], Never))
class Foo: ...
# revealed: Foo & tuple[str]
reveal_type(bottom_materialization(Intersection[Any | Foo, tuple[str]]))
static_assert(is_equivalent_to(Bottom[Intersection[Any | Foo, tuple[str]]], Intersection[Foo, tuple[str]]))
reveal_type(top_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
reveal_type(bottom_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
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]
```
## Negation (via `Not`)
@ -286,38 +330,44 @@ All positions in a negation are contravariant.
```py
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
reveal_type(top_materialization(Not[Any])) # revealed: object
reveal_type(bottom_materialization(Not[Any])) # revealed: Never
static_assert(is_equivalent_to(Top[Not[Any]], object))
static_assert(is_equivalent_to(Bottom[Not[Any]], Never))
# tuple[Any, int] is in a contravariant position, so the
# top materialization is Never and the negation of it
# revealed: object
reveal_type(top_materialization(Not[tuple[Any, int]]))
# revealed: ~tuple[object, int]
reveal_type(bottom_materialization(Not[tuple[Any, int]]))
static_assert(is_equivalent_to(Top[Not[tuple[Any, int]]], object))
static_assert(is_equivalent_to(Bottom[Not[tuple[Any, int]]], Not[tuple[object, int]]))
```
## `type`
```toml
[environment]
python-version = "3.12"
```
```py
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
reveal_type(bottom_materialization(type[Any])) # revealed: Never
static_assert(is_equivalent_to(Top[type[Any]], type))
static_assert(is_equivalent_to(Bottom[type[Any]], Never))
reveal_type(top_materialization(type[Unknown])) # revealed: type
reveal_type(bottom_materialization(type[Unknown])) # revealed: Never
static_assert(is_equivalent_to(Top[type[Unknown]], type))
static_assert(is_equivalent_to(Bottom[type[Unknown]], Never))
reveal_type(top_materialization(type[int | Any])) # revealed: type
reveal_type(bottom_materialization(type[int | Any])) # revealed: type[int]
static_assert(is_equivalent_to(Top[type[int | Any]], type))
static_assert(is_equivalent_to(Bottom[type[int | Any]], type[int]))
# Here, `T` has an upper bound of `type`
reveal_type(top_materialization(list[type[Any]])) # revealed: list[T_all]
reveal_type(bottom_materialization(list[type[Any]])) # revealed: list[T_all]
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]
```
## Type variables
@ -329,26 +379,19 @@ python-version = "3.12"
```py
from typing import Any, Never, TypeVar
from ty_extensions import (
TypeOf,
Unknown,
bottom_materialization,
top_materialization,
static_assert,
is_subtype_of,
)
from ty_extensions import Unknown, Bottom, Top, static_assert, is_subtype_of
def bounded_by_gradual[T: Any](t: T) -> None:
# Top materialization of `T: Any` is `T: object`
# 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:
# Top materialization of `T: (int, Any)` is `T: (int, object)`
# 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
@ -361,9 +404,14 @@ variable itself.
- If the type variable is contravariant, the materialization happens as per the surrounding
variance, but the variance is flipped
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Any, Generic, TypeVar
from ty_extensions import bottom_materialization, top_materialization
from typing import Any, Generic, TypeVar, Never
from ty_extensions import Bottom, Top, static_assert, is_equivalent_to
T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True)
@ -378,14 +426,15 @@ class GenericCovariant(Generic[T_co]):
class GenericContravariant(Generic[T_contra]):
pass
reveal_type(top_materialization(GenericInvariant[Any])) # revealed: GenericInvariant[T_all]
reveal_type(bottom_materialization(GenericInvariant[Any])) # revealed: GenericInvariant[T_all]
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_materialization(GenericCovariant[Any])) # revealed: GenericCovariant[object]
reveal_type(bottom_materialization(GenericCovariant[Any])) # revealed: GenericCovariant[Never]
static_assert(is_equivalent_to(Top[GenericCovariant[Any]], GenericCovariant[object]))
static_assert(is_equivalent_to(Bottom[GenericCovariant[Any]], GenericCovariant[Never]))
reveal_type(top_materialization(GenericContravariant[Any])) # revealed: GenericContravariant[Never]
reveal_type(bottom_materialization(GenericContravariant[Any])) # revealed: GenericContravariant[object]
static_assert(is_equivalent_to(Top[GenericContravariant[Any]], GenericContravariant[Never]))
static_assert(is_equivalent_to(Bottom[GenericContravariant[Any]], GenericContravariant[object]))
```
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 ty_extensions import TypeOf
def invariant(callable: Callable[[GenericInvariant[Any]], None]) -> None:
# revealed: (GenericInvariant[T_all], /) -> None
reveal_type(top_materialization(TypeOf[callable]))
type InvariantCallable = Callable[[GenericInvariant[Any]], None]
type CovariantCallable = Callable[[GenericCovariant[Any]], None]
type ContravariantCallable = Callable[[GenericContravariant[Any]], None]
# revealed: (GenericInvariant[T_all], /) -> None
reveal_type(bottom_materialization(TypeOf[callable]))
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
def covariant(callable: Callable[[GenericCovariant[Any]], None]) -> None:
# revealed: (GenericCovariant[Never], /) -> None
reveal_type(top_materialization(TypeOf[callable]))
def covariant(top: Top[CovariantCallable], bottom: Bottom[CovariantCallable]) -> None:
reveal_type(top) # revealed: (GenericCovariant[Never], /) -> None
reveal_type(bottom) # revealed: (GenericCovariant[object], /) -> None
# revealed: (GenericCovariant[object], /) -> None
reveal_type(bottom_materialization(TypeOf[callable]))
def contravariant(callable: Callable[[GenericContravariant[Any]], None]) -> None:
# revealed: (GenericContravariant[object], /) -> None
reveal_type(top_materialization(TypeOf[callable]))
# revealed: (GenericContravariant[Never], /) -> None
reveal_type(bottom_materialization(TypeOf[callable]))
def contravariant(top: Top[ContravariantCallable], bottom: Bottom[ContravariantCallable]) -> None:
reveal_type(top) # revealed: (GenericContravariant[object], /) -> None
reveal_type(bottom) # revealed: (GenericContravariant[Never], /) -> None
```
## Invalid use
`Top[]` and `Bottom[]` are special forms that take a single argument.
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()
}
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(
self,
Signature::new(
@ -5741,6 +5726,8 @@ impl<'db> Type<'db> {
SpecialFormType::Optional
| SpecialFormType::Not
| SpecialFormType::Top
| SpecialFormType::Bottom
| SpecialFormType::TypeOf
| SpecialFormType::TypeIs
| 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) => {
if let [Some(first_arg)] = overload.parameter_types() {
if let Some(len_ty) = first_arg.len(db) {

View file

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

View file

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

View file

@ -10599,6 +10599,54 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
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 => {
let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice {
&*tuple.elts

View file

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

View file

@ -24,6 +24,12 @@ Not: _SpecialForm
Intersection: _SpecialForm
TypeOf: _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`
# 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.
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
# `inspect.getmembers(obj)`, with at least the following differences:
#