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

Fix enum attributes are not members #17207

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4220,6 +4220,9 @@ def analyze_name_lvalue(
lvalue,
)

if explicit_type and has_explicit_value:
self.fail("Type annotations are not allowed for enum members", lvalue)

if (not existing or isinstance(existing.node, PlaceholderNode)) and not outer:
# Define new variable.
var = self.make_name_lvalue_var(lvalue, kind, not explicit_type, has_explicit_value)
Expand Down
6 changes: 6 additions & 0 deletions mypy/semanal_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ def build_enum_call_typeinfo(
var = Var(item)
var.info = info
var.is_property = True
# When an enum is created by its functional form `Enum(name, values)`
# - if it is a string it is first split by commas/whitespace
# - if it is an iterable of single items each item is assigned a value starting at `start`
# - if it is an iterable of (name, value) then the given values will be used
# either way, each item should be treated as if it has an explicit value.
var.has_explicit_value = True
var._fullname = f"{info.fullname}.{item}"
info.names[item] = SymbolTableNode(MDEF, var)
return info
Expand Down
16 changes: 3 additions & 13 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
)
from mypy.state import state
from mypy.types import (
ENUM_REMOVED_PROPS,
AnyType,
CallableType,
ExtraAttrs,
Expand Down Expand Up @@ -901,18 +900,9 @@ class Status(Enum):
return make_simplified_union(items, contract_literals=False)
elif isinstance(typ, Instance) and typ.type.fullname == target_fullname:
if typ.type.is_enum:
new_items = []
for name, symbol in typ.type.names.items():
if not isinstance(symbol.node, Var):
continue
# 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)
return make_simplified_union(
[LiteralType(name, typ) for name in typ.get_enum_values()], contract_literals=False
)
elif typ.type.fullname == "builtins.bool":
return make_simplified_union(
[LiteralType(True, typ), LiteralType(False, typ)], contract_literals=False
Expand Down
9 changes: 8 additions & 1 deletion mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1550,7 +1550,14 @@ def is_singleton_type(self) -> bool:
def get_enum_values(self) -> list[str]:
"""Return the list of values for an Enum."""
return [
name for name, sym in self.type.names.items() if isinstance(sym.node, mypy.nodes.Var)
name
for name, sym in self.type.names.items()
if (
isinstance(sym.node, mypy.nodes.Var)
and name not in ENUM_REMOVED_PROPS
and not name.startswith("__")
and sym.node.has_explicit_value
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This last condition changes the behavior of mypy which currently considers unassigned but annotated attributes as members of the enum.
I would say the new behavior is better but it must be noted in the PR title or description that this is a breaking change that must be considered on its own.
Looking at the mypy primer output, it looks like members of enums constructed using the enum call syntax do not have the has_explicit_value set which breaks type narrowing. I suggest changing the variable here to also set this flag perhaps with a comment explaining the reason -- something like "E = Enum("E", "A") is equivalent to class E(Enum): A = auto()"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for noticing that, I've not really used them in that form, but hopefully the comment is not too wordy. I only glanced at the code that you pointed to, but I was cross referencing https://github.com/python/cpython/blob/7528b84e947f727734bd802356e380673553387d/Lib/enum.py#L828-L839 and how much of this is implemented by mypy? 🤔

)
]


Expand Down
4 changes: 2 additions & 2 deletions mypyc/test-data/run-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ from enum import Enum

class TestEnum(Enum):
_order_ = "a b"
a : int = 1
b : int = 2
a = 1
b = 2

@classmethod
def test(cls) -> int:
Expand Down
23 changes: 22 additions & 1 deletion test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -1764,7 +1764,7 @@ class B(A):
x = 1 # E: Cannot override writable attribute "x" with a final one

class A1(Enum):
x: int = 1
x: int = 1 # E: Type annotations are not allowed for enum members
class B1(A1): # E: Cannot extend enum with existing members: "A1"
pass

Expand Down Expand Up @@ -2185,3 +2185,24 @@ reveal_type(A.y.value) # N: Revealed type is "Literal[2]?"
def some_a(a: A):
reveal_type(a.value) # N: Revealed type is "Union[Literal[1]?, Literal[2]?]"
[builtins fixtures/dict.pyi]


[case testErrorOnAnnotatedMember]
from enum import Enum

class Medal(Enum):
gold: int = 1 # E: Type annotations are not allowed for enum members
silver: str = 2 # E: Type annotations are not allowed for enum members \
# E: Incompatible types in assignment (expression has type "int", variable has type "str")
bronze = 3


[case testEnumMemberWithPlaceholder]
from enum import Enum

class Pet(Enum):
CAT = ...
DOG: str = ... # E: Type annotations are not allowed for enum members \
# E: Incompatible types in assignment (expression has type "ellipsis", variable has type "str")

[file test.pyi]
54 changes: 54 additions & 0 deletions test-data/unit/check-python310.test
Original file line number Diff line number Diff line change
Expand Up @@ -1499,6 +1499,60 @@ def g(m: Medal) -> int:
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.bronze]"
return 2


[case testMatchLiteralPatternEnumWithTypedAttribute]
from enum import Enum
from typing import NoReturn
def assert_never(x: NoReturn) -> None: ...

class int:
def __new__(cls, value: int): pass

class Medal(int, Enum):
prize: str
terencehonles marked this conversation as resolved.
Show resolved Hide resolved

def __new__(cls, value: int, prize: str) -> Medal:
enum = int.__new__(cls, value)
enum._value_ = value
enum.prize = prize
return enum

gold = (1, 'cash prize')
silver = (2, 'sponsorship')
bronze = (3, 'nothing')

m: Medal

match m:
case Medal.gold:
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.gold]"
case Medal.silver:
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.silver]"
case Medal.bronze:
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.bronze]"
case _ as unreachable:
assert_never(unreachable)

[builtins fixtures/tuple.pyi]

[case testMatchLiteralPatternFunctionalEnum]
from enum import Enum
from typing import NoReturn
def assert_never(x: NoReturn) -> None: ...

Medal = Enum('Medal', 'gold silver bronze')
m: Medal

match m:
case Medal.gold:
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.gold]"
case Medal.silver:
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.silver]"
case Medal.bronze:
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.bronze]"
case _ as unreachable:
assert_never(unreachable)

[case testMatchLiteralPatternEnumCustomEquals-skip]
from enum import Enum
class Medal(Enum):
Expand Down
16 changes: 13 additions & 3 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -1599,14 +1599,24 @@ _testSpecialTypingProtocols.py:8: error: Statement is unreachable
[case testEnumValueWithPlaceholderNodeType]
# https://github.com/python/mypy/issues/11971
from enum import Enum
from typing import Callable, Dict
from typing import Any, Callable, Dict
class Foo(Enum):
Bar: Foo = Callable[[str], None]
Baz: Foo = Callable[[Dict[str, "Missing"]], None]
Baz: Any = Callable[[Dict[str, "Missing"]], None]

reveal_type(Foo.Bar)
reveal_type(Foo.Bar.value) # this should probably not be "Foo" https://typing.readthedocs.io/en/latest/spec/enums.html#member-values
reveal_type(Foo.Baz)
reveal_type(Foo.Baz.value)
[out]
_testEnumValueWithPlaceholderNodeType.py:5: error: Type annotations are not allowed for enum members
_testEnumValueWithPlaceholderNodeType.py:5: error: Incompatible types in assignment (expression has type "<typing special form>", variable has type "Foo")
_testEnumValueWithPlaceholderNodeType.py:6: error: Incompatible types in assignment (expression has type "<typing special form>", variable has type "Foo")
_testEnumValueWithPlaceholderNodeType.py:6: error: Type annotations are not allowed for enum members
_testEnumValueWithPlaceholderNodeType.py:6: error: Name "Missing" is not defined
_testEnumValueWithPlaceholderNodeType.py:8: note: Revealed type is "Literal[_testEnumValueWithPlaceholderNodeType.Foo.Bar]?"
_testEnumValueWithPlaceholderNodeType.py:9: note: Revealed type is "_testEnumValueWithPlaceholderNodeType.Foo"
_testEnumValueWithPlaceholderNodeType.py:10: note: Revealed type is "Literal[_testEnumValueWithPlaceholderNodeType.Foo.Baz]?"
_testEnumValueWithPlaceholderNodeType.py:11: note: Revealed type is "Any"

[case testTypeshedRecursiveTypesExample]
from typing import List, Union
Expand Down
Loading