mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 06:11:21 +00:00
[ty] List all enum
members (#19283)
## Summary Adds a way to list all members of an `Enum` and implements almost all of the mechanisms by which members are distinguished from non-members ([spec](https://typing.python.org/en/latest/spec/enums.html#defining-members)). This has no effect on actual enums, so far. ## Test Plan New Markdown tests using `ty_extensions.enum_members`.
This commit is contained in:
parent
cb530a0216
commit
f22da352db
7 changed files with 639 additions and 2 deletions
439
crates/ty_python_semantic/resources/mdtest/enums.md
Normal file
439
crates/ty_python_semantic/resources/mdtest/enums.md
Normal file
|
@ -0,0 +1,439 @@
|
|||
# Enums
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
|
||||
class Color(Enum):
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
BLUE = 3
|
||||
|
||||
reveal_type(Color.RED) # revealed: @Todo(Attribute access on enum classes)
|
||||
reveal_type(Color.RED.value) # revealed: @Todo(Attribute access on enum classes)
|
||||
|
||||
# TODO: Should be `Color` or `Literal[Color.RED]`
|
||||
reveal_type(Color["RED"]) # revealed: Unknown
|
||||
|
||||
# TODO: Could be `Literal[Color.RED]` to be more precise
|
||||
reveal_type(Color(1)) # revealed: Color
|
||||
|
||||
reveal_type(Color.RED in Color) # revealed: bool
|
||||
```
|
||||
|
||||
## Enum members
|
||||
|
||||
### Basic
|
||||
|
||||
Simple enums with integer or string values:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class ColorInt(Enum):
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
BLUE = 3
|
||||
|
||||
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
|
||||
reveal_type(enum_members(ColorInt))
|
||||
|
||||
class ColorStr(Enum):
|
||||
RED = "red"
|
||||
GREEN = "green"
|
||||
BLUE = "blue"
|
||||
|
||||
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
|
||||
reveal_type(enum_members(ColorStr))
|
||||
```
|
||||
|
||||
### When deriving from `IntEnum`
|
||||
|
||||
```py
|
||||
from enum import IntEnum
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class ColorInt(IntEnum):
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
BLUE = 3
|
||||
|
||||
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
|
||||
reveal_type(enum_members(ColorInt))
|
||||
```
|
||||
|
||||
### Declared non-member attributes
|
||||
|
||||
Attributes on the enum class that are declared are not considered members of the enum:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class Answer(Enum):
|
||||
YES = 1
|
||||
NO = 2
|
||||
|
||||
non_member_1: int
|
||||
|
||||
# TODO: this could be considered an error:
|
||||
non_member_1: str = "some value"
|
||||
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
### Non-member attributes with disallowed type
|
||||
|
||||
Methods, callables, descriptors (including properties), and nested classes that are defined in the
|
||||
class are not treated as enum members:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from ty_extensions import enum_members
|
||||
from typing import Callable, Literal
|
||||
|
||||
def identity(x) -> int:
|
||||
return x
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance, owner):
|
||||
return 0
|
||||
|
||||
class Answer(Enum):
|
||||
YES = 1
|
||||
NO = 2
|
||||
|
||||
def some_method(self) -> None: ...
|
||||
@staticmethod
|
||||
def some_static_method() -> None: ...
|
||||
@classmethod
|
||||
def some_class_method(cls) -> None: ...
|
||||
|
||||
some_callable = lambda x: 0
|
||||
declared_callable: Callable[[int], int] = identity
|
||||
function_reference = identity
|
||||
|
||||
some_descriptor = Descriptor()
|
||||
|
||||
@property
|
||||
def some_property(self) -> str:
|
||||
return ""
|
||||
|
||||
class NestedClass: ...
|
||||
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
### `enum.property`
|
||||
|
||||
Enum attributes that are defined using `enum.property` are not considered members:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from enum import Enum, property as enum_property
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class Answer(Enum):
|
||||
YES = 1
|
||||
NO = 2
|
||||
|
||||
@enum_property
|
||||
def some_property(self) -> str:
|
||||
return "property value"
|
||||
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
### `types.DynamicClassAttribute`
|
||||
|
||||
Attributes defined using `types.DynamicClassAttribute` are not considered members:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from ty_extensions import enum_members
|
||||
from types import DynamicClassAttribute
|
||||
|
||||
class Answer(Enum):
|
||||
YES = 1
|
||||
NO = 2
|
||||
|
||||
@DynamicClassAttribute
|
||||
def dynamic_property(self) -> str:
|
||||
return "dynamic value"
|
||||
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
### In stubs
|
||||
|
||||
Stubs can optionally use `...` for the actual value:
|
||||
|
||||
```pyi
|
||||
from enum import Enum
|
||||
from ty_extensions import enum_members
|
||||
from typing import cast
|
||||
|
||||
class Color(Enum):
|
||||
RED = ...
|
||||
GREEN = cast(int, ...)
|
||||
BLUE = 3
|
||||
|
||||
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
|
||||
reveal_type(enum_members(Color))
|
||||
```
|
||||
|
||||
### Aliases
|
||||
|
||||
Enum members can have aliases, which are not considered separate members:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class Answer(Enum):
|
||||
YES = 1
|
||||
NO = 2
|
||||
|
||||
DEFINITELY = YES
|
||||
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
### Using `auto()`
|
||||
|
||||
```py
|
||||
from enum import Enum, auto
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class Answer(Enum):
|
||||
YES = auto()
|
||||
NO = auto()
|
||||
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
Combining aliases with `auto()`:
|
||||
|
||||
```py
|
||||
from enum import Enum, auto
|
||||
|
||||
class Answer(Enum):
|
||||
YES = auto()
|
||||
NO = auto()
|
||||
|
||||
DEFINITELY = YES
|
||||
|
||||
# TODO: This should ideally be `tuple[Literal["YES"], Literal["NO"]]`
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"], Literal["DEFINITELY"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
### `member` and `nonmember`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from enum import Enum, auto, member, nonmember
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class Answer(Enum):
|
||||
YES = member(1)
|
||||
NO = member(2)
|
||||
OTHER = nonmember(17)
|
||||
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
`member` can also be used as a decorator:
|
||||
|
||||
```py
|
||||
from enum import Enum, member
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class Answer(Enum):
|
||||
yes = member(1)
|
||||
no = member(2)
|
||||
|
||||
@member
|
||||
def maybe(self) -> None:
|
||||
return
|
||||
|
||||
# revealed: tuple[Literal["yes"], Literal["no"], Literal["maybe"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
### Class-private names
|
||||
|
||||
An attribute with a [class-private name] (beginning with, but not ending in, a double underscore) is
|
||||
treated as a non-member:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class Answer(Enum):
|
||||
YES = 1
|
||||
NO = 2
|
||||
|
||||
__private_member = 3
|
||||
__maybe__ = 4
|
||||
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"], Literal["__maybe__"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
### Ignored names
|
||||
|
||||
An enum class can define a class symbol named `_ignore_`. This can be a string containing a
|
||||
whitespace-delimited list of names:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class Answer(Enum):
|
||||
_ignore_ = "IGNORED _other_ignored also_ignored"
|
||||
|
||||
YES = 1
|
||||
NO = 2
|
||||
|
||||
IGNORED = 3
|
||||
_other_ignored = "test"
|
||||
also_ignored = "test2"
|
||||
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
`_ignore_` can also be a list of names:
|
||||
|
||||
```py
|
||||
class Answer2(Enum):
|
||||
_ignore_ = ["MAYBE", "_other"]
|
||||
|
||||
YES = 1
|
||||
NO = 2
|
||||
|
||||
MAYBE = 3
|
||||
_other = "test"
|
||||
|
||||
# TODO: This should be `tuple[Literal["YES"], Literal["NO"]]`
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"], Literal["MAYBE"], Literal["_other"]]
|
||||
reveal_type(enum_members(Answer2))
|
||||
```
|
||||
|
||||
### Special names
|
||||
|
||||
Make sure that special names like `name` and `value` can be used for enum members (without
|
||||
conflicting with `Enum.name` and `Enum.value`):
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class Answer(Enum):
|
||||
name = 1
|
||||
value = 2
|
||||
|
||||
# revealed: tuple[Literal["name"], Literal["value"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
|
||||
# TODO: These should be `Answer` or `Literal[Answer.name]`/``Literal[Answer.value]`
|
||||
reveal_type(Answer.name) # revealed: @Todo(Attribute access on enum classes)
|
||||
reveal_type(Answer.value) # revealed: @Todo(Attribute access on enum classes)
|
||||
```
|
||||
|
||||
## Iterating over enum members
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
|
||||
class Color(Enum):
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
BLUE = 3
|
||||
|
||||
for color in Color:
|
||||
# TODO: Should be `Color`
|
||||
reveal_type(color) # revealed: Unknown
|
||||
|
||||
# TODO: Should be `list[Color]`
|
||||
reveal_type(list(Color)) # revealed: list[Unknown]
|
||||
```
|
||||
|
||||
## Properties of enum types
|
||||
|
||||
### Implicitly final
|
||||
|
||||
An enum with one or more defined members cannot be subclassed. They are implicitly "final".
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
|
||||
class Color(Enum):
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
BLUE = 3
|
||||
|
||||
# TODO: This should emit an error
|
||||
class ExtendedColor(Color):
|
||||
YELLOW = 4
|
||||
|
||||
def f(color: Color):
|
||||
if isinstance(color, int):
|
||||
# TODO: This should be `Never`
|
||||
reveal_type(color) # revealed: Color & int
|
||||
```
|
||||
|
||||
An `Enum` subclass without any defined members can be subclassed:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class MyEnum(Enum):
|
||||
def some_method(self) -> None:
|
||||
pass
|
||||
|
||||
class Answer(MyEnum):
|
||||
YES = 1
|
||||
NO = 2
|
||||
|
||||
# revealed: tuple[Literal["YES"], Literal["NO"]]
|
||||
reveal_type(enum_members(Answer))
|
||||
```
|
||||
|
||||
## Custom enum types
|
||||
|
||||
To do: <https://typing.python.org/en/latest/spec/enums.html#enum-definition>
|
||||
|
||||
## Function syntax
|
||||
|
||||
To do: <https://typing.python.org/en/latest/spec/enums.html#enum-definition>
|
||||
|
||||
## Exhaustiveness checking
|
||||
|
||||
To do
|
||||
|
||||
## References
|
||||
|
||||
- Typing spec: <https://typing.python.org/en/latest/spec/enums.html>
|
||||
- Documentation: <https://docs.python.org/3/library/enum.html>
|
||||
|
||||
[class-private name]: https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers
|
Loading…
Add table
Add a link
Reference in a new issue