Skip to content

Commit c3f7ba9

Browse files
committed
[ty] List all enum members
1 parent 87f6f08 commit c3f7ba9

File tree

7 files changed

+516
-2
lines changed

7 files changed

+516
-2
lines changed
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
# Enums
2+
3+
## Basic
4+
5+
```py
6+
from enum import Enum
7+
8+
class Color(Enum):
9+
RED = 1
10+
GREEN = 2
11+
BLUE = 3
12+
13+
reveal_type(Color.RED) # revealed: @Todo(Attribute access on enum classes)
14+
reveal_type(Color.RED.value) # revealed: @Todo(Attribute access on enum classes)
15+
16+
# TODO: Should be `Color` or `Literal[Color.RED]`
17+
reveal_type(Color["RED"]) # revealed: Unknown
18+
19+
# TODO: Should be `Color` or `Literal[Color.RED]`
20+
reveal_type(Color(1)) # revealed: Color
21+
22+
reveal_type(Color.RED in Color) # revealed: bool
23+
```
24+
25+
## Enum members
26+
27+
### Basic
28+
29+
Simple enums with integer or string values:
30+
31+
```py
32+
from enum import Enum
33+
from ty_extensions import enum_members
34+
35+
class ColorInt(Enum):
36+
RED = 1
37+
GREEN = 2
38+
BLUE = 3
39+
40+
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
41+
reveal_type(enum_members(ColorInt))
42+
43+
class ColorStr(Enum):
44+
RED = "red"
45+
GREEN = "green"
46+
BLUE = "blue"
47+
48+
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
49+
reveal_type(enum_members(ColorStr))
50+
```
51+
52+
### When deriving from `IntEnum`
53+
54+
```py
55+
from enum import IntEnum
56+
from ty_extensions import enum_members
57+
58+
class ColorInt(IntEnum):
59+
RED = 1
60+
GREEN = 2
61+
BLUE = 3
62+
63+
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
64+
reveal_type(enum_members(ColorInt))
65+
```
66+
67+
### Declared non-member attributes
68+
69+
Attributes on the enum class that are declared are not considered members of the enum. Similarly,
70+
methods are not considered members of the enum:
71+
72+
```py
73+
from enum import Enum
74+
from ty_extensions import enum_members
75+
76+
class Answer(Enum):
77+
YES = 1
78+
NO = 2
79+
80+
non_member_1: int
81+
82+
# TODO: this could be considered an error:
83+
non_member_1: str = "some value"
84+
85+
def some_method(self) -> None: ...
86+
@property
87+
def some_property(self) -> str:
88+
return ""
89+
90+
class Nested: ...
91+
92+
# revealed: tuple[Literal["YES"], Literal["NO"]]
93+
reveal_type(enum_members(Answer))
94+
```
95+
96+
### Non-member attributes with disallowed type
97+
98+
Some attributes are not considered members based on their type:
99+
100+
```py
101+
from enum import Enum
102+
from ty_extensions import enum_members
103+
104+
def identity(x) -> int:
105+
return x
106+
107+
class Answer(Enum):
108+
YES = 1
109+
NO = 2
110+
111+
non_member = lambda x: 0
112+
non_member_2 = identity
113+
114+
# revealed: tuple[Literal["YES"], Literal["NO"]]
115+
reveal_type(enum_members(Answer))
116+
```
117+
118+
### In stubs
119+
120+
Stubs can optionally use `...` for the actual value:
121+
122+
```pyi
123+
from enum import Enum
124+
from ty_extensions import enum_members
125+
from typing import cast
126+
127+
class Color(Enum):
128+
RED = ...
129+
GREEN = cast(int, ...)
130+
BLUE = 3
131+
132+
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
133+
reveal_type(enum_members(Color))
134+
```
135+
136+
### Aliases
137+
138+
Enum members can have aliases, which are not considered separate members:
139+
140+
```py
141+
from enum import Enum
142+
from ty_extensions import enum_members
143+
144+
class Answer(Enum):
145+
YES = 1
146+
NO = 2
147+
148+
DEFINITELY = YES
149+
150+
# revealed: tuple[Literal["YES"], Literal["NO"]]
151+
reveal_type(enum_members(Answer))
152+
```
153+
154+
### Using `auto()`
155+
156+
```py
157+
from enum import Enum, auto
158+
from ty_extensions import enum_members
159+
160+
class Answer(Enum):
161+
YES = auto()
162+
NO = auto()
163+
164+
# revealed: tuple[Literal["YES"], Literal["NO"]]
165+
reveal_type(enum_members(Answer))
166+
```
167+
168+
Combining aliases with `auto()`:
169+
170+
```py
171+
from enum import Enum, auto
172+
173+
class Answer(Enum):
174+
YES = auto()
175+
NO = auto()
176+
177+
DEFINITELY = YES
178+
179+
# TODO: This should ideally be `tuple[Literal["YES"], Literal["NO"]]`
180+
# revealed: tuple[Literal["YES"], Literal["NO"], Literal["DEFINITELY"]]
181+
reveal_type(enum_members(Answer))
182+
```
183+
184+
### `member` and `nonmember`
185+
186+
```toml
187+
[environment]
188+
python-version = "3.11"
189+
```
190+
191+
```py
192+
from enum import Enum, auto, member, nonmember
193+
from ty_extensions import enum_members
194+
195+
class Answer(Enum):
196+
YES = member(1)
197+
NO = member(2)
198+
OTHER = nonmember(17)
199+
200+
# revealed: tuple[Literal["YES"], Literal["NO"]]
201+
reveal_type(enum_members(Answer))
202+
```
203+
204+
`member` can also be used as a decorator:
205+
206+
```py
207+
from enum import Enum, member
208+
from ty_extensions import enum_members
209+
210+
class Answer(Enum):
211+
yes = member(1)
212+
no = member(2)
213+
214+
@member
215+
def maybe(self) -> None:
216+
return
217+
218+
# revealed: tuple[Literal["yes"], Literal["no"], Literal["maybe"]]
219+
reveal_type(enum_members(Answer))
220+
```
221+
222+
### Private names
223+
224+
An attribute with a private name (beginning with, but not ending in, a double underscore) is treated
225+
as a non-member:
226+
227+
```py
228+
from enum import Enum
229+
from ty_extensions import enum_members
230+
231+
class Answer(Enum):
232+
YES = 1
233+
NO = 2
234+
235+
__private_member = 3
236+
__maybe__ = 4
237+
238+
# revealed: tuple[Literal["YES"], Literal["NO"], Literal["__maybe__"]]
239+
reveal_type(enum_members(Answer))
240+
```
241+
242+
### Ignored names
243+
244+
An enum class can define a class symbol named `_ignore_`. This can be a string containing a
245+
space-delimited list of names:
246+
247+
```py
248+
from enum import Enum
249+
from ty_extensions import enum_members
250+
251+
class Answer(Enum):
252+
_ignore_ = "MAYBE _other"
253+
254+
YES = 1
255+
NO = 2
256+
257+
MAYBE = 3
258+
_other = "test"
259+
260+
# revealed: tuple[Literal["YES"], Literal["NO"]]
261+
reveal_type(enum_members(Answer))
262+
```
263+
264+
`_ignore_` can also be a list of names:
265+
266+
```py
267+
class Answer2(Enum):
268+
_ignore_ = ["MAYBE", "_other"]
269+
270+
YES = 1
271+
NO = 2
272+
273+
MAYBE = 3
274+
_other = "test"
275+
276+
# TODO: This should be `tuple[Literal["YES"], Literal["NO"]]`
277+
# revealed: tuple[Literal["YES"], Literal["NO"], Literal["MAYBE"], Literal["_other"]]
278+
reveal_type(enum_members(Answer2))
279+
```
280+
281+
## Iterating over enum members
282+
283+
```py
284+
from enum import Enum
285+
286+
class Color(Enum):
287+
RED = 1
288+
GREEN = 2
289+
BLUE = 3
290+
291+
for color in Color:
292+
# TODO: Should be `Color`
293+
reveal_type(color) # revealed: Unknown
294+
295+
# TODO: Should be `list[Color]`
296+
reveal_type(list(Color)) # revealed: list[Unknown]
297+
```
298+
299+
## Properties of enum types
300+
301+
### Implicitly final
302+
303+
An enum with one or more defined members cannot be subclassed. They are implicitly "final".
304+
305+
```py
306+
from enum import Enum
307+
308+
class Color(Enum):
309+
RED = 1
310+
GREEN = 2
311+
BLUE = 3
312+
313+
# TODO: This should emit an error
314+
class ExtendedColor(Color):
315+
YELLOW = 4
316+
```
317+
318+
## Custom enum types
319+
320+
To do: <https://typing.python.org/en/latest/spec/enums.html#enum-definition>
321+
322+
## Function syntax
323+
324+
To do: <https://typing.python.org/en/latest/spec/enums.html#enum-definition>
325+
326+
## Exhaustiveness checking
327+
328+
To do
329+
330+
## References
331+
332+
- Typing spec: <https://typing.python.org/en/latest/spec/enums.html>
333+
- Documentation: <https://docs.python.org/3/library/enum.html>

