-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[ty] support enum _value_ annotation
#22228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2fd025c
5c87d3e
581d099
da7588d
01d9ca8
c265c6f
ba7371d
45de0bf
de50a9f
6ddf331
ad839d1
7590221
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,6 +100,175 @@ class Answer(Enum): | |
| reveal_type(enum_members(Answer)) | ||
| ``` | ||
|
|
||
| ### Declared `_value_` annotation | ||
|
|
||
| If a `_value_` annotation is defined on an `Enum` class, all enum member values must be compatible | ||
| with the declared type: | ||
|
|
||
| ```pyi | ||
| from enum import Enum | ||
|
|
||
| class Color(Enum): | ||
| _value_: int | ||
| RED = 1 | ||
| GREEN = "green" # error: [invalid-assignment] | ||
| BLUE = ... | ||
| YELLOW = None # error: [invalid-assignment] | ||
| # In stub files, `[]` is not exempt from type checking (only `...` is). | ||
| PURPLE = [] # error: [invalid-assignment] | ||
|
Comment on lines
+117
to
+118
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kind of an odd comment and test. Not necessarily opposed to including the test, but I don't really get why anyone would think that |
||
| ``` | ||
|
|
||
| When `_value_` is annotated, `.value` and `._value_` are inferred as the declared type: | ||
|
|
||
| ```py | ||
| from enum import Enum | ||
|
|
||
| class Color2(Enum): | ||
| _value_: int | ||
| RED = 1 | ||
| GREEN = 2 | ||
|
|
||
| reveal_type(Color2.RED.value) # revealed: int | ||
| reveal_type(Color2.RED._value_) # revealed: int | ||
| ``` | ||
|
|
||
| ### `_value_` annotation with `__init__` | ||
|
|
||
| When `__init__` is defined, member values are validated by synthesizing a call to `__init__`. The | ||
| `_value_` annotation still constrains assignments to `self._value_` inside `__init__`: | ||
|
|
||
| ```py | ||
| from enum import Enum | ||
|
|
||
| class Planet(Enum): | ||
| _value_: int | ||
|
|
||
| def __init__(self, value: int, mass: float, radius: float): | ||
| self._value_ = value | ||
|
|
||
| MERCURY = (1, 3.303e23, 2.4397e6) | ||
| SATURN = "saturn" # error: [invalid-assignment] | ||
|
|
||
| reveal_type(Planet.MERCURY.value) # revealed: int | ||
| reveal_type(Planet.MERCURY._value_) # revealed: int | ||
| ``` | ||
|
|
||
| ### `_value_` annotation incompatible with `__init__` | ||
|
|
||
| When `_value_` and `__init__` disagree, the assignment inside `__init__` is flagged: | ||
|
|
||
| ```py | ||
| from enum import Enum | ||
|
|
||
| class Planet(Enum): | ||
| _value_: str | ||
carljm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def __init__(self, value: int, mass: float, radius: float): | ||
| self._value_ = value # error: [invalid-assignment] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's also have a happy-path test with both |
||
|
|
||
| MERCURY = (1, 3.303e23, 2.4397e6) | ||
| SATURN = "saturn" # error: [invalid-assignment] | ||
|
|
||
| reveal_type(Planet.MERCURY.value) # revealed: str | ||
| reveal_type(Planet.MERCURY._value_) # revealed: str | ||
| ``` | ||
|
|
||
| ### `__init__` without `_value_` annotation | ||
|
|
||
| When `__init__` is defined but no explicit `_value_` annotation exists, member values are validated | ||
| against the `__init__` signature. Values that are incompatible with `__init__` are flagged: | ||
|
|
||
| ```py | ||
| from enum import Enum | ||
|
|
||
| class Planet2(Enum): | ||
| def __init__(self, mass: float, radius: float): | ||
| self.mass = mass | ||
| self.radius = radius | ||
|
|
||
| MERCURY = (3.303e23, 2.4397e6) | ||
| VENUS = (4.869e24, 6.0518e6) | ||
| INVALID = "not a planet" # error: [invalid-assignment] | ||
|
|
||
| reveal_type(Planet2.MERCURY.value) # revealed: Any | ||
| reveal_type(Planet2.MERCURY._value_) # revealed: Any | ||
| ``` | ||
|
|
||
| ### Inherited `_value_` annotation | ||
|
|
||
| A `_value_` annotation on a parent enum is inherited by subclasses. Member values are validated | ||
| against the inherited annotation, and `.value` uses the declared type: | ||
|
|
||
| ```py | ||
| from enum import Enum | ||
|
|
||
| class Base(Enum): | ||
| _value_: int | ||
|
|
||
| class Child(Base): | ||
| A = 1 | ||
| B = "not an int" # error: [invalid-assignment] | ||
|
|
||
| reveal_type(Child.A.value) # revealed: int | ||
| ``` | ||
|
|
||
| This also works through multiple levels of inheritance, where `_value_` is declared on an | ||
| intermediate class: | ||
|
|
||
| ```py | ||
| from enum import Enum | ||
|
|
||
| class Grandparent(Enum): | ||
| pass | ||
|
|
||
| class Parent(Grandparent): | ||
| _value_: int | ||
|
|
||
| class Child(Parent): | ||
| A = 1 | ||
| B = "not an int" # error: [invalid-assignment] | ||
|
|
||
| reveal_type(Child.A.value) # revealed: int | ||
| ``` | ||
|
|
||
| ### Inherited `__init__` | ||
|
|
||
| A custom `__init__` on a parent enum is inherited by subclasses. Member values are validated against | ||
| the inherited `__init__` signature: | ||
|
|
||
| ```py | ||
| from enum import Enum | ||
|
|
||
| class Base(Enum): | ||
| def __init__(self, a: int, b: str): | ||
| self._value_ = a | ||
|
|
||
| class Child(Base): | ||
| A = (1, "foo") | ||
| B = "should be checked against __init__" # error: [invalid-assignment] | ||
|
|
||
| reveal_type(Child.A.value) # revealed: Any | ||
| ``` | ||
|
|
||
| This also works through multiple levels of inheritance: | ||
|
|
||
| ```py | ||
| from enum import Enum | ||
|
|
||
| class Grandparent(Enum): | ||
| def __init__(self, a: int, b: str): | ||
| self._value_ = a | ||
|
|
||
| class Parent(Grandparent): | ||
| pass | ||
|
|
||
| class Child(Parent): | ||
| A = (1, "foo") | ||
| B = "bad" # error: [invalid-assignment] | ||
|
|
||
| reveal_type(Child.A.value) # revealed: Any | ||
| ``` | ||
|
|
||
| ### Non-member attributes with disallowed type | ||
|
|
||
| Methods, callables, descriptors (including properties), and nested classes that are defined in the | ||
|
|
@@ -358,7 +527,8 @@ class SingleMember(StrEnum): | |
| reveal_type(SingleMember.SINGLE.value) # revealed: Literal["single"] | ||
| ``` | ||
|
|
||
| Using `auto()` with `IntEnum` also works as expected: | ||
| Using `auto()` with `IntEnum` also works as expected. `IntEnum` declares `_value_: int` in typeshed, | ||
| so `.value` is typed as `int` rather than a precise literal: | ||
|
|
||
| ```py | ||
| from enum import IntEnum, auto | ||
|
|
@@ -367,8 +537,8 @@ class Answer(IntEnum): | |
| YES = auto() | ||
| NO = auto() | ||
|
|
||
| reveal_type(Answer.YES.value) # revealed: Literal[1] | ||
| reveal_type(Answer.NO.value) # revealed: Literal[2] | ||
| reveal_type(Answer.YES.value) # revealed: int | ||
| reveal_type(Answer.NO.value) # revealed: int | ||
| ``` | ||
|
|
||
| As does using `auto()` for other enums that use `int` as a mixin: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.