Repository: python/mypy

Issue:
Private property in subclass of Enum is not indexable
Hi,

* Are you reporting a bug, or opening a feature request?
Reporting a minor bug in `--strict` mode when adding private property to Enum subclass.

```python
import operator
import enum
from typing import Callable

@enum.unique
class Comparator(enum.Enum):
    LessThan = "<"
    LessThanOrEqualTo = "<="
    EqualTo = "=="
    NotEqualTo = "!="
    GreaterThanOrEqualTo = ">="
    GreaterThan = ">"

    __operator__ = {
        LessThan: operator.lt,
        LessThanOrEqualTo: operator.le,
        EqualTo: operator.eq,
        NotEqualTo: operator.ne,
        GreaterThanOrEqualTo: operator.ge,
        GreaterThan: operator.gt
    }

    @property
    def operator(self) -> Callable[[object, object], bool]:
        return Comparator.__operator__[self.value]

Comparator.LessThan.operator(1,2)
Comparator("<").operator(1,2)
```
* What is the actual behavior/output?
The output is the following, where the first line is a missing stub for `operator`, and the second is the error in this report:
```
<string>:26: warning: Returning Any from function declared to return "Callable[[object, object], bool]"
<string>:26: error: Value of type "Comparator" is not indexable
```
* What is the behavior/output you expect?
`<string>:26: error: Value of type "Comparator" is not indexable` should not appear.

* What are the versions of mypy and Python you are using?
mypy: 0.610
python: 3.6.5
Could not run mypy from master

* What are the mypy flags you are using? (For example --strict-optional)
`--strict`






Discussion:
Probably what's happening here is that mypy doesn't implement the `enum` feature where names surrounded by underscores don't turn into enum members. This should be relatively easy to fix.

Resolving patch:
diff --git a/mypy/checkmember.py b/mypy/checkmember.py
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -705,19 +705,9 @@ def analyze_class_attribute_access(itype: Instance,
         check_final_member(name, info, mx.msg, mx.context)
 
     if info.is_enum and not (mx.is_lvalue or is_decorated or is_method):
-        # Skip "_order_" and "__order__", since Enum will remove it
-        if name in ("_order_", "__order__"):
-            return mx.msg.has_no_attr(
-                mx.original_type, itype, name, mx.context, mx.module_symbol_table
-            )
-
-        enum_literal = LiteralType(name, fallback=itype)
-        # When we analyze enums, the corresponding Instance is always considered to be erased
-        # due to how the signature of Enum.__new__ is `(cls: Type[_T], value: object) -> _T`
-        # in typeshed. However, this is really more of an implementation detail of how Enums
-        # are typed, and we really don't want to treat every single Enum value as if it were
-        # from type variable substitution. So we reset the 'erased' field here.
-        return itype.copy_modified(erased=False, last_known_value=enum_literal)
+        enum_class_attribute_type = analyze_enum_class_attribute_access(itype, name, mx)
+        if enum_class_attribute_type:
+            return enum_class_attribute_type
 
     t = node.type
     if t:
@@ -815,6 +805,28 @@ def analyze_class_attribute_access(itype: Instance,
         return typ
 
 
+def analyze_enum_class_attribute_access(itype: Instance,
+                                        name: str,
+                                        mx: MemberContext,
+                                        ) -> Optional[Type]:
+    # Skip "_order_" and "__order__", since Enum will remove it
+    if name in ("_order_", "__order__"):
+        return mx.msg.has_no_attr(
+            mx.original_type, itype, name, mx.context, mx.module_symbol_table
+        )
+    # For other names surrendered by underscores, we don't make them Enum members
+    if name.startswith('__') and name.endswith("__") and name.replace('_', '') != '':
+        return None
+
+    enum_literal = LiteralType(name, fallback=itype)
+    # When we analyze enums, the corresponding Instance is always considered to be erased
+    # due to how the signature of Enum.__new__ is `(cls: Type[_T], value: object) -> _T`
+    # in typeshed. However, this is really more of an implementation detail of how Enums
+    # are typed, and we really don't want to treat every single Enum value as if it were
+    # from type variable substitution. So we reset the 'erased' field here.
+    return itype.copy_modified(erased=False, last_known_value=enum_literal)
+
+
 def add_class_tvars(t: ProperType, isuper: Optional[Instance],
                     is_classmethod: bool,
                     original_type: Type,


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
@@ -1136,3 +1136,29 @@ else:
     reveal_type(x4)  # N: Revealed type is '__main__.Foo'
     reveal_type(x5)  # N: Revealed type is '__main__.Foo'
 [builtins fixtures/primitives.pyi]
+
+[case testPrivateAttributeNotAsEnumMembers]
+import enum
+
+class Comparator(enum.Enum):
+    LessThan = "<"
+    LessThanOrEqualTo = "<="
+    EqualTo = "=="
+    NotEqualTo = "!="
+    GreaterThanOrEqualTo = ">="
+    GreaterThan = ">"
+
+    __foo__ = {
+        LessThan: 1,
+        LessThanOrEqualTo: 2,
+        EqualTo: 3,
+        NotEqualTo: 4,
+        GreaterThanOrEqualTo: 5,
+        GreaterThan: 6,
+    }
+
+    def foo(self) -> int:
+        return Comparator.__foo__[self.value]
+
+reveal_type(Comparator.__foo__)  # N: Revealed type is 'builtins.dict[builtins.str, builtins.int]'
+[builtins fixtures/dict.pyi]