crates/ty_python_semantic/src/types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ mod context;
6767
mod cyclic;
6868
mod diagnostic;
6969
mod display;
70+
mod enums;
7071
mod function;
7172
mod generics;
7273
pub(crate) mod ide_support;

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use crate::types::tuple::TupleType;
3131
use crate::types::{
3232
BoundMethodType, ClassLiteral, DataclassParams, KnownClass, KnownInstanceType,
3333
MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType,
34-
WrapperDescriptorKind, ide_support, todo_type,
34+
WrapperDescriptorKind, enums, ide_support, todo_type,
3535
};
3636
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};
3737
use ruff_python_ast as ast;
@@ -661,6 +661,22 @@ impl<'db> Bindings<'db> {
661661
}
662662
}
663663

664+
Some(KnownFunction::EnumMembers) => {
665+
if let [Some(ty)] = overload.parameter_types() {
666+
let return_ty = match ty {
667+
Type::ClassLiteral(class) => TupleType::from_elements(
668+
db,
669+
enums::enum_members(db, *class)
670+
.into_iter()
671+
.map(|member| Type::string_literal(db, &member)),
672+
),
673+
_ => Type::unknown(),
674+
};
675+
676+
overload.set_return_type(return_ty);
677+
}
678+
}
679+
664680
Some(KnownFunction::AllMembers) => {
665681
if let [Some(ty)] = overload.parameter_types() {
666682
overload.set_return_type(TupleType::from_elements(

0 commit comments

Comments
 (0)