[ty] Eagerly evaluate types.UnionType elements as type expressions (#21531)
## Summary Eagerly evaluate the elements of a PEP 604 union in value position (e.g. `IntOrStr = int | str`) as type expressions and store the result (the corresponding `Type::Union` if all elements are valid type expressions, or the first encountered `InvalidTypeExpressionError`) on the `UnionTypeInstance`, such that the `Type::Union(…)` does not need to be recomputed every time the implicit type alias is used in a type annotation. This might lead to performance improvements for large unions, but is also necessary for correctness, because the elements of the union might refer to type variables that need to be looked up in the scope of the type alias, not at the usage site. ## Test Plan New Markdown tests
This commit is contained in:
@@ -191,13 +191,13 @@ def _(
|
||||
reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes)
|
||||
reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int
|
||||
# TODO should be Unknown | int
|
||||
reveal_type(type_var_or_int) # revealed: T@_ | int
|
||||
reveal_type(type_var_or_int) # revealed: typing.TypeVar | int
|
||||
# TODO should be int | Unknown
|
||||
reveal_type(int_or_type_var) # revealed: int | T@_
|
||||
reveal_type(int_or_type_var) # revealed: int | typing.TypeVar
|
||||
# TODO should be Unknown | None
|
||||
reveal_type(type_var_or_none) # revealed: T@_ | None
|
||||
reveal_type(type_var_or_none) # revealed: typing.TypeVar | None
|
||||
# TODO should be None | Unknown
|
||||
reveal_type(none_or_type_var) # revealed: None | T@_
|
||||
reveal_type(none_or_type_var) # revealed: None | typing.TypeVar
|
||||
```
|
||||
|
||||
If a type is unioned with itself in a value expression, the result is just that type. No
|
||||
|
||||
@@ -159,19 +159,43 @@ IntOrStr = Union[int, str]
|
||||
reveal_type(IntOrStr) # revealed: types.UnionType
|
||||
|
||||
def _(x: int | str | bytes | memoryview | range):
|
||||
# TODO: no error
|
||||
# error: [invalid-argument-type]
|
||||
if isinstance(x, IntOrStr):
|
||||
# TODO: Should be `int | str`
|
||||
reveal_type(x) # revealed: int | str | bytes | memoryview[int] | range
|
||||
# TODO: no error
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(x) # revealed: int | str
|
||||
elif isinstance(x, Union[bytes, memoryview]):
|
||||
# TODO: Should be `bytes | memoryview[int]`
|
||||
reveal_type(x) # revealed: int | str | bytes | memoryview[int] | range
|
||||
reveal_type(x) # revealed: bytes | memoryview[int]
|
||||
else:
|
||||
# TODO: Should be `range`
|
||||
reveal_type(x) # revealed: int | str | bytes | memoryview[int] | range
|
||||
reveal_type(x) # revealed: range
|
||||
|
||||
def _(x: int | str | None):
|
||||
if isinstance(x, Union[int, None]):
|
||||
reveal_type(x) # revealed: int | None
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
|
||||
ListStrOrInt = Union[list[str], int]
|
||||
|
||||
def _(x: dict[int, str] | ListStrOrInt):
|
||||
# TODO: this should ideally be an error
|
||||
if isinstance(x, ListStrOrInt):
|
||||
# TODO: this should not be narrowed
|
||||
reveal_type(x) # revealed: list[str] | int
|
||||
|
||||
# TODO: this should ideally be an error
|
||||
if isinstance(x, Union[list[str], int]):
|
||||
# TODO: this should not be narrowed
|
||||
reveal_type(x) # revealed: list[str] | int
|
||||
```
|
||||
|
||||
## `Optional` as `classinfo`
|
||||
|
||||
```py
|
||||
from typing import Optional
|
||||
|
||||
def _(x: int | str | None):
|
||||
if isinstance(x, Optional[int]):
|
||||
reveal_type(x) # revealed: int | None
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
## `classinfo` is a `typing.py` special form
|
||||
@@ -289,6 +313,23 @@ def _(flag: bool):
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
```
|
||||
|
||||
## Generic aliases are not supported as second argument
|
||||
|
||||
The `classinfo` argument cannot be a generic alias:
|
||||
|
||||
```py
|
||||
def _(x: list[str] | list[int] | list[bytes]):
|
||||
# TODO: Ideally, this would be an error (requires https://github.com/astral-sh/ty/issues/116)
|
||||
if isinstance(x, list[int]):
|
||||
# No narrowing here:
|
||||
reveal_type(x) # revealed: list[str] | list[int] | list[bytes]
|
||||
|
||||
# error: [invalid-argument-type] "Invalid second argument to `isinstance`"
|
||||
if isinstance(x, list[int] | list[str]):
|
||||
# No narrowing here:
|
||||
reveal_type(x) # revealed: list[str] | list[int] | list[bytes]
|
||||
```
|
||||
|
||||
## `type[]` types are narrowed as well as class-literal types
|
||||
|
||||
```py
|
||||
|
||||
@@ -212,19 +212,12 @@ IntOrStr = Union[int, str]
|
||||
reveal_type(IntOrStr) # revealed: types.UnionType
|
||||
|
||||
def f(x: type[int | str | bytes | range]):
|
||||
# TODO: No error
|
||||
# error: [invalid-argument-type]
|
||||
if issubclass(x, IntOrStr):
|
||||
# TODO: Should be `type[int] | type[str]`
|
||||
reveal_type(x) # revealed: type[int] | type[str] | type[bytes] | <class 'range'>
|
||||
# TODO: No error
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(x) # revealed: type[int] | type[str]
|
||||
elif issubclass(x, Union[bytes, memoryview]):
|
||||
# TODO: Should be `type[bytes]`
|
||||
reveal_type(x) # revealed: type[int] | type[str] | type[bytes] | <class 'range'>
|
||||
reveal_type(x) # revealed: type[bytes]
|
||||
else:
|
||||
# TODO: Should be `<class 'range'>`
|
||||
reveal_type(x) # revealed: type[int] | type[str] | type[bytes] | <class 'range'>
|
||||
reveal_type(x) # revealed: <class 'range'>
|
||||
```
|
||||
|
||||
## Special cases
|
||||
|
||||
Reference in New Issue
Block a user