diff --git a/mypy/checker.py b/mypy/checker.py index 04a286beef5e..62ae0c1bf83d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2719,10 +2719,8 @@ def check_enum(self, defn: ClassDef) -> None: self.check_enum_new(defn) def check_final_enum(self, defn: ClassDef, base: TypeInfo) -> None: - for sym in base.names.values(): - if self.is_final_enum_value(sym): - self.fail(f'Cannot extend enum with existing members: "{base.name}"', defn) - break + if base.enum_members: + self.fail(f'Cannot extend enum with existing members: "{base.name}"', defn) def is_final_enum_value(self, sym: SymbolTableNode) -> bool: if isinstance(sym.node, (FuncBase, Decorator)): diff --git a/mypy/nodes.py b/mypy/nodes.py index 6487ee4b745c..2d2dc4ac91c1 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -14,7 +14,7 @@ import mypy.strconv from mypy.options import Options -from mypy.util import is_typeshed_file, short_type +from mypy.util import is_sunder, is_typeshed_file, short_type from mypy.visitor import ExpressionVisitor, NodeVisitor, StatementVisitor if TYPE_CHECKING: @@ -3243,32 +3243,55 @@ def protocol_members(self) -> list[str]: @property def enum_members(self) -> list[str]: - return [ - name - for name, sym in self.names.items() - if ( - ( - isinstance(sym.node, Var) - and name not in EXCLUDED_ENUM_ATTRIBUTES - and not name.startswith("__") - and sym.node.has_explicit_value - and not ( - isinstance( - typ := mypy.types.get_proper_type(sym.node.type), mypy.types.Instance - ) + # TODO: cache the results? + members = [] + for name, sym in self.names.items(): + # Case 1: + # + # class MyEnum(Enum): + # @member + # def some(self): ... + if isinstance(sym.node, Decorator): + if any( + dec.fullname == "enum.member" + for dec in sym.node.decorators + if isinstance(dec, RefExpr) + ): + members.append(name) + continue + # Case 2: + # + # class MyEnum(Enum): + # x = 1 + # + # Case 3: + # + # class MyEnum(Enum): + # class Other: ... + elif isinstance(sym.node, (Var, TypeInfo)): + if ( + # TODO: properly support ignored names from `_ignore_` + name in EXCLUDED_ENUM_ATTRIBUTES + or is_sunder(name) + or name.startswith("__") # dunder and private + ): + continue # name is excluded + + if isinstance(sym.node, Var): + if not sym.node.has_explicit_value: + continue # unannotated value not a member + + typ = mypy.types.get_proper_type(sym.node.type) + if isinstance( + typ, mypy.types.FunctionLike + ) or ( # explicit `@member` is required + isinstance(typ, mypy.types.Instance) and typ.type.fullname == "enum.nonmember" - ) - ) - or ( - isinstance(sym.node, Decorator) - and any( - dec.fullname == "enum.member" - for dec in sym.node.decorators - if isinstance(dec, RefExpr) - ) - ) - ) - ] + ): + continue # name is not a member + + members.append(name) + return members def __getitem__(self, name: str) -> SymbolTableNode: n = self.get(name) diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index a3abf53e29ac..72e22f2fae94 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -1197,16 +1197,20 @@ def func(x: Union[int, None, Empty] = _empty) -> int: [builtins fixtures/primitives.pyi] [case testEnumReachabilityPEP484ExampleSingletonWithMethod] +# flags: --python-version 3.11 from typing import Final, Union -from enum import Enum +from enum import Enum, member class Empty(Enum): - token = lambda x: x + # note, that without `member` we cannot tell that `token` is a member: + token = member(lambda x: x) def f(self) -> int: return 1 _empty = Empty.token +reveal_type(_empty) # N: Revealed type is "__main__.Empty" +reveal_type(Empty.f) # N: Revealed type is "def (self: __main__.Empty) -> builtins.int" def func(x: Union[int, None, Empty] = _empty) -> int: boom = x + 42 # E: Unsupported left operand type for + ("None") \ @@ -1615,6 +1619,65 @@ class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot extend enum with e pass [builtins fixtures/bool.pyi] +[case testEnumImplicitlyFinalForSubclassingWithCallableMember] +# flags: --python-version 3.11 +from enum import Enum, IntEnum, Flag, IntFlag, member + +class NonEmptyEnum(Enum): + @member + def call(self) -> None: ... +class NonEmptyIntEnum(IntEnum): + @member + def call(self) -> None: ... +class NonEmptyFlag(Flag): + @member + def call(self) -> None: ... +class NonEmptyIntFlag(IntFlag): + @member + def call(self) -> None: ... + +class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot extend enum with existing members: "NonEmptyEnum" + pass +class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot extend enum with existing members: "NonEmptyIntEnum" + pass +class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot extend enum with existing members: "NonEmptyFlag" + pass +class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot extend enum with existing members: "NonEmptyIntFlag" + pass +[builtins fixtures/bool.pyi] + +[case testEnumCanExtendEnumsWithNonMembers] +# flags: --python-version 3.11 +from enum import Enum, IntEnum, Flag, IntFlag, nonmember + +class NonEmptyEnum(Enum): + x = nonmember(1) +class NonEmptyIntEnum(IntEnum): + x = nonmember(1) +class NonEmptyFlag(Flag): + x = nonmember(1) +class NonEmptyIntFlag(IntFlag): + x = nonmember(1) + +class ErrorEnumWithoutValue(NonEmptyEnum): + pass +class ErrorIntEnumWithoutValue(NonEmptyIntEnum): + pass +class ErrorFlagWithoutValue(NonEmptyFlag): + pass +class ErrorIntFlagWithoutValue(NonEmptyIntFlag): + pass +[builtins fixtures/bool.pyi] + +[case testLambdaIsNotEnumMember] +from enum import Enum + +class My(Enum): + x = lambda a: a + +class Other(My): ... +[builtins fixtures/bool.pyi] + [case testSubclassingNonFinalEnums] from enum import Enum, IntEnum, Flag, IntFlag, EnumMeta @@ -1839,6 +1902,10 @@ from enum import Enum class A(Enum): class Inner: pass class B(A): pass # E: Cannot extend enum with existing members: "A" + +class A1(Enum): + class __Inner: pass +class B1(A1): pass [builtins fixtures/bool.pyi] [case testEnumFinalSpecialProps] @@ -1922,7 +1989,7 @@ from enum import Enum class A(Enum): # E: Detected enum "lib.A" in a type stub with zero members. There is a chance this is due to a recent change in the semantics of enum membership. If so, use `member = value` to mark an enum member, instead of `member: type` \ # N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members x: int -class B(A): # E: Cannot extend enum with existing members: "A" +class B(A): x = 1 # E: Cannot override writable attribute "x" with a final one class C(Enum):