Skip to content

Commit 7e09c2a

Browse files
Petter FribergJukkaL
Petter Friberg
authored andcommitted
Support overriding dunder attributes in Enum subclass (#12138)
Allows any dunder (`__name__`) attributes except `__members__` (due to it being read-only) to be overridden in enum subclasses. Fixes #12132.
1 parent 837543e commit 7e09c2a

File tree

4 files changed

+79
-7
lines changed

4 files changed

+79
-7
lines changed

mypy/checker.py

+19-5
Original file line numberDiff line numberDiff line change
@@ -1829,11 +1829,7 @@ def visit_class_def(self, defn: ClassDef) -> None:
18291829
if typ.is_protocol and typ.defn.type_vars:
18301830
self.check_protocol_variance(defn)
18311831
if not defn.has_incompatible_baseclass and defn.info.is_enum:
1832-
for base in defn.info.mro[1:-1]: # we don't need self and `object`
1833-
if base.is_enum and base.fullname not in ENUM_BASES:
1834-
self.check_final_enum(defn, base)
1835-
self.check_enum_bases(defn)
1836-
self.check_enum_new(defn)
1832+
self.check_enum(defn)
18371833

18381834
def check_final_deletable(self, typ: TypeInfo) -> None:
18391835
# These checks are only for mypyc. Only perform some checks that are easier
@@ -1891,6 +1887,24 @@ def check_init_subclass(self, defn: ClassDef) -> None:
18911887
# all other bases have already been checked.
18921888
break
18931889

1890+
def check_enum(self, defn: ClassDef) -> None:
1891+
assert defn.info.is_enum
1892+
if defn.info.fullname not in ENUM_BASES:
1893+
for sym in defn.info.names.values():
1894+
if (isinstance(sym.node, Var) and sym.node.has_explicit_value and
1895+
sym.node.name == '__members__'):
1896+
# `__members__` will always be overwritten by `Enum` and is considered
1897+
# read-only so we disallow assigning a value to it
1898+
self.fail(
1899+
message_registry.ENUM_MEMBERS_ATTR_WILL_BE_OVERRIDEN, sym.node
1900+
)
1901+
for base in defn.info.mro[1:-1]: # we don't need self and `object`
1902+
if base.is_enum and base.fullname not in ENUM_BASES:
1903+
self.check_final_enum(defn, base)
1904+
1905+
self.check_enum_bases(defn)
1906+
self.check_enum_new(defn)
1907+
18941908
def check_final_enum(self, defn: ClassDef, base: TypeInfo) -> None:
18951909
for sym in base.names.values():
18961910
if self.is_final_enum_value(sym):

mypy/message_registry.py

+5
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,11 @@ def format(self, *args: object, **kwargs: object) -> "ErrorMessage":
204204
)
205205
CANNOT_MAKE_DELETABLE_FINAL: Final = ErrorMessage("Deletable attribute cannot be final")
206206

207+
# Enum
208+
ENUM_MEMBERS_ATTR_WILL_BE_OVERRIDEN: Final = ErrorMessage(
209+
'Assigned "__members__" will be overriden by "Enum" internally'
210+
)
211+
207212
# ClassVar
208213
CANNOT_OVERRIDE_INSTANCE_VAR: Final = ErrorMessage(
209214
'Cannot override instance variable (previously declared on base class "{}") with class '

mypy/semanal.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
)
117117
from mypy.util import (
118118
correct_relative_import, unmangle, module_prefix, is_typeshed_file, unnamed_function,
119+
is_dunder,
119120
)
120121
from mypy.scope import Scope
121122
from mypy.semanal_shared import (
@@ -2473,8 +2474,9 @@ def store_final_status(self, s: AssignmentStmt) -> None:
24732474
cur_node = self.type.names.get(lval.name, None)
24742475
if (cur_node and isinstance(cur_node.node, Var) and
24752476
not (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs)):
2476-
cur_node.node.is_final = True
2477-
s.is_final_def = True
2477+
# Double underscored members are writable on an `Enum`.
2478+
# (Except read-only `__members__` but that is handled in type checker)
2479+
cur_node.node.is_final = s.is_final_def = not is_dunder(cur_node.node.name)
24782480

24792481
# Special case: deferred initialization of a final attribute in __init__.
24802482
# In this case we just pretend this is a valid final definition to suppress

test-data/unit/check-enum.test

+51
Original file line numberDiff line numberDiff line change
@@ -2029,3 +2029,54 @@ class C(Enum):
20292029

20302030
C._ignore_ # E: "Type[C]" has no attribute "_ignore_"
20312031
[typing fixtures/typing-medium.pyi]
2032+
2033+
[case testCanOverrideDunderAttributes]
2034+
import typing
2035+
from enum import Enum, Flag
2036+
2037+
class BaseEnum(Enum):
2038+
__dunder__ = 1
2039+
__labels__: typing.Dict[int, str]
2040+
2041+
class Override(BaseEnum):
2042+
__dunder__ = 2
2043+
__labels__ = {1: "1"}
2044+
2045+
Override.__dunder__ = 3
2046+
BaseEnum.__dunder__ = 3
2047+
Override.__labels__ = {2: "2"}
2048+
2049+
class FlagBase(Flag):
2050+
__dunder__ = 1
2051+
__labels__: typing.Dict[int, str]
2052+
2053+
class FlagOverride(FlagBase):
2054+
__dunder__ = 2
2055+
__labels = {1: "1"}
2056+
2057+
FlagOverride.__dunder__ = 3
2058+
FlagBase.__dunder__ = 3
2059+
FlagOverride.__labels__ = {2: "2"}
2060+
[builtins fixtures/dict.pyi]
2061+
2062+
[case testCanNotInitialize__members__]
2063+
import typing
2064+
from enum import Enum
2065+
2066+
class WritingMembers(Enum):
2067+
__members__: typing.Dict[Enum, Enum] = {} # E: Assigned "__members__" will be overriden by "Enum" internally
2068+
2069+
class OnlyAnnotatedMembers(Enum):
2070+
__members__: typing.Dict[Enum, Enum]
2071+
[builtins fixtures/dict.pyi]
2072+
2073+
[case testCanOverrideDunderOnNonFirstBaseEnum]
2074+
import typing
2075+
from enum import Enum
2076+
2077+
class Some:
2078+
__labels__: typing.Dict[int, str]
2079+
2080+
class A(Some, Enum):
2081+
__labels__ = {1: "1"}
2082+
[builtins fixtures/dict.pyi]

0 commit comments

Comments
 (0)