From 412c03a6d94a431d0dc9358951002ff98f18311f Mon Sep 17 00:00:00 2001 From: STerliakov Date: Tue, 19 Aug 2025 13:28:38 +0200 Subject: [PATCH 1/5] Do not treat unassigned attributes as enum members --- mypy/checkmember.py | 4 ++++ mypy/nodes.py | 1 + mypy/typeanal.py | 2 +- test-data/unit/check-enum.test | 20 +++++++++++++++++++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 2c41f2e273cc..77423d5dd1f1 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -1342,6 +1342,7 @@ def apply_class_attr_hook( def analyze_enum_class_attribute_access( itype: Instance, name: str, mx: MemberContext ) -> Type | None: + # This function should be kept in sync with TypeInfo.enum_members # Skip these since Enum will remove it if name in EXCLUDED_ENUM_ATTRIBUTES: return report_missing_attribute(mx.original_type, itype, name, mx) @@ -1350,6 +1351,9 @@ def analyze_enum_class_attribute_access( return None node = itype.type.get(name) + if not isinstance(node.node, Var) or not node.node.has_explicit_value: + # Annotated but not assigned attributes are not enum members + return None if node and node.type: proper = get_proper_type(node.type) # Support `A = nonmember(1)` function call and decorator. diff --git a/mypy/nodes.py b/mypy/nodes.py index 99b9bf72c948..7c418970e6a7 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3317,6 +3317,7 @@ def protocol_members(self) -> list[str]: @property def enum_members(self) -> list[str]: + # This method should be kept in sync with checkmember.analyze_enum_class_attribute_access # TODO: cache the results? members = [] for name, sym in self.names.items(): diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 204d3061c734..afe4a387f83d 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -959,7 +959,7 @@ def analyze_unbound_type_without_type_info( isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum - and not sym.node.name.startswith("__") + and sym.node.name in sym.node.info.enum_members ): value = sym.node.name base_enum_short_name = sym.node.info.name diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index 3bcf9745a801..070c86484869 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -457,7 +457,8 @@ class F(Generic[T], Enum): # E: Enum class cannot be generic x: T y: T -reveal_type(F[int].x) # N: Revealed type is "__main__.F[builtins.int]" +reveal_type(F[int].x) # E: Access to generic instance variables via class is ambiguous \ + # N: Revealed type is "builtins.int" [builtins fixtures/enum.pyi] [case testEnumFlag] @@ -1422,6 +1423,23 @@ class Comparator(enum.Enum): reveal_type(Comparator.__foo__) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" [builtins fixtures/dict.pyi] +[case testEnumClassAttributeUnannotated] +import enum +from typing import ClassVar, Literal + +class MyEnum(enum.Enum): + foo: ClassVar[str] + bar: str + + VALUE_A = 1 + VALUE_B = 2 + +reveal_type(MyEnum.foo) # N: Revealed type is "builtins.str" +reveal_type(MyEnum.bar) # N: Revealed type is "builtins.str" +x: Literal[MyEnum.foo] # E: Parameter 1 of Literal[...] is invalid +y: Literal[MyEnum.bar] # E: Parameter 1 of Literal[...] is invalid +[builtins fixtures/enum.pyi] + [case testEnumWithInstanceAttributes] from enum import Enum class Foo(Enum): From c381ea3fa6cd37d67bb8143debcfd2095a53c681 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Tue, 19 Aug 2025 13:31:48 +0200 Subject: [PATCH 2/5] selfcheck fix --- mypy/checkmember.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 77423d5dd1f1..91d8de796595 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -1351,10 +1351,10 @@ def analyze_enum_class_attribute_access( return None node = itype.type.get(name) - if not isinstance(node.node, Var) or not node.node.has_explicit_value: + if not node or not isinstance(node.node, Var) or not node.node.has_explicit_value: # Annotated but not assigned attributes are not enum members return None - if node and node.type: + if node.type: proper = get_proper_type(node.type) # Support `A = nonmember(1)` function call and decorator. if ( From df9cfd8a60cd0897bf27ca0b7258e3954df7c9f2 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Tue, 19 Aug 2025 13:47:37 +0200 Subject: [PATCH 3/5] Sync with `analyze_enum_class_attribute_access` --- mypy/checkmember.py | 7 +------ mypy/nodes.py | 1 - test-data/unit/check-enum.test | 30 ++++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 91d8de796595..fa09216052d7 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -1342,18 +1342,13 @@ def apply_class_attr_hook( def analyze_enum_class_attribute_access( itype: Instance, name: str, mx: MemberContext ) -> Type | None: - # This function should be kept in sync with TypeInfo.enum_members # Skip these since Enum will remove it if name in EXCLUDED_ENUM_ATTRIBUTES: return report_missing_attribute(mx.original_type, itype, name, mx) - # Dunders and private names are not Enum members - if name.startswith("__") and name.replace("_", "") != "": + if name not in itype.type.enum_members: return None node = itype.type.get(name) - if not node or not isinstance(node.node, Var) or not node.node.has_explicit_value: - # Annotated but not assigned attributes are not enum members - return None if node.type: proper = get_proper_type(node.type) # Support `A = nonmember(1)` function call and decorator. diff --git a/mypy/nodes.py b/mypy/nodes.py index 7c418970e6a7..99b9bf72c948 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3317,7 +3317,6 @@ def protocol_members(self) -> list[str]: @property def enum_members(self) -> list[str]: - # This method should be kept in sync with checkmember.analyze_enum_class_attribute_access # TODO: cache the results? members = [] for name, sym in self.names.items(): diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index 070c86484869..95105905806f 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -1440,6 +1440,36 @@ x: Literal[MyEnum.foo] # E: Parameter 1 of Literal[...] is invalid y: Literal[MyEnum.bar] # E: Parameter 1 of Literal[...] is invalid [builtins fixtures/enum.pyi] +[case testEnumAttributesInheritedFromMixin] +from enum import Enum +from typing import TYPE_CHECKING, ClassVar, Final, Literal + +class A: + var1 = 1 + var2: ClassVar[str] + var3: Final[int] = 3 + var4: str + +class E(A, Enum): + mem = 1 + +reveal_type(E.var1) # N: Revealed type is "builtins.int" +reveal_type(E.var2) # N: Revealed type is "builtins.str" +reveal_type(E.var3) # N: Revealed type is "builtins.int" +reveal_type(E.var4) # N: Revealed type is "builtins.str" +reveal_type(E.mem) # N: Revealed type is "Literal[__main__.E.mem]?" + +E.var1.value # E: "int" has no attribute "value" +E.var2.name # E: "str" has no attribute "name" +E.mem.name + +x1: Literal[E.var1] # E: Parameter 1 of Literal[...] is invalid +x2: Literal[E.var2] # E: Parameter 1 of Literal[...] is invalid +x3: Literal[E.var3] # E: Parameter 1 of Literal[...] is invalid +x4: Literal[E.var4] # E: Parameter 1 of Literal[...] is invalid +m: Literal[E.mem] +[builtins fixtures/enum.pyi] + [case testEnumWithInstanceAttributes] from enum import Enum class Foo(Enum): From 592f9d04ab502e7e5a0fc0aa23611b4e39f58500 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Tue, 19 Aug 2025 13:51:10 +0200 Subject: [PATCH 4/5] And type fix again --- mypy/checkmember.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index fa09216052d7..4fd97d567237 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -1349,7 +1349,7 @@ def analyze_enum_class_attribute_access( return None node = itype.type.get(name) - if node.type: + if node and node.type: proper = get_proper_type(node.type) # Support `A = nonmember(1)` function call and decorator. if ( From bb5139143e3fd5ee8e8dff14a77dca7094392718 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Tue, 19 Aug 2025 14:01:32 +0200 Subject: [PATCH 5/5] Ough, we should unwrap nonmembers first --- mypy/checkmember.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 4fd97d567237..9644edcfcd07 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -1345,8 +1345,6 @@ def analyze_enum_class_attribute_access( # Skip these since Enum will remove it if name in EXCLUDED_ENUM_ATTRIBUTES: return report_missing_attribute(mx.original_type, itype, name, mx) - if name not in itype.type.enum_members: - return None node = itype.type.get(name) if node and node.type: @@ -1359,6 +1357,9 @@ def analyze_enum_class_attribute_access( ): return proper.args[0] + if name not in itype.type.enum_members: + return None + enum_literal = LiteralType(name, fallback=itype) return itype.copy_modified(last_known_value=enum_literal)