mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 12:16:43 +00:00
[ty] support subscripting typing.Literal with a type alias (#21207)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Fixes https://github.com/astral-sh/ty/issues/1368 ## Summary Add support for patterns like this, where a type alias to a literal type (or union of literal types) is used to subscript `typing.Literal`: ```py type MyAlias = Literal[1] def _(x: Literal[MyAlias]): ... ``` This shows up in the ecosystem report for PEP 613 type alias support. One interesting case is an alias to `bool` or an enum type. `bool` is an equivalent type to `Literal[True, False]`, which is a union of literal types. Similarly an enum type `E` is also equivalent to a union of its member literal types. Since (for explicit type aliases) we infer the RHS directly as a type expression, this makes it difficult for us to distinguish between `bool` and `Literal[True, False]`, so we allow either one to (or an alias to either one) to appear inside `Literal`, where other type checkers allow only the latter. I think for implicit type aliases it may be simpler to support only types derived from actually subscripting `typing.Literal`, though, so I didn't make a TODO-comment commitment here. ## Test Plan Added mdtests, including TODO-filled tests for PEP 613 and implicit type aliases. ### Conformance suite All changes here are positive -- we now emit errors on lines that should be errors. This is a side effect of the new implementation, not the primary purpose of this PR, but it's still a positive change. ### Ecosystem Eliminates one ecosystem false positive, where a PEP 695 type alias for a union of literal types is used to subscript `typing.Literal`.
This commit is contained in:
parent
566d1d6497
commit
c32234cf0d
4 changed files with 251 additions and 24 deletions
|
|
@ -39,6 +39,8 @@ def f():
|
|||
reveal_type(a7) # revealed: None
|
||||
reveal_type(a8) # revealed: Literal[1]
|
||||
reveal_type(b1) # revealed: Literal[Color.RED]
|
||||
# TODO should be `Literal[MissingT.MISSING]`
|
||||
reveal_type(b2) # revealed: @Todo(functional `Enum` syntax)
|
||||
|
||||
# error: [invalid-type-form]
|
||||
invalid1: Literal[3 + 4]
|
||||
|
|
@ -66,6 +68,208 @@ a_list: list[int] = [1, 2, 3]
|
|||
invalid6: Literal[a_list[0]]
|
||||
```
|
||||
|
||||
## Parameterizing with a type alias
|
||||
|
||||
`typing.Literal` can also be parameterized with a type alias for any literal type or union of
|
||||
literal types.
|
||||
|
||||
### PEP 695 type alias
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from enum import Enum
|
||||
|
||||
import mod
|
||||
|
||||
class E(Enum):
|
||||
A = 1
|
||||
B = 2
|
||||
|
||||
type SingleInt = Literal[1]
|
||||
type SingleStr = Literal["foo"]
|
||||
type SingleBytes = Literal[b"bar"]
|
||||
type SingleBool = Literal[True]
|
||||
type SingleNone = Literal[None]
|
||||
type SingleEnum = Literal[E.A]
|
||||
type UnionLiterals = Literal[1, "foo", b"bar", True, None, E.A]
|
||||
# We support this because it is an equivalent type to the following union of literals, but maybe
|
||||
# we should not, because it doesn't use `Literal` form? Other type checkers do not.
|
||||
type AnEnum1 = E
|
||||
type AnEnum2 = Literal[E.A, E.B]
|
||||
# Similarly, we support this because it is equivalent to `Literal[True, False]`.
|
||||
type Bool1 = bool
|
||||
type Bool2 = Literal[True, False]
|
||||
|
||||
def _(
|
||||
single_int: Literal[SingleInt],
|
||||
single_str: Literal[SingleStr],
|
||||
single_bytes: Literal[SingleBytes],
|
||||
single_bool: Literal[SingleBool],
|
||||
single_none: Literal[SingleNone],
|
||||
single_enum: Literal[SingleEnum],
|
||||
union_literals: Literal[UnionLiterals],
|
||||
an_enum1: Literal[AnEnum1],
|
||||
an_enum2: Literal[AnEnum2],
|
||||
bool1: Literal[Bool1],
|
||||
bool2: Literal[Bool2],
|
||||
multiple: Literal[SingleInt, SingleStr, SingleEnum],
|
||||
single_int_other_module: Literal[mod.SingleInt],
|
||||
):
|
||||
reveal_type(single_int) # revealed: Literal[1]
|
||||
reveal_type(single_str) # revealed: Literal["foo"]
|
||||
reveal_type(single_bytes) # revealed: Literal[b"bar"]
|
||||
reveal_type(single_bool) # revealed: Literal[True]
|
||||
reveal_type(single_none) # revealed: None
|
||||
reveal_type(single_enum) # revealed: Literal[E.A]
|
||||
reveal_type(union_literals) # revealed: Literal[1, "foo", b"bar", True, E.A] | None
|
||||
reveal_type(an_enum1) # revealed: E
|
||||
reveal_type(an_enum2) # revealed: E
|
||||
reveal_type(bool1) # revealed: bool
|
||||
reveal_type(bool2) # revealed: bool
|
||||
reveal_type(multiple) # revealed: Literal[1, "foo", E.A]
|
||||
reveal_type(single_int_other_module) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
type SingleInt = Literal[2]
|
||||
```
|
||||
|
||||
### PEP 613 type alias
|
||||
|
||||
```py
|
||||
from typing import Literal, TypeAlias
|
||||
from enum import Enum
|
||||
|
||||
class E(Enum):
|
||||
A = 1
|
||||
B = 2
|
||||
|
||||
SingleInt: TypeAlias = Literal[1]
|
||||
SingleStr: TypeAlias = Literal["foo"]
|
||||
SingleBytes: TypeAlias = Literal[b"bar"]
|
||||
SingleBool: TypeAlias = Literal[True]
|
||||
SingleNone: TypeAlias = Literal[None]
|
||||
SingleEnum: TypeAlias = Literal[E.A]
|
||||
UnionLiterals: TypeAlias = Literal[1, "foo", b"bar", True, None, E.A]
|
||||
AnEnum1: TypeAlias = E
|
||||
AnEnum2: TypeAlias = Literal[E.A, E.B]
|
||||
Bool1: TypeAlias = bool
|
||||
Bool2: TypeAlias = Literal[True, False]
|
||||
|
||||
def _(
|
||||
single_int: Literal[SingleInt],
|
||||
single_str: Literal[SingleStr],
|
||||
single_bytes: Literal[SingleBytes],
|
||||
single_bool: Literal[SingleBool],
|
||||
single_none: Literal[SingleNone],
|
||||
single_enum: Literal[SingleEnum],
|
||||
union_literals: Literal[UnionLiterals],
|
||||
# Could also not error
|
||||
an_enum1: Literal[AnEnum1], # error: [invalid-type-form]
|
||||
an_enum2: Literal[AnEnum2],
|
||||
# Could also not error
|
||||
bool1: Literal[Bool1], # error: [invalid-type-form]
|
||||
bool2: Literal[Bool2],
|
||||
multiple: Literal[SingleInt, SingleStr, SingleEnum],
|
||||
):
|
||||
# TODO should be `Literal[1]`
|
||||
reveal_type(single_int) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal["foo"]`
|
||||
reveal_type(single_str) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal[b"bar"]`
|
||||
reveal_type(single_bytes) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal[True]`
|
||||
reveal_type(single_bool) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `None`
|
||||
reveal_type(single_none) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal[E.A]`
|
||||
reveal_type(single_enum) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal[1, "foo", b"bar", True, E.A] | None`
|
||||
reveal_type(union_literals) # revealed: @Todo(Inference of subscript on special form)
|
||||
# Could also be `E`
|
||||
reveal_type(an_enum1) # revealed: Unknown
|
||||
# TODO should be `E`
|
||||
reveal_type(an_enum2) # revealed: @Todo(Inference of subscript on special form)
|
||||
# Could also be `bool`
|
||||
reveal_type(bool1) # revealed: Unknown
|
||||
# TODO should be `bool`
|
||||
reveal_type(bool2) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal[1, "foo", E.A]`
|
||||
reveal_type(multiple) # revealed: @Todo(Inference of subscript on special form)
|
||||
```
|
||||
|
||||
### Implicit type alias
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from enum import Enum
|
||||
|
||||
class E(Enum):
|
||||
A = 1
|
||||
B = 2
|
||||
|
||||
SingleInt = Literal[1]
|
||||
SingleStr = Literal["foo"]
|
||||
SingleBytes = Literal[b"bar"]
|
||||
SingleBool = Literal[True]
|
||||
SingleNone = Literal[None]
|
||||
SingleEnum = Literal[E.A]
|
||||
UnionLiterals = Literal[1, "foo", b"bar", True, None, E.A]
|
||||
# For implicit type aliases, we may not want to support this. It's simpler not to, and no other
|
||||
# type checker does.
|
||||
AnEnum1 = E
|
||||
AnEnum2 = Literal[E.A, E.B]
|
||||
# For implicit type aliases, we may not want to support this.
|
||||
Bool1 = bool
|
||||
Bool2 = Literal[True, False]
|
||||
|
||||
def _(
|
||||
single_int: Literal[SingleInt],
|
||||
single_str: Literal[SingleStr],
|
||||
single_bytes: Literal[SingleBytes],
|
||||
single_bool: Literal[SingleBool],
|
||||
single_none: Literal[SingleNone],
|
||||
single_enum: Literal[SingleEnum],
|
||||
union_literals: Literal[UnionLiterals],
|
||||
an_enum1: Literal[AnEnum1], # error: [invalid-type-form]
|
||||
an_enum2: Literal[AnEnum2],
|
||||
bool1: Literal[Bool1], # error: [invalid-type-form]
|
||||
bool2: Literal[Bool2],
|
||||
multiple: Literal[SingleInt, SingleStr, SingleEnum],
|
||||
):
|
||||
# TODO should be `Literal[1]`
|
||||
reveal_type(single_int) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal["foo"]`
|
||||
reveal_type(single_str) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal[b"bar"]`
|
||||
reveal_type(single_bytes) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal[True]`
|
||||
reveal_type(single_bool) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `None`
|
||||
reveal_type(single_none) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal[E.A]`
|
||||
reveal_type(single_enum) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal[1, "foo", b"bar", True, E.A] | None`
|
||||
reveal_type(union_literals) # revealed: @Todo(Inference of subscript on special form)
|
||||
reveal_type(an_enum1) # revealed: Unknown
|
||||
# TODO should be `E`
|
||||
reveal_type(an_enum2) # revealed: @Todo(Inference of subscript on special form)
|
||||
reveal_type(bool1) # revealed: Unknown
|
||||
# TODO should be `bool`
|
||||
reveal_type(bool2) # revealed: @Todo(Inference of subscript on special form)
|
||||
# TODO should be `Literal[1, "foo", E.A]`
|
||||
reveal_type(multiple) # revealed: @Todo(Inference of subscript on special form)
|
||||
```
|
||||
|
||||
## Shortening unions of literals
|
||||
|
||||
When a Literal is parameterized with more than one value, it’s treated as exactly to equivalent to
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ class Color(Enum):
|
|||
RED = "red"
|
||||
|
||||
f: dict[list[Literal[1]], list[Literal[Color.RED]]] = {[1]: [Color.RED, Color.RED]}
|
||||
reveal_type(f) # revealed: dict[list[Literal[1]], list[Literal[Color.RED]]]
|
||||
reveal_type(f) # revealed: dict[list[Literal[1]], list[Color]]
|
||||
|
||||
class X[T]:
|
||||
def __init__(self, value: T): ...
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue