Skip to content

Commit d81a9ef

Browse files
terencehonlesJelleZijlstrahamdanalhauntsaninja
authored
Fix enum attributes are not members (#17207)
This adds on to the change in #17182 and fixes enum attributes being used as members. Fixes: #16730 --------- Co-authored-by: Jelle Zijlstra <[email protected]> Co-authored-by: Ali Hamdan <[email protected]> Co-authored-by: hauntsaninja <[email protected]>
1 parent 654ae20 commit d81a9ef

File tree

8 files changed

+150
-34
lines changed

8 files changed

+150
-34
lines changed

mypy/semanal.py

+7
Original file line numberDiff line numberDiff line change
@@ -4311,6 +4311,13 @@ def analyze_name_lvalue(
43114311
lvalue,
43124312
)
43134313

4314+
if explicit_type and has_explicit_value:
4315+
self.fail("Enum members must be left unannotated", lvalue)
4316+
self.note(
4317+
"See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members",
4318+
lvalue,
4319+
)
4320+
43144321
if (not existing or isinstance(existing.node, PlaceholderNode)) and not outer:
43154322
# Define new variable.
43164323
var = self.make_name_lvalue_var(

mypy/semanal_enum.py

+6
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ def build_enum_call_typeinfo(
143143
var = Var(item)
144144
var.info = info
145145
var.is_property = True
146+
# When an enum is created by its functional form `Enum(name, values)`
147+
# - if it is a string it is first split by commas/whitespace
148+
# - if it is an iterable of single items each item is assigned a value starting at `start`
149+
# - if it is an iterable of (name, value) then the given values will be used
150+
# either way, each item should be treated as if it has an explicit value.
151+
var.has_explicit_value = True
146152
var._fullname = f"{info.fullname}.{item}"
147153
info.names[item] = SymbolTableNode(MDEF, var)
148154
return info

mypy/typeops.py

+6-18
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
)
3232
from mypy.state import state
3333
from mypy.types import (
34-
ENUM_REMOVED_PROPS,
3534
AnyType,
3635
CallableType,
3736
ExtraAttrs,
@@ -958,27 +957,16 @@ class Status(Enum):
958957
items = [
959958
try_expanding_sum_type_to_union(item, target_fullname) for item in typ.relevant_items()
960959
]
961-
return make_simplified_union(items, contract_literals=False)
962960
elif isinstance(typ, Instance) and typ.type.fullname == target_fullname:
963961
if typ.type.is_enum:
964-
new_items = []
965-
for name, symbol in typ.type.names.items():
966-
if not isinstance(symbol.node, Var):
967-
continue
968-
# Skip these since Enum will remove it
969-
if name in ENUM_REMOVED_PROPS:
970-
continue
971-
# Skip private attributes
972-
if name.startswith("__"):
973-
continue
974-
new_items.append(LiteralType(name, typ))
975-
return make_simplified_union(new_items, contract_literals=False)
962+
items = [LiteralType(name, typ) for name in typ.get_enum_values()]
976963
elif typ.type.fullname == "builtins.bool":
977-
return make_simplified_union(
978-
[LiteralType(True, typ), LiteralType(False, typ)], contract_literals=False
979-
)
964+
items = [LiteralType(True, typ), LiteralType(False, typ)]
965+
else:
966+
return typ
980967

981-
return typ
968+
# if the expanded union would be `Never` leave the type as is
969+
return typ if not items else make_simplified_union(items, contract_literals=False)
982970

983971

984972
def try_contracting_literals_in_union(types: Sequence[Type]) -> list[ProperType]:

mypy/types.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -1566,7 +1566,14 @@ def is_singleton_type(self) -> bool:
15661566
def get_enum_values(self) -> list[str]:
15671567
"""Return the list of values for an Enum."""
15681568
return [
1569-
name for name, sym in self.type.names.items() if isinstance(sym.node, mypy.nodes.Var)
1569+
name
1570+
for name, sym in self.type.names.items()
1571+
if (
1572+
isinstance(sym.node, mypy.nodes.Var)
1573+
and name not in ENUM_REMOVED_PROPS
1574+
and not name.startswith("__")
1575+
and sym.node.has_explicit_value
1576+
)
15701577
]
15711578

15721579

mypyc/test-data/run-classes.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,8 @@ from enum import Enum
270270

271271
class TestEnum(Enum):
272272
_order_ = "a b"
273-
a : int = 1
274-
b : int = 2
273+
a = 1
274+
b = 2
275275

276276
@classmethod
277277
def test(cls) -> int:

test-data/unit/check-enum.test

+67-1
Original file line numberDiff line numberDiff line change
@@ -1764,7 +1764,8 @@ class B(A):
17641764
x = 1 # E: Cannot override writable attribute "x" with a final one
17651765

17661766
class A1(Enum):
1767-
x: int = 1
1767+
x: int = 1 # E: Enum members must be left unannotated \
1768+
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members
17681769
class B1(A1): # E: Cannot extend enum with existing members: "A1"
17691770
pass
17701771

@@ -1779,6 +1780,7 @@ class A3(Enum):
17791780
x: Final[int] # type: ignore
17801781
class B3(A3):
17811782
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "A3")
1783+
17821784
[builtins fixtures/bool.pyi]
17831785

17841786
[case testEnumNotFinalWithMethodsAndUninitializedValuesStub]
@@ -2185,3 +2187,67 @@ reveal_type(A.y.value) # N: Revealed type is "Literal[2]?"
21852187
def some_a(a: A):
21862188
reveal_type(a.value) # N: Revealed type is "Union[Literal[1]?, Literal[2]?]"
21872189
[builtins fixtures/dict.pyi]
2190+
2191+
2192+
[case testErrorOnAnnotatedMember]
2193+
from enum import Enum
2194+
2195+
class Medal(Enum):
2196+
gold: int = 1 # E: Enum members must be left unannotated \
2197+
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members
2198+
silver: str = 2 # E: Enum members must be left unannotated \
2199+
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members \
2200+
# E: Incompatible types in assignment (expression has type "int", variable has type "str")
2201+
bronze = 3
2202+
2203+
[case testEnumMemberWithPlaceholder]
2204+
from enum import Enum
2205+
2206+
class Pet(Enum):
2207+
CAT = ...
2208+
DOG: str = ... # E: Enum members must be left unannotated \
2209+
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members \
2210+
# E: Incompatible types in assignment (expression has type "ellipsis", variable has type "str")
2211+
2212+
[case testEnumValueWithPlaceholderNodeType]
2213+
# https://github.com/python/mypy/issues/11971
2214+
from enum import Enum
2215+
from typing import Any, Callable, Dict
2216+
class Foo(Enum):
2217+
Bar: Foo = Callable[[str], None] # E: Enum members must be left unannotated \
2218+
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members \
2219+
# E: Incompatible types in assignment (expression has type "<typing special form>", variable has type "Foo")
2220+
Baz: Any = Callable[[Dict[str, "Missing"]], None] # E: Enum members must be left unannotated \
2221+
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members \
2222+
# E: Type application targets a non-generic function or class \
2223+
# E: Name "Missing" is not defined
2224+
2225+
reveal_type(Foo.Bar) # N: Revealed type is "Literal[__main__.Foo.Bar]?"
2226+
reveal_type(Foo.Bar.value) # N: Revealed type is "__main__.Foo"
2227+
reveal_type(Foo.Baz) # N: Revealed type is "Literal[__main__.Foo.Baz]?"
2228+
reveal_type(Foo.Baz.value) # N: Revealed type is "Any"
2229+
[builtins fixtures/tuple.pyi]
2230+
[typing fixtures/typing-full.pyi]
2231+
2232+
2233+
[case testEnumWithOnlyImplicitMembersUsingAnnotationOnly]
2234+
# flags: --warn-unreachable
2235+
import enum
2236+
2237+
2238+
class E(enum.IntEnum):
2239+
A: int
2240+
B: int
2241+
2242+
2243+
def do_check(value: E) -> None:
2244+
reveal_type(value) # N: Revealed type is "__main__.E"
2245+
# this is a nonmember check, not an emum member check, and it should not narrow the value
2246+
if value is E.A:
2247+
return
2248+
2249+
reveal_type(value) # N: Revealed type is "__main__.E"
2250+
"should be reachable"
2251+
2252+
[builtins fixtures/primitives.pyi]
2253+
[typing fixtures/typing-full.pyi]

test-data/unit/check-python310.test

+54
Original file line numberDiff line numberDiff line change
@@ -1525,6 +1525,60 @@ def g(m: Medal) -> int:
15251525
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.bronze]"
15261526
return 2
15271527

1528+
1529+
[case testMatchLiteralPatternEnumWithTypedAttribute]
1530+
from enum import Enum
1531+
from typing import NoReturn
1532+
def assert_never(x: NoReturn) -> None: ...
1533+
1534+
class int:
1535+
def __new__(cls, value: int): pass
1536+
1537+
class Medal(int, Enum):
1538+
prize: str
1539+
1540+
def __new__(cls, value: int, prize: str) -> Medal:
1541+
enum = int.__new__(cls, value)
1542+
enum._value_ = value
1543+
enum.prize = prize
1544+
return enum
1545+
1546+
gold = (1, 'cash prize')
1547+
silver = (2, 'sponsorship')
1548+
bronze = (3, 'nothing')
1549+
1550+
m: Medal
1551+
1552+
match m:
1553+
case Medal.gold:
1554+
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.gold]"
1555+
case Medal.silver:
1556+
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.silver]"
1557+
case Medal.bronze:
1558+
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.bronze]"
1559+
case _ as unreachable:
1560+
assert_never(unreachable)
1561+
1562+
[builtins fixtures/tuple.pyi]
1563+
1564+
[case testMatchLiteralPatternFunctionalEnum]
1565+
from enum import Enum
1566+
from typing import NoReturn
1567+
def assert_never(x: NoReturn) -> None: ...
1568+
1569+
Medal = Enum('Medal', 'gold silver bronze')
1570+
m: Medal
1571+
1572+
match m:
1573+
case Medal.gold:
1574+
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.gold]"
1575+
case Medal.silver:
1576+
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.silver]"
1577+
case Medal.bronze:
1578+
reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.bronze]"
1579+
case _ as unreachable:
1580+
assert_never(unreachable)
1581+
15281582
[case testMatchLiteralPatternEnumCustomEquals-skip]
15291583
from enum import Enum
15301584
class Medal(Enum):

test-data/unit/pythoneval.test

-12
Original file line numberDiff line numberDiff line change
@@ -1555,18 +1555,6 @@ if isinstance(obj, Awaitable):
15551555
_testSpecialTypingProtocols.py:6: note: Revealed type is "Tuple[builtins.int]"
15561556
_testSpecialTypingProtocols.py:8: error: Statement is unreachable
15571557

1558-
[case testEnumValueWithPlaceholderNodeType]
1559-
# https://github.com/python/mypy/issues/11971
1560-
from enum import Enum
1561-
from typing import Callable, Dict
1562-
class Foo(Enum):
1563-
Bar: Foo = Callable[[str], None]
1564-
Baz: Foo = Callable[[Dict[str, "Missing"]], None]
1565-
[out]
1566-
_testEnumValueWithPlaceholderNodeType.py:5: error: Incompatible types in assignment (expression has type "<typing special form>", variable has type "Foo")
1567-
_testEnumValueWithPlaceholderNodeType.py:6: error: Incompatible types in assignment (expression has type "<typing special form>", variable has type "Foo")
1568-
_testEnumValueWithPlaceholderNodeType.py:6: error: Name "Missing" is not defined
1569-
15701558
[case testTypeshedRecursiveTypesExample]
15711559
from typing import List, Union
15721560

0 commit comments

Comments
 (0)