Skip to content

Commit

Permalink
Enum private attributes are not enum members (#17182)
Browse files Browse the repository at this point in the history
Fixes #17098
  • Loading branch information
hamdanal committed Apr 27, 2024
1 parent 8bc7966 commit ba6febc
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 5 deletions.
4 changes: 2 additions & 2 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -1139,8 +1139,8 @@ def analyze_enum_class_attribute_access(
# Skip these since Enum will remove it
if name in ENUM_REMOVED_PROPS:
return report_missing_attribute(mx.original_type, itype, name, mx)
# For other names surrendered by underscores, we don't make them Enum members
if name.startswith("__") and name.endswith("__") and name.replace("_", "") != "":
# Dunders and private names are not Enum members
if name.startswith("__") and name.replace("_", "") != "":
return None

enum_literal = LiteralType(name, fallback=itype)
Expand Down
7 changes: 6 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3979,7 +3979,12 @@ def analyze_name_lvalue(
existing = names.get(name)

outer = self.is_global_or_nonlocal(name)
if kind == MDEF and isinstance(self.type, TypeInfo) and self.type.is_enum:
if (
kind == MDEF
and isinstance(self.type, TypeInfo)
and self.type.is_enum
and not name.startswith("__")
):
# Special case: we need to be sure that `Enum` keys are unique.
if existing is not None and not isinstance(existing.node, PlaceholderNode):
self.fail(
Expand Down
7 changes: 6 additions & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,12 @@ def analyze_unbound_type_without_type_info(
# If, in the distant future, we decide to permit things like
# `def foo(x: Color.RED) -> None: ...`, we can remove that
# check entirely.
if isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum:
if (
isinstance(sym.node, Var)
and sym.node.info
and sym.node.info.is_enum
and not sym.node.name.startswith("__")
):
value = sym.node.name
base_enum_short_name = sym.node.info.name
if not defining_literal:
Expand Down
3 changes: 3 additions & 0 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,9 @@ class Status(Enum):
# Skip these since Enum will remove it
if name in ENUM_REMOVED_PROPS:
continue
# Skip private attributes
if name.startswith("__"):
continue
new_items.append(LiteralType(name, typ))
return make_simplified_union(new_items, contract_literals=False)
elif typ.type.fullname == "builtins.bool":
Expand Down
33 changes: 33 additions & 0 deletions test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,10 @@ from enum import Enum
class Correct(Enum):
x = 'y'
y = 'x'
class Correct2(Enum):
x = 'y'
__z = 'y'
__z = 'x'
class Foo(Enum):
A = 1
A = 'a' # E: Attempted to reuse member name "A" in Enum definition "Foo" \
Expand Down Expand Up @@ -2105,3 +2109,32 @@ class AllPartialList(Enum):

def check(self) -> None:
reveal_type(self.value) # N: Revealed type is "builtins.list[Any]"

[case testEnumPrivateAttributeNotMember]
from enum import Enum

class MyEnum(Enum):
A = 1
B = 2
__my_dict = {A: "ham", B: "spam"}

# TODO: change the next line to use MyEnum._MyEnum__my_dict when mypy implements name mangling
x: MyEnum = MyEnum.__my_dict # E: Incompatible types in assignment (expression has type "Dict[int, str]", variable has type "MyEnum")

[case testEnumWithPrivateAttributeReachability]
# flags: --warn-unreachable
from enum import Enum

class MyEnum(Enum):
A = 1
B = 2
__my_dict = {A: "ham", B: "spam"}

e: MyEnum
if e == MyEnum.A:
reveal_type(e) # N: Revealed type is "Literal[__main__.MyEnum.A]"
elif e == MyEnum.B:
reveal_type(e) # N: Revealed type is "Literal[__main__.MyEnum.B]"
else:
reveal_type(e) # E: Statement is unreachable
[builtins fixtures/dict.pyi]
4 changes: 3 additions & 1 deletion test-data/unit/check-literal.test
Original file line number Diff line number Diff line change
Expand Up @@ -2503,7 +2503,7 @@ class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3

__ROUGE = RED
def func(self) -> int: pass

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

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

0 comments on commit ba6febc

Please sign in to comment.