Skip to content

Commit 9d3a052

Browse files
authored
Support attribute access on enum members correctly (#19422)
Fixes #11368 (apparently canonical). Fixes #10910. Fixes #12107. Fixes #13841. Fixes #15186. Fixes #15454. Fixes #19418. `mypy` now understands attribute access on enum members - the "recursive" behaviour of supporting access of almost-all enum members from members. "Almost", because `.name` and `.value` take precedence even if a member of the same name exists. ```python from enum import Enum class E(Enum): FOO = 1 BAR = 1 # The following is still a `E.BAR` instance: E.FOO.FOO.BAR.BAR ``` Looks like this is a much wanted feature.
1 parent 06e28f8 commit 9d3a052

File tree

4 files changed

+66
-6
lines changed

4 files changed

+66
-6
lines changed

mypy/checkmember.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,18 @@ def analyze_var(
921921
result = AnyType(TypeOfAny.special_form)
922922
fullname = f"{var.info.fullname}.{name}"
923923
hook = mx.chk.plugin.get_attribute_hook(fullname)
924+
925+
if var.info.is_enum and not mx.is_lvalue:
926+
if name in var.info.enum_members and name not in {"name", "value"}:
927+
enum_literal = LiteralType(name, fallback=itype)
928+
result = itype.copy_modified(last_known_value=enum_literal)
929+
elif (
930+
isinstance(p_result := get_proper_type(result), Instance)
931+
and p_result.type.fullname == "enum.nonmember"
932+
and p_result.args
933+
):
934+
# Unwrap nonmember similar to class-level access
935+
result = p_result.args[0]
924936
if result and not (implicit or var.info.is_protocol and is_instance_var(var)):
925937
result = analyze_descriptor_access(result, mx)
926938
if hook:

mypy/plugins/enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def _implements_new(info: TypeInfo) -> bool:
144144
def enum_member_callback(ctx: mypy.plugin.FunctionContext) -> Type:
145145
"""By default `member(1)` will be inferred as `member[int]`,
146146
we want to improve the inference to be `Literal[1]` here."""
147-
if ctx.arg_types or ctx.arg_types[0]:
147+
if ctx.arg_types and ctx.arg_types[0]:
148148
arg = get_proper_type(ctx.arg_types[0][0])
149149
proper_return = get_proper_type(ctx.default_return_type)
150150
if (

test-data/unit/check-enum.test

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,7 +1953,8 @@ class A(Enum):
19531953
x: int
19541954
def method(self) -> int: pass
19551955
class B(A):
1956-
x = 1 # E: Cannot override writable attribute "x" with a final one
1956+
x = 1 # E: Cannot override writable attribute "x" with a final one \
1957+
# E: Incompatible types in assignment (expression has type "B", base class "A" defined the type as "int")
19571958

19581959
class A1(Enum):
19591960
x: int = 1 # E: Enum members must be left unannotated \
@@ -1971,8 +1972,8 @@ class B2(A2): # E: Cannot extend enum with existing members: "A2"
19711972
class A3(Enum):
19721973
x: Final[int] # type: ignore
19731974
class B3(A3):
1974-
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "A3")
1975-
1975+
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "A3") \
1976+
# E: Incompatible types in assignment (expression has type "B3", base class "A3" defined the type as "int")
19761977
[builtins fixtures/bool.pyi]
19771978

19781979
[case testEnumNotFinalWithMethodsAndUninitializedValuesStub]
@@ -1984,14 +1985,16 @@ class A(Enum): # E: Detected enum "lib.A" in a type stub with zero members. The
19841985
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members
19851986
x: int
19861987
class B(A):
1987-
x = 1 # E: Cannot override writable attribute "x" with a final one
1988+
x = 1 # E: Cannot override writable attribute "x" with a final one \
1989+
# E: Incompatible types in assignment (expression has type "B", base class "A" defined the type as "int")
19881990

19891991
class C(Enum):
19901992
x = 1
19911993
class D(C): # E: Cannot extend enum with existing members: "C" \
19921994
# E: Detected enum "lib.D" in a type stub with zero members. There is a chance this is due to a recent change in the semantics of enum membership. If so, use `member = value` to mark an enum member, instead of `member: type` \
19931995
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members
1994-
x: int # E: Cannot assign to final name "x"
1996+
x: int # E: Incompatible types in assignment (expression has type "int", base class "C" defined the type as "C") \
1997+
# E: Cannot assign to final name "x"
19951998
[builtins fixtures/bool.pyi]
19961999

19972000
[case testEnumNotFinalWithMethodsAndUninitializedValuesStubMember]
@@ -2419,6 +2422,49 @@ def some_a(a: A):
24192422
reveal_type(a) # N: Revealed type is "Literal[__main__.A.x]"
24202423
[builtins fixtures/dict.pyi]
24212424

2425+
[case testEnumAccessFromInstance]
2426+
# flags: --python-version 3.11 --warn-unreachable
2427+
# This was added in 3.11
2428+
from enum import Enum, member, nonmember
2429+
2430+
class A(Enum):
2431+
x = 1
2432+
y = member(2)
2433+
z = nonmember(3)
2434+
2435+
reveal_type(A.x) # N: Revealed type is "Literal[__main__.A.x]?"
2436+
reveal_type(A.y) # N: Revealed type is "Literal[__main__.A.y]?"
2437+
reveal_type(A.z) # N: Revealed type is "builtins.int"
2438+
2439+
reveal_type(A.x.x) # N: Revealed type is "Literal[__main__.A.x]?"
2440+
reveal_type(A.x.x.x) # N: Revealed type is "Literal[__main__.A.x]?"
2441+
reveal_type(A.x.y) # N: Revealed type is "Literal[__main__.A.y]?"
2442+
reveal_type(A.x.y.y) # N: Revealed type is "Literal[__main__.A.y]?"
2443+
reveal_type(A.x.z) # N: Revealed type is "builtins.int"
2444+
2445+
reveal_type(A.y.x) # N: Revealed type is "Literal[__main__.A.x]?"
2446+
reveal_type(A.y.y) # N: Revealed type is "Literal[__main__.A.y]?"
2447+
reveal_type(A.y.z) # N: Revealed type is "builtins.int"
2448+
2449+
A.z.x # E: "int" has no attribute "x"
2450+
2451+
class B(Enum):
2452+
x = 1
2453+
value = 2
2454+
2455+
reveal_type(B.x) # N: Revealed type is "Literal[__main__.B.x]?"
2456+
reveal_type(B.x.value) # N: Revealed type is "Literal[2]?"
2457+
reveal_type(B.x.x.value) # N: Revealed type is "Literal[2]?"
2458+
B.x.value.value # E: "int" has no attribute "value"
2459+
B.x.value.value.value # E: "int" has no attribute "value"
2460+
reveal_type(B.value) # N: Revealed type is "Literal[__main__.B.value]?"
2461+
reveal_type(B.value.x) # N: Revealed type is "Literal[__main__.B.x]?"
2462+
reveal_type(B.value.x.x) # N: Revealed type is "Literal[__main__.B.x]?"
2463+
reveal_type(B.value.x.value) # N: Revealed type is "Literal[2]?"
2464+
B.value.x.value.value # E: "int" has no attribute "value"
2465+
B.value.value.value # E: "int" has no attribute "value"
2466+
[builtins fixtures/dict.pyi]
2467+
24222468

24232469
[case testErrorOnAnnotatedMember]
24242470
from enum import Enum

test-data/unit/check-incremental.test

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5675,10 +5675,12 @@ class FinalEnum(Enum):
56755675
[builtins fixtures/isinstance.pyi]
56765676
[out]
56775677
main:3: error: Cannot override writable attribute "x" with a final one
5678+
main:3: error: Incompatible types in assignment (expression has type "Ok", base class "RegularEnum" defined the type as "int")
56785679
main:4: error: Cannot extend enum with existing members: "FinalEnum"
56795680
main:5: error: Cannot override final attribute "x" (previously declared in base class "FinalEnum")
56805681
[out2]
56815682
main:3: error: Cannot override writable attribute "x" with a final one
5683+
main:3: error: Incompatible types in assignment (expression has type "Ok", base class "RegularEnum" defined the type as "int")
56825684
main:4: error: Cannot extend enum with existing members: "FinalEnum"
56835685
main:5: error: Cannot override final attribute "x" (previously declared in base class "FinalEnum")
56845686

0 commit comments

Comments
 (0)