From ba6febc903776491ea445cef2ef5375b95e178cd Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sun, 28 Apr 2024 00:46:32 +0300 Subject: [PATCH] Enum private attributes are not enum members (#17182) Fixes #17098 --- mypy/checkmember.py | 4 ++-- mypy/semanal.py | 7 ++++++- mypy/typeanal.py | 7 ++++++- mypy/typeops.py | 3 +++ test-data/unit/check-enum.test | 33 +++++++++++++++++++++++++++++++ test-data/unit/check-literal.test | 4 +++- 6 files changed, 53 insertions(+), 5 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 64d6733f5309..5824b00a37f6 100644 --- 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 index 1fc58a6c11f1..91a6b1808987 100644 --- 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 index c2c578045297..5cde7da721ec 100644 --- 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 index 5b396308d955..a59bd3739562 100644 --- 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": diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index b4e8795859c3..e8e65f464eaf 100644 --- 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 index 3cf6e8ff17e9..423ba74eba72 100644 --- 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