diff --git a/mypy/semanal.py b/mypy/semanal.py index 55eb9cbaf426..59e4594353f0 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -4311,6 +4311,13 @@ def analyze_name_lvalue( lvalue, ) + if explicit_type and has_explicit_value: + self.fail("Enum members must be left unannotated", lvalue) + self.note( + "See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members", + lvalue, + ) + if (not existing or isinstance(existing.node, PlaceholderNode)) and not outer: # Define new variable. var = self.make_name_lvalue_var( diff --git a/mypy/semanal_enum.py b/mypy/semanal_enum.py index 30e0bd56c312..0094b719bc96 100644 --- a/mypy/semanal_enum.py +++ b/mypy/semanal_enum.py @@ -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 diff --git a/mypy/typeops.py b/mypy/typeops.py index 0699cda53cfa..36e929284bf4 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -31,7 +31,6 @@ ) from mypy.state import state from mypy.types import ( - ENUM_REMOVED_PROPS, AnyType, CallableType, ExtraAttrs, @@ -958,27 +957,16 @@ class Status(Enum): items = [ try_expanding_sum_type_to_union(item, target_fullname) for item in typ.relevant_items() ] - 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) + items = [LiteralType(name, typ) for name in typ.get_enum_values()] elif typ.type.fullname == "builtins.bool": - return make_simplified_union( - [LiteralType(True, typ), LiteralType(False, typ)], contract_literals=False - ) + items = [LiteralType(True, typ), LiteralType(False, typ)] + else: + return typ - return typ + # if the expanded union would be `Never` leave the type as is + return typ if not items else make_simplified_union(items, contract_literals=False) def try_contracting_literals_in_union(types: Sequence[Type]) -> list[ProperType]: diff --git a/mypy/types.py b/mypy/types.py index b9bee535c05e..9632bd22adb6 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1566,7 +1566,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 + ) ] diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 40287ab963aa..90d0521ffc37 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -270,8 +270,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: diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index 78a114eda764..09e2abb30358 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -1764,7 +1764,8 @@ 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: Enum members must be left unannotated \ + # N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members class B1(A1): # E: Cannot extend enum with existing members: "A1" pass @@ -1779,6 +1780,7 @@ class A3(Enum): x: Final[int] # type: ignore class B3(A3): x = 1 # E: Cannot override final attribute "x" (previously declared in base class "A3") + [builtins fixtures/bool.pyi] [case testEnumNotFinalWithMethodsAndUninitializedValuesStub] @@ -2185,3 +2187,67 @@ 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: Enum members must be left unannotated \ + # N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members + silver: str = 2 # E: Enum members must be left unannotated \ + # N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-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: Enum members must be left unannotated \ + # N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members \ + # E: Incompatible types in assignment (expression has type "ellipsis", variable has type "str") + +[case testEnumValueWithPlaceholderNodeType] +# https://github.com/python/mypy/issues/11971 +from enum import Enum +from typing import Any, Callable, Dict +class Foo(Enum): + Bar: Foo = Callable[[str], None] # E: Enum members must be left unannotated \ + # N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members \ + # E: Incompatible types in assignment (expression has type "", variable has type "Foo") + Baz: Any = Callable[[Dict[str, "Missing"]], None] # E: Enum members must be left unannotated \ + # N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members \ + # E: Type application targets a non-generic function or class \ + # E: Name "Missing" is not defined + +reveal_type(Foo.Bar) # N: Revealed type is "Literal[__main__.Foo.Bar]?" +reveal_type(Foo.Bar.value) # N: Revealed type is "__main__.Foo" +reveal_type(Foo.Baz) # N: Revealed type is "Literal[__main__.Foo.Baz]?" +reveal_type(Foo.Baz.value) # N: Revealed type is "Any" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +[case testEnumWithOnlyImplicitMembersUsingAnnotationOnly] +# flags: --warn-unreachable +import enum + + +class E(enum.IntEnum): + A: int + B: int + + +def do_check(value: E) -> None: + reveal_type(value) # N: Revealed type is "__main__.E" + # this is a nonmember check, not an emum member check, and it should not narrow the value + if value is E.A: + return + + reveal_type(value) # N: Revealed type is "__main__.E" + "should be reachable" + +[builtins fixtures/primitives.pyi] +[typing fixtures/typing-full.pyi] diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index e7028a027e25..58b70d7b74d8 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1525,6 +1525,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 + + 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): diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 997cdc0d0ff0..4942a5fd5f2f 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1555,18 +1555,6 @@ if isinstance(obj, Awaitable): _testSpecialTypingProtocols.py:6: note: Revealed type is "Tuple[builtins.int]" _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 -class Foo(Enum): - Bar: Foo = Callable[[str], None] - Baz: Foo = Callable[[Dict[str, "Missing"]], None] -[out] -_testEnumValueWithPlaceholderNodeType.py:5: error: Incompatible types in assignment (expression has type "", variable has type "Foo") -_testEnumValueWithPlaceholderNodeType.py:6: error: Incompatible types in assignment (expression has type "", variable has type "Foo") -_testEnumValueWithPlaceholderNodeType.py:6: error: Name "Missing" is not defined - [case testTypeshedRecursiveTypesExample] from typing import List, Union