mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 20:24:27 +00:00
[ty] Implicit type aliases: Support for PEP 604 unions (#21195)
## Summary
Add support for implicit type aliases that use PEP 604 unions:
```py
IntOrStr = int | str
reveal_type(IntOrStr) # UnionType
def _(int_or_str: IntOrStr):
reveal_type(int_or_str) # int | str
```
## Typing conformance
The changes are either removed false positives, or new diagnostics due
to known limitations unrelated to this PR.
## Ecosystem impact
Spot checked, a mix of true positives and known limitations.
## Test Plan
New Markdown tests.
This commit is contained in:
parent
fe4ee81b97
commit
1fe958c694
13 changed files with 334 additions and 36 deletions
|
|
@ -72,9 +72,6 @@ def f(x: Union) -> None:
|
|||
|
||||
## Implicit type aliases using new-style unions
|
||||
|
||||
We don't recognize these as type aliases yet, but we also don't emit false-positive diagnostics if
|
||||
you use them in type expressions:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
|
|
@ -84,5 +81,5 @@ python-version = "3.10"
|
|||
X = int | str
|
||||
|
||||
def f(y: X):
|
||||
reveal_type(y) # revealed: @Todo(Support for `types.UnionType` instances in type expressions)
|
||||
reveal_type(y) # revealed: int | str
|
||||
```
|
||||
|
|
|
|||
|
|
@ -17,6 +17,209 @@ def f(x: MyInt):
|
|||
f(1)
|
||||
```
|
||||
|
||||
## None
|
||||
|
||||
```py
|
||||
MyNone = None
|
||||
|
||||
# TODO: this should not be an error
|
||||
# error: [invalid-type-form] "Variable of type `None` is not allowed in a type expression"
|
||||
def g(x: MyNone):
|
||||
# TODO: this should be `None`
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
g(None)
|
||||
```
|
||||
|
||||
## Unions
|
||||
|
||||
We also support unions in type aliases:
|
||||
|
||||
```py
|
||||
from typing_extensions import Any, Never
|
||||
from ty_extensions import Unknown
|
||||
|
||||
IntOrStr = int | str
|
||||
IntOrStrOrBytes1 = int | str | bytes
|
||||
IntOrStrOrBytes2 = (int | str) | bytes
|
||||
IntOrStrOrBytes3 = int | (str | bytes)
|
||||
IntOrStrOrBytes4 = IntOrStr | bytes
|
||||
BytesOrIntOrStr = bytes | IntOrStr
|
||||
IntOrNone = int | None
|
||||
NoneOrInt = None | int
|
||||
IntOrStrOrNone = IntOrStr | None
|
||||
NoneOrIntOrStr = None | IntOrStr
|
||||
IntOrAny = int | Any
|
||||
AnyOrInt = Any | int
|
||||
NoneOrAny = None | Any
|
||||
AnyOrNone = Any | None
|
||||
NeverOrAny = Never | Any
|
||||
AnyOrNever = Any | Never
|
||||
UnknownOrInt = Unknown | int
|
||||
IntOrUnknown = int | Unknown
|
||||
|
||||
reveal_type(IntOrStr) # revealed: UnionType
|
||||
reveal_type(IntOrStrOrBytes1) # revealed: UnionType
|
||||
reveal_type(IntOrStrOrBytes2) # revealed: UnionType
|
||||
reveal_type(IntOrStrOrBytes3) # revealed: UnionType
|
||||
reveal_type(IntOrStrOrBytes4) # revealed: UnionType
|
||||
reveal_type(BytesOrIntOrStr) # revealed: UnionType
|
||||
reveal_type(IntOrNone) # revealed: UnionType
|
||||
reveal_type(NoneOrInt) # revealed: UnionType
|
||||
reveal_type(IntOrStrOrNone) # revealed: UnionType
|
||||
reveal_type(NoneOrIntOrStr) # revealed: UnionType
|
||||
reveal_type(IntOrAny) # revealed: UnionType
|
||||
reveal_type(AnyOrInt) # revealed: UnionType
|
||||
reveal_type(NoneOrAny) # revealed: UnionType
|
||||
reveal_type(AnyOrNone) # revealed: UnionType
|
||||
reveal_type(NeverOrAny) # revealed: UnionType
|
||||
reveal_type(AnyOrNever) # revealed: UnionType
|
||||
reveal_type(UnknownOrInt) # revealed: UnionType
|
||||
reveal_type(IntOrUnknown) # revealed: UnionType
|
||||
|
||||
def _(
|
||||
int_or_str: IntOrStr,
|
||||
int_or_str_or_bytes1: IntOrStrOrBytes1,
|
||||
int_or_str_or_bytes2: IntOrStrOrBytes2,
|
||||
int_or_str_or_bytes3: IntOrStrOrBytes3,
|
||||
int_or_str_or_bytes4: IntOrStrOrBytes4,
|
||||
bytes_or_int_or_str: BytesOrIntOrStr,
|
||||
int_or_none: IntOrNone,
|
||||
none_or_int: NoneOrInt,
|
||||
int_or_str_or_none: IntOrStrOrNone,
|
||||
none_or_int_or_str: NoneOrIntOrStr,
|
||||
int_or_any: IntOrAny,
|
||||
any_or_int: AnyOrInt,
|
||||
none_or_any: NoneOrAny,
|
||||
any_or_none: AnyOrNone,
|
||||
never_or_any: NeverOrAny,
|
||||
any_or_never: AnyOrNever,
|
||||
unknown_or_int: UnknownOrInt,
|
||||
int_or_unknown: IntOrUnknown,
|
||||
):
|
||||
reveal_type(int_or_str) # revealed: int | str
|
||||
reveal_type(int_or_str_or_bytes1) # revealed: int | str | bytes
|
||||
reveal_type(int_or_str_or_bytes2) # revealed: int | str | bytes
|
||||
reveal_type(int_or_str_or_bytes3) # revealed: int | str | bytes
|
||||
reveal_type(int_or_str_or_bytes4) # revealed: int | str | bytes
|
||||
reveal_type(bytes_or_int_or_str) # revealed: bytes | int | str
|
||||
reveal_type(int_or_none) # revealed: int | None
|
||||
reveal_type(none_or_int) # revealed: None | int
|
||||
reveal_type(int_or_str_or_none) # revealed: int | str | None
|
||||
reveal_type(none_or_int_or_str) # revealed: None | int | str
|
||||
reveal_type(int_or_any) # revealed: int | Any
|
||||
reveal_type(any_or_int) # revealed: Any | int
|
||||
reveal_type(none_or_any) # revealed: None | Any
|
||||
reveal_type(any_or_none) # revealed: Any | None
|
||||
reveal_type(never_or_any) # revealed: Any
|
||||
reveal_type(any_or_never) # revealed: Any
|
||||
reveal_type(unknown_or_int) # revealed: Unknown | int
|
||||
reveal_type(int_or_unknown) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
If a type is unioned with itself in a value expression, the result is just that type. No
|
||||
`types.UnionType` instance is created:
|
||||
|
||||
```py
|
||||
IntOrInt = int | int
|
||||
ListOfIntOrListOfInt = list[int] | list[int]
|
||||
|
||||
reveal_type(IntOrInt) # revealed: <class 'int'>
|
||||
reveal_type(ListOfIntOrListOfInt) # revealed: <class 'list[int]'>
|
||||
|
||||
def _(int_or_int: IntOrInt, list_of_int_or_list_of_int: ListOfIntOrListOfInt):
|
||||
reveal_type(int_or_int) # revealed: int
|
||||
reveal_type(list_of_int_or_list_of_int) # revealed: list[int]
|
||||
```
|
||||
|
||||
`NoneType` has no special or-operator behavior, so this is an error:
|
||||
|
||||
```py
|
||||
None | None # error: [unsupported-operator] "Operator `|` is unsupported between objects of type `None` and `None`"
|
||||
```
|
||||
|
||||
When constructing something non-sensical like `int | 1`, we could ideally emit a diagnostic for the
|
||||
expression itself, as it leads to a `TypeError` at runtime. No other type checker supports this, so
|
||||
for now we only emit an error when it is used in a type expression:
|
||||
|
||||
```py
|
||||
IntOrOne = int | 1
|
||||
|
||||
# error: [invalid-type-form] "Variable of type `Literal[1]` is not allowed in a type expression"
|
||||
def _(int_or_one: IntOrOne):
|
||||
reveal_type(int_or_one) # revealed: Unknown
|
||||
```
|
||||
|
||||
If you were to somehow get hold of an opaque instance of `types.UnionType`, that could not be used
|
||||
as a type expression:
|
||||
|
||||
```py
|
||||
from types import UnionType
|
||||
|
||||
def f(SomeUnionType: UnionType):
|
||||
# error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression"
|
||||
some_union: SomeUnionType
|
||||
|
||||
f(int | str)
|
||||
```
|
||||
|
||||
## Generic types
|
||||
|
||||
Implicit type aliases can also refer to generic types:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
MyList = list[T]
|
||||
|
||||
def _(my_list: MyList[int]):
|
||||
# TODO: This should be `list[int]`
|
||||
reveal_type(my_list) # revealed: @Todo(unknown type subscript)
|
||||
|
||||
ListOrTuple = list[T] | tuple[T, ...]
|
||||
|
||||
reveal_type(ListOrTuple) # revealed: UnionType
|
||||
|
||||
def _(list_or_tuple: ListOrTuple[int]):
|
||||
reveal_type(list_or_tuple) # revealed: @Todo(Generic specialization of types.UnionType)
|
||||
```
|
||||
|
||||
## Stringified annotations?
|
||||
|
||||
From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html):
|
||||
|
||||
> Type aliases may be as complex as type hints in annotations – anything that is acceptable as a
|
||||
> type hint is acceptable in a type alias
|
||||
|
||||
However, no other type checker seems to support stringified annotations in implicit type aliases. We
|
||||
currently also do not support them:
|
||||
|
||||
```py
|
||||
AliasForStr = "str"
|
||||
|
||||
# error: [invalid-type-form] "Variable of type `Literal["str"]` is not allowed in a type expression"
|
||||
def _(s: AliasForStr):
|
||||
reveal_type(s) # revealed: Unknown
|
||||
|
||||
IntOrStr = int | "str"
|
||||
|
||||
# error: [invalid-type-form] "Variable of type `Literal["str"]` is not allowed in a type expression"
|
||||
def _(int_or_str: IntOrStr):
|
||||
reveal_type(int_or_str) # revealed: Unknown
|
||||
```
|
||||
|
||||
We *do* support stringified annotations if they appear in a position where a type expression is
|
||||
syntactically expected:
|
||||
|
||||
```py
|
||||
ListOfInts = list["int"]
|
||||
|
||||
def _(list_of_ints: ListOfInts):
|
||||
reveal_type(list_of_ints) # revealed: list[int]
|
||||
```
|
||||
|
||||
## Recursive
|
||||
|
||||
### Old union syntax
|
||||
|
|
|
|||
|
|
@ -291,6 +291,20 @@ class Foo(x): ...
|
|||
reveal_mro(Foo) # revealed: (<class 'Foo'>, Unknown, <class 'object'>)
|
||||
```
|
||||
|
||||
## `UnionType` instances are now allowed as a base
|
||||
|
||||
This is not legal:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
EitherOr = A | B
|
||||
|
||||
# error: [invalid-base] "Invalid class base with type `UnionType`"
|
||||
class Foo(EitherOr): ...
|
||||
```
|
||||
|
||||
## `__bases__` is a union of a dynamic type and valid bases
|
||||
|
||||
If a dynamic type such as `Any` or `Unknown` is one of the elements in the union, and all other
|
||||
|
|
|
|||
|
|
@ -146,13 +146,11 @@ def _(flag: bool):
|
|||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
# error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `Literal["a"]"
|
||||
if isinstance(x, "a"):
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
# error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `Literal["int"]"
|
||||
if isinstance(x, "int"):
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -214,8 +214,7 @@ def flag() -> bool:
|
|||
|
||||
t = int if flag() else str
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
# error: [invalid-argument-type] "Argument to function `issubclass` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `Literal["str"]"
|
||||
if issubclass(t, "str"):
|
||||
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue