Skip to content

Commit ba6febc

Browse files
authored
Enum private attributes are not enum members (#17182)
Fixes #17098
1 parent 8bc7966 commit ba6febc

File tree

6 files changed

+53
-5
lines changed

6 files changed

+53
-5
lines changed

mypy/checkmember.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,8 +1139,8 @@ def analyze_enum_class_attribute_access(
11391139
# Skip these since Enum will remove it
11401140
if name in ENUM_REMOVED_PROPS:
11411141
return report_missing_attribute(mx.original_type, itype, name, mx)
1142-
# For other names surrendered by underscores, we don't make them Enum members
1143-
if name.startswith("__") and name.endswith("__") and name.replace("_", "") != "":
1142+
# Dunders and private names are not Enum members
1143+
if name.startswith("__") and name.replace("_", "") != "":
11441144
return None
11451145

11461146
enum_literal = LiteralType(name, fallback=itype)

mypy/semanal.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3979,7 +3979,12 @@ def analyze_name_lvalue(
39793979
existing = names.get(name)
39803980

39813981
outer = self.is_global_or_nonlocal(name)
3982-
if kind == MDEF and isinstance(self.type, TypeInfo) and self.type.is_enum:
3982+
if (
3983+
kind == MDEF
3984+
and isinstance(self.type, TypeInfo)
3985+
and self.type.is_enum
3986+
and not name.startswith("__")
3987+
):
39833988
# Special case: we need to be sure that `Enum` keys are unique.
39843989
if existing is not None and not isinstance(existing.node, PlaceholderNode):
39853990
self.fail(

mypy/typeanal.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,12 @@ def analyze_unbound_type_without_type_info(
868868
# If, in the distant future, we decide to permit things like
869869
# `def foo(x: Color.RED) -> None: ...`, we can remove that
870870
# check entirely.
871-
if isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum:
871+
if (
872+
isinstance(sym.node, Var)
873+
and sym.node.info
874+
and sym.node.info.is_enum
875+
and not sym.node.name.startswith("__")
876+
):
872877
value = sym.node.name
873878
base_enum_short_name = sym.node.info.name
874879
if not defining_literal:

mypy/typeops.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,9 @@ class Status(Enum):
885885
# Skip these since Enum will remove it
886886
if name in ENUM_REMOVED_PROPS:
887887
continue
888+
# Skip private attributes
889+
if name.startswith("__"):
890+
continue
888891
new_items.append(LiteralType(name, typ))
889892
return make_simplified_union(new_items, contract_literals=False)
890893
elif typ.type.fullname == "builtins.bool":

test-data/unit/check-enum.test

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,10 @@ from enum import Enum
14251425
class Correct(Enum):
14261426
x = 'y'
14271427
y = 'x'
1428+
class Correct2(Enum):
1429+
x = 'y'
1430+
__z = 'y'
1431+
__z = 'x'
14281432
class Foo(Enum):
14291433
A = 1
14301434
A = 'a' # E: Attempted to reuse member name "A" in Enum definition "Foo" \
@@ -2105,3 +2109,32 @@ class AllPartialList(Enum):
21052109

21062110
def check(self) -> None:
21072111
reveal_type(self.value) # N: Revealed type is "builtins.list[Any]"
2112+
2113+
[case testEnumPrivateAttributeNotMember]
2114+
from enum import Enum
2115+
2116+
class MyEnum(Enum):
2117+
A = 1
2118+
B = 2
2119+
__my_dict = {A: "ham", B: "spam"}
2120+
2121+
# TODO: change the next line to use MyEnum._MyEnum__my_dict when mypy implements name mangling
2122+
x: MyEnum = MyEnum.__my_dict # E: Incompatible types in assignment (expression has type "Dict[int, str]", variable has type "MyEnum")
2123+
2124+
[case testEnumWithPrivateAttributeReachability]
2125+
# flags: --warn-unreachable
2126+
from enum import Enum
2127+
2128+
class MyEnum(Enum):
2129+
A = 1
2130+
B = 2
2131+
__my_dict = {A: "ham", B: "spam"}
2132+
2133+
e: MyEnum
2134+
if e == MyEnum.A:
2135+
reveal_type(e) # N: Revealed type is "Literal[__main__.MyEnum.A]"
2136+
elif e == MyEnum.B:
2137+
reveal_type(e) # N: Revealed type is "Literal[__main__.MyEnum.B]"
2138+
else:
2139+
reveal_type(e) # E: Statement is unreachable
2140+
[builtins fixtures/dict.pyi]

test-data/unit/check-literal.test

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2503,7 +2503,7 @@ class Color(Enum):
25032503
RED = 1
25042504
GREEN = 2
25052505
BLUE = 3
2506-
2506+
__ROUGE = RED
25072507
def func(self) -> int: pass
25082508

25092509
r: Literal[Color.RED]
@@ -2512,6 +2512,8 @@ b: Literal[Color.BLUE]
25122512
bad1: Literal[Color] # E: Parameter 1 of Literal[...] is invalid
25132513
bad2: Literal[Color.func] # E: Parameter 1 of Literal[...] is invalid
25142514
bad3: Literal[Color.func()] # E: Invalid type: Literal[...] cannot contain arbitrary expressions
2515+
# TODO: change the next line to use Color._Color__ROUGE when mypy implements name mangling
2516+
bad4: Literal[Color.__ROUGE] # E: Parameter 1 of Literal[...] is invalid
25152517

25162518
def expects_color(x: Color) -> None: pass
25172519
def expects_red(x: Literal[Color.RED]) -> None: pass

0 commit comments

Comments
 (0)