Skip to content

Commit

Permalink
Fix enum attributes are not members (#17207)
Browse files Browse the repository at this point in the history
This adds on to the change in #17182
and fixes enum attributes being used as members.

Fixes: #16730

---------

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Co-authored-by: Ali Hamdan <ali.hamdan.dev@gmail.com>
Co-authored-by: hauntsaninja <hauntsaninja@gmail.com>
  • Loading branch information
4 people authored Oct 29, 2024
1 parent 654ae20 commit d81a9ef
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 34 deletions.
7 changes: 7 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
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
24 changes: 6 additions & 18 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
)
from mypy.state import state
from mypy.types import (
ENUM_REMOVED_PROPS,
AnyType,
CallableType,
ExtraAttrs,
Expand Down Expand Up @@ -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]:
Expand Down
9 changes: 8 additions & 1 deletion mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
]


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 @@ -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:
Expand Down
68 changes: 67 additions & 1 deletion test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]
Expand Down Expand Up @@ -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 "<typing special form>", 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]
54 changes: 54 additions & 0 deletions test-data/unit/check-python310.test
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
12 changes: 0 additions & 12 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<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: Name "Missing" is not defined

[case testTypeshedRecursiveTypesExample]
from typing import List, Union

Expand Down

0 comments on commit d81a9ef

Please sign in to comment.