Skip to content

Commit ed18112

Browse files
authored
[ty] Add support for Literals in implicit type aliases (#21296)
## Summary Add support for `Literal` types in implicit type aliases. part of astral-sh/ty#221 ## Ecosystem analysis This looks good to me, true positives and known problems. ## Test Plan New Markdown tests.
1 parent 8ba1cfe commit ed18112

File tree

6 files changed

+179
-68
lines changed

6 files changed

+179
-68
lines changed

crates/ty_python_semantic/resources/mdtest/annotations/literal.md

Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -181,30 +181,20 @@ def _(
181181
bool2: Literal[Bool2],
182182
multiple: Literal[SingleInt, SingleStr, SingleEnum],
183183
):
184-
# TODO should be `Literal[1]`
185-
reveal_type(single_int) # revealed: @Todo(Inference of subscript on special form)
186-
# TODO should be `Literal["foo"]`
187-
reveal_type(single_str) # revealed: @Todo(Inference of subscript on special form)
188-
# TODO should be `Literal[b"bar"]`
189-
reveal_type(single_bytes) # revealed: @Todo(Inference of subscript on special form)
190-
# TODO should be `Literal[True]`
191-
reveal_type(single_bool) # revealed: @Todo(Inference of subscript on special form)
192-
# TODO should be `None`
193-
reveal_type(single_none) # revealed: @Todo(Inference of subscript on special form)
194-
# TODO should be `Literal[E.A]`
195-
reveal_type(single_enum) # revealed: @Todo(Inference of subscript on special form)
196-
# TODO should be `Literal[1, "foo", b"bar", True, E.A] | None`
197-
reveal_type(union_literals) # revealed: @Todo(Inference of subscript on special form)
184+
reveal_type(single_int) # revealed: Literal[1]
185+
reveal_type(single_str) # revealed: Literal["foo"]
186+
reveal_type(single_bytes) # revealed: Literal[b"bar"]
187+
reveal_type(single_bool) # revealed: Literal[True]
188+
reveal_type(single_none) # revealed: None
189+
reveal_type(single_enum) # revealed: Literal[E.A]
190+
reveal_type(union_literals) # revealed: Literal[1, "foo", b"bar", True, E.A] | None
198191
# Could also be `E`
199192
reveal_type(an_enum1) # revealed: Unknown
200-
# TODO should be `E`
201-
reveal_type(an_enum2) # revealed: @Todo(Inference of subscript on special form)
193+
reveal_type(an_enum2) # revealed: E
202194
# Could also be `bool`
203195
reveal_type(bool1) # revealed: Unknown
204-
# TODO should be `bool`
205-
reveal_type(bool2) # revealed: @Todo(Inference of subscript on special form)
206-
# TODO should be `Literal[1, "foo", E.A]`
207-
reveal_type(multiple) # revealed: @Todo(Inference of subscript on special form)
196+
reveal_type(bool2) # revealed: bool
197+
reveal_type(multiple) # revealed: Literal[1, "foo", E.A]
208198
```
209199

210200
### Implicit type alias
@@ -246,28 +236,18 @@ def _(
246236
bool2: Literal[Bool2],
247237
multiple: Literal[SingleInt, SingleStr, SingleEnum],
248238
):
249-
# TODO should be `Literal[1]`
250-
reveal_type(single_int) # revealed: @Todo(Inference of subscript on special form)
251-
# TODO should be `Literal["foo"]`
252-
reveal_type(single_str) # revealed: @Todo(Inference of subscript on special form)
253-
# TODO should be `Literal[b"bar"]`
254-
reveal_type(single_bytes) # revealed: @Todo(Inference of subscript on special form)
255-
# TODO should be `Literal[True]`
256-
reveal_type(single_bool) # revealed: @Todo(Inference of subscript on special form)
257-
# TODO should be `None`
258-
reveal_type(single_none) # revealed: @Todo(Inference of subscript on special form)
259-
# TODO should be `Literal[E.A]`
260-
reveal_type(single_enum) # revealed: @Todo(Inference of subscript on special form)
261-
# TODO should be `Literal[1, "foo", b"bar", True, E.A] | None`
262-
reveal_type(union_literals) # revealed: @Todo(Inference of subscript on special form)
239+
reveal_type(single_int) # revealed: Literal[1]
240+
reveal_type(single_str) # revealed: Literal["foo"]
241+
reveal_type(single_bytes) # revealed: Literal[b"bar"]
242+
reveal_type(single_bool) # revealed: Literal[True]
243+
reveal_type(single_none) # revealed: None
244+
reveal_type(single_enum) # revealed: Literal[E.A]
245+
reveal_type(union_literals) # revealed: Literal[1, "foo", b"bar", True, E.A] | None
263246
reveal_type(an_enum1) # revealed: Unknown
264-
# TODO should be `E`
265-
reveal_type(an_enum2) # revealed: @Todo(Inference of subscript on special form)
247+
reveal_type(an_enum2) # revealed: E
266248
reveal_type(bool1) # revealed: Unknown
267-
# TODO should be `bool`
268-
reveal_type(bool2) # revealed: @Todo(Inference of subscript on special form)
269-
# TODO should be `Literal[1, "foo", E.A]`
270-
reveal_type(multiple) # revealed: @Todo(Inference of subscript on special form)
249+
reveal_type(bool2) # revealed: bool
250+
reveal_type(multiple) # revealed: Literal[1, "foo", E.A]
271251
```
272252

273253
## Shortening unions of literals

crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ g(None)
3333
We also support unions in type aliases:
3434

3535
```py
36-
from typing_extensions import Any, Never
36+
from typing_extensions import Any, Never, Literal
3737
from ty_extensions import Unknown
3838

3939
IntOrStr = int | str
@@ -54,6 +54,8 @@ NeverOrAny = Never | Any
5454
AnyOrNever = Any | Never
5555
UnknownOrInt = Unknown | int
5656
IntOrUnknown = int | Unknown
57+
StrOrZero = str | Literal[0]
58+
ZeroOrStr = Literal[0] | str
5759

5860
reveal_type(IntOrStr) # revealed: types.UnionType
5961
reveal_type(IntOrStrOrBytes1) # revealed: types.UnionType
@@ -73,6 +75,8 @@ reveal_type(NeverOrAny) # revealed: types.UnionType
7375
reveal_type(AnyOrNever) # revealed: types.UnionType
7476
reveal_type(UnknownOrInt) # revealed: types.UnionType
7577
reveal_type(IntOrUnknown) # revealed: types.UnionType
78+
reveal_type(StrOrZero) # revealed: types.UnionType
79+
reveal_type(ZeroOrStr) # revealed: types.UnionType
7680

7781
def _(
7882
int_or_str: IntOrStr,
@@ -93,6 +97,8 @@ def _(
9397
any_or_never: AnyOrNever,
9498
unknown_or_int: UnknownOrInt,
9599
int_or_unknown: IntOrUnknown,
100+
str_or_zero: StrOrZero,
101+
zero_or_str: ZeroOrStr,
96102
):
97103
reveal_type(int_or_str) # revealed: int | str
98104
reveal_type(int_or_str_or_bytes1) # revealed: int | str | bytes
@@ -112,6 +118,8 @@ def _(
112118
reveal_type(any_or_never) # revealed: Any
113119
reveal_type(unknown_or_int) # revealed: Unknown | int
114120
reveal_type(int_or_unknown) # revealed: int | Unknown
121+
reveal_type(str_or_zero) # revealed: str | Literal[0]
122+
reveal_type(zero_or_str) # revealed: Literal[0] | str
115123
```
116124

117125
If a type is unioned with itself in a value expression, the result is just that type. No
@@ -255,6 +263,68 @@ def _(list_or_tuple: ListOrTuple[int]):
255263
reveal_type(list_or_tuple) # revealed: @Todo(Generic specialization of types.UnionType)
256264
```
257265

266+
## `Literal`s
267+
268+
We also support `typing.Literal` in implicit type aliases.
269+
270+
```py
271+
from typing import Literal
272+
from enum import Enum
273+
274+
IntLiteral1 = Literal[26]
275+
IntLiteral2 = Literal[0x1A]
276+
IntLiterals = Literal[-1, 0, 1]
277+
NestedLiteral = Literal[Literal[1]]
278+
StringLiteral = Literal["a"]
279+
BytesLiteral = Literal[b"b"]
280+
BoolLiteral = Literal[True]
281+
MixedLiterals = Literal[1, "a", True, None]
282+
283+
class Color(Enum):
284+
RED = 0
285+
GREEN = 1
286+
BLUE = 2
287+
288+
EnumLiteral = Literal[Color.RED]
289+
290+
def _(
291+
int_literal1: IntLiteral1,
292+
int_literal2: IntLiteral2,
293+
int_literals: IntLiterals,
294+
nested_literal: NestedLiteral,
295+
string_literal: StringLiteral,
296+
bytes_literal: BytesLiteral,
297+
bool_literal: BoolLiteral,
298+
mixed_literals: MixedLiterals,
299+
enum_literal: EnumLiteral,
300+
):
301+
reveal_type(int_literal1) # revealed: Literal[26]
302+
reveal_type(int_literal2) # revealed: Literal[26]
303+
reveal_type(int_literals) # revealed: Literal[-1, 0, 1]
304+
reveal_type(nested_literal) # revealed: Literal[1]
305+
reveal_type(string_literal) # revealed: Literal["a"]
306+
reveal_type(bytes_literal) # revealed: Literal[b"b"]
307+
reveal_type(bool_literal) # revealed: Literal[True]
308+
reveal_type(mixed_literals) # revealed: Literal[1, "a", True] | None
309+
reveal_type(enum_literal) # revealed: Literal[Color.RED]
310+
```
311+
312+
We reject invalid uses:
313+
314+
```py
315+
# error: [invalid-type-form] "Type arguments for `Literal` must be `None`, a literal value (int, bool, str, or bytes), or an enum member"
316+
LiteralInt = Literal[int]
317+
318+
reveal_type(LiteralInt) # revealed: Unknown
319+
320+
def _(weird: LiteralInt):
321+
reveal_type(weird) # revealed: Unknown
322+
323+
# error: [invalid-type-form] "`Literal[26]` is not a generic class"
324+
def _(weird: IntLiteral1[int]):
325+
reveal_type(weird) # revealed: Unknown
326+
```
327+
258328
## Stringified annotations?
259329

260330
From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html):

crates/ty_python_semantic/src/types.rs

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6451,9 +6451,9 @@ impl<'db> Type<'db> {
64516451
invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Generic],
64526452
fallback_type: Type::unknown(),
64536453
}),
6454-
KnownInstanceType::UnionType(union_type) => {
6454+
KnownInstanceType::UnionType(list) => {
64556455
let mut builder = UnionBuilder::new(db);
6456-
for element in union_type.elements(db) {
6456+
for element in list.elements(db) {
64576457
builder = builder.add(element.in_type_expression(
64586458
db,
64596459
scope_id,
@@ -6462,6 +6462,7 @@ impl<'db> Type<'db> {
64626462
}
64636463
Ok(builder.build())
64646464
}
6465+
KnownInstanceType::Literal(list) => Ok(list.to_union(db)),
64656466
},
64666467

64676468
Type::SpecialForm(special_form) => match special_form {
@@ -7675,7 +7676,10 @@ pub enum KnownInstanceType<'db> {
76757676

76767677
/// A single instance of `types.UnionType`, which stores the left- and
76777678
/// right-hand sides of a PEP 604 union.
7678-
UnionType(UnionTypeInstance<'db>),
7679+
UnionType(TypeList<'db>),
7680+
7681+
/// A single instance of `typing.Literal`
7682+
Literal(TypeList<'db>),
76797683
}
76807684

76817685
fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
@@ -7702,9 +7706,9 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
77027706
visitor.visit_type(db, default_ty);
77037707
}
77047708
}
7705-
KnownInstanceType::UnionType(union_type) => {
7706-
for element in union_type.elements(db) {
7707-
visitor.visit_type(db, element);
7709+
KnownInstanceType::UnionType(list) | KnownInstanceType::Literal(list) => {
7710+
for element in list.elements(db) {
7711+
visitor.visit_type(db, *element);
77087712
}
77097713
}
77107714
}
@@ -7743,7 +7747,8 @@ impl<'db> KnownInstanceType<'db> {
77437747
// Nothing to normalize
77447748
Self::ConstraintSet(set)
77457749
}
7746-
Self::UnionType(union_type) => Self::UnionType(union_type.normalized_impl(db, visitor)),
7750+
Self::UnionType(list) => Self::UnionType(list.normalized_impl(db, visitor)),
7751+
Self::Literal(list) => Self::Literal(list.normalized_impl(db, visitor)),
77477752
}
77487753
}
77497754

@@ -7762,6 +7767,7 @@ impl<'db> KnownInstanceType<'db> {
77627767
Self::Field(_) => KnownClass::Field,
77637768
Self::ConstraintSet(_) => KnownClass::ConstraintSet,
77647769
Self::UnionType(_) => KnownClass::UnionType,
7770+
Self::Literal(_) => KnownClass::GenericAlias,
77657771
}
77667772
}
77677773

@@ -7842,6 +7848,7 @@ impl<'db> KnownInstanceType<'db> {
78427848
)
78437849
}
78447850
KnownInstanceType::UnionType(_) => f.write_str("types.UnionType"),
7851+
KnownInstanceType::Literal(_) => f.write_str("typing.Literal"),
78457852
}
78467853
}
78477854
}
@@ -8972,32 +8979,46 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
89728979
}
89738980
}
89748981

8975-
/// An instance of `types.UnionType`.
8982+
/// A salsa-interned list of types.
89768983
///
89778984
/// # Ordering
89788985
/// Ordering is based on the context's salsa-assigned id and not on its values.
89798986
/// The id may change between runs, or when the context was garbage collected and recreated.
89808987
#[salsa::interned(debug)]
89818988
#[derive(PartialOrd, Ord)]
8982-
pub struct UnionTypeInstance<'db> {
8983-
left: Type<'db>,
8984-
right: Type<'db>,
8989+
pub struct TypeList<'db> {
8990+
#[returns(deref)]
8991+
elements: Box<[Type<'db>]>,
89858992
}
89868993

8987-
impl get_size2::GetSize for UnionTypeInstance<'_> {}
8994+
impl get_size2::GetSize for TypeList<'_> {}
8995+
8996+
impl<'db> TypeList<'db> {
8997+
pub(crate) fn from_elements(
8998+
db: &'db dyn Db,
8999+
elements: impl IntoIterator<Item = Type<'db>>,
9000+
) -> TypeList<'db> {
9001+
TypeList::new(db, elements.into_iter().collect::<Box<[_]>>())
9002+
}
89889003

8989-
impl<'db> UnionTypeInstance<'db> {
8990-
pub(crate) fn elements(self, db: &'db dyn Db) -> [Type<'db>; 2] {
8991-
[self.left(db), self.right(db)]
9004+
pub(crate) fn singleton(db: &'db dyn Db, element: Type<'db>) -> TypeList<'db> {
9005+
TypeList::from_elements(db, [element])
89929006
}
89939007

89949008
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
8995-
UnionTypeInstance::new(
9009+
TypeList::new(
89969010
db,
8997-
self.left(db).normalized_impl(db, visitor),
8998-
self.right(db).normalized_impl(db, visitor),
9011+
self.elements(db)
9012+
.iter()
9013+
.map(|ty| ty.normalized_impl(db, visitor))
9014+
.collect::<Box<[_]>>(),
89999015
)
90009016
}
9017+
9018+
/// Turn this list of types `[T1, T2, ...]` into a union type `T1 | T2 | ...`.
9019+
pub(crate) fn to_union(self, db: &'db dyn Db) -> Type<'db> {
9020+
UnionType::from_elements(db, self.elements(db))
9021+
}
90019022
}
90029023

90039024
/// Error returned if a type is not awaitable.

crates/ty_python_semantic/src/types/class_base.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ impl<'db> ClassBase<'db> {
168168
| KnownInstanceType::Deprecated(_)
169169
| KnownInstanceType::Field(_)
170170
| KnownInstanceType::ConstraintSet(_)
171-
| KnownInstanceType::UnionType(_) => None,
171+
| KnownInstanceType::UnionType(_)
172+
| KnownInstanceType::Literal(_) => None,
172173
},
173174

174175
Type::SpecialForm(special_form) => match special_form {

0 commit comments

Comments
 (0)