Repository: python/mypy

Issue:
An enum attribute with a private (mangled) name should not be considered an enum member
This is based on [a bug report](https://github.com/microsoft/pyright/issues/7619) that someone just filed against pyright. Mypy appears to have the same bug.

The enum documentation indicates that an attribute with a private (mangled) name is not treated as an enum member.

```python
from enum import Enum

class MyEnum(Enum):
    A = 1
    B = 2
    C = 3

    __my_dict = {A: "ham", B: "spam", C: "egg"}

# Should reveal `dict[MyEnum, str]`, not `Literal[MyEnum.__my_dict]`
reveal_type(MyEnum.__my_dict)
```


Discussion:

Patch:
diff --git a/mypy/checkmember.py b/mypy/checkmember.py
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -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)
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -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(
diff --git a/mypy/typeanal.py b/mypy/typeanal.py
--- a/mypy/typeanal.py
+++ b/mypy/typeanal.py
@@ -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:
diff --git a/mypy/typeops.py b/mypy/typeops.py
--- a/mypy/typeops.py
+++ b/mypy/typeops.py
@@ -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":


Test patch:
diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test
--- a/test-data/unit/check-enum.test
+++ b/test-data/unit/check-enum.test
@@ -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" \
@@ -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]
diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test
--- a/test-data/unit/check-literal.test
+++ b/test-data/unit/check-literal.test
@@ -2503,7 +2503,7 @@ class Color(Enum):
     RED = 1
     GREEN = 2
     BLUE = 3
-
+    __ROUGE = RED
     def func(self) -> int: pass
 
 r: Literal[Color.RED]
@@ -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