Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always use .enum_members to find enum members #18675

Merged
merged 3 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand Down
75 changes: 49 additions & 26 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
73 changes: 70 additions & 3 deletions test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -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") \
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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):
Expand Down