Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Adds @enum.unique decorator support #11249

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
48 changes: 48 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1790,11 +1790,59 @@ def visit_class_def(self, defn: ClassDef) -> None:
sig, _ = self.expr_checker.check_call(dec, [temp],
[nodes.ARG_POS], defn,
callable_name=fullname)

if defn.info.is_enum and refers_to_fullname(decorator, 'enum.unique'):
self.check_enum_unique_decorator(defn, decorator)
# TODO: Apply the sig to the actual TypeInfo so we can handle decorators
# that completely swap out the type. (e.g. Callable[[Type[A]], Type[B]])
if typ.is_protocol and typ.defn.type_vars:
self.check_protocol_variance(defn)

def check_enum_unique_decorator(self, defn: ClassDef, decorator: Expression) -> None:
# TODO: this can also check known `Literal` types in the future.
known_values = []

# All `Enum`s are just a series of `a = 1`, `b = 2` assignments in their bodies.
# We need raw exressions in these assignments.
for node in defn.defs.body:
if not isinstance(node, AssignmentStmt):
continue

# Next, we need to be sure that all parts of `Enum` definition
# are just simple names and are present in `TypeInfo`.
for lvalue in node.lvalues:
if not isinstance(lvalue, NameExpr):
continue
field = defn.info.get(lvalue.name)
if field is None or not isinstance(field.node, Var):
continue

# Validation passed, continue.
is_known, value = mypy.checkexpr.try_getting_statically_known_value(
node.rvalue)
if not is_known:
# We continue, because value was not known.
# But, others possibly still can be reported.
continue
known_values.append(value)

# Since we might end up with unhashable objects, like `[1]` or `{}`,
# we cannot use `dict` or `Counter`.
# It should not hit us hard, because `Enum`s rarely have lots of values.
errors = []
seen = []
for value in known_values:
if value in seen:
errors.append(value)
if value not in seen:
seen.append(value)

# Now, report all errors:
for error in errors:
self.fail('Duplicate value "{}" in "{}" unique enum definition'.format(
error, defn.name,
), defn)

def check_final_deletable(self, typ: TypeInfo) -> None:
# These checks are only for mypyc. Only perform some checks that are easier
# to implement here than in mypyc.
Expand Down
77 changes: 76 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from contextlib import contextmanager
import itertools
from typing import (
Any, cast, Dict, Set, List, Tuple, Callable, Union, Optional, Sequence, Iterator
Any, cast, Dict, Set, List, Tuple, Callable, Union,
Optional, Sequence, Iterator, Iterable
)
from typing_extensions import ClassVar, Final, overload, TypeAlias as _TypeAlias

Expand Down Expand Up @@ -4578,3 +4579,77 @@ def get_partial_instance_type(t: Optional[Type]) -> Optional[PartialType]:
if t is None or not isinstance(t, PartialType) or t.type is None:
return None
return t


def try_getting_statically_known_value(node: Expression) -> Tuple[bool, Any]:
"""We try to get statically known expression's value.

Imagine, that you have:

class Some(enum.Enum):
one = 'one'
two = ('t', 'w', 'o')
three = None
four = method_call()

The first boolean represent whether given node was a literal value.
The second element is literal's value if any.

When trying to call ``try_getting_literal_expr`` on ``Some.one``
it will return ``True, 'one'`` tuple.
On ``Some.two`` it will return ``True, ('t', 'w', 'o')`` tuple.
On ``three`` it will return ``True, None``.
On ``four`` it will return ``False, None``.

We also recurse into nested nodes like ``TupleExpr``, ``ListExpr``, etc.
"""
if isinstance(node, (StrExpr, UnicodeExpr, IntExpr, FloatExpr, ComplexExpr)):
return True, node.value
if isinstance(node, BytesExpr):
# Since `bytes`'s value is store as `str` in `mypy`,
# we use this hack to tell them appart.
return True, f'b"{node.value}"'
if isinstance(node, NameExpr):
if node.name == 'None':
return True, None
if node.name in ('True', 'False'):
return True, node.name == 'True'
if isinstance(node, EllipsisExpr):
return True, ...

# Recursive types:
if isinstance(node, TupleExpr):
return _recursive_statically_known_value(node.items, tuple)
if isinstance(node, ListExpr):
return _recursive_statically_known_value(node.items, list)
if isinstance(node, SetExpr):
return _recursive_statically_known_value(node.items, set)
if isinstance(node, DictExpr):
keys = []
values = []
for key, value in node.items:
if key is None:
return False, None # We've met `**` unpacking

known_key, key_value = try_getting_statically_known_value(key)
if not known_key:
return False, None
known_value, value_value = try_getting_statically_known_value(value)
if not known_value:
return False, None
keys.append(key_value)
values.append(value_value)
return True, dict(zip(keys, values))
return False, None


def _recursive_statically_known_value(items: Iterable[Expression],
typ: type) -> Tuple[bool, Any]:
res: List[Any] = []
for item in items:
is_known, item_value = try_getting_statically_known_value(item)
if is_known:
res.append(item_value)
else:
return False, None
return True, typ(res)
219 changes: 218 additions & 1 deletion test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,11 @@ import enum
@enum.unique
class E(enum.Enum):
x = 1
y = 1 # NOTE: This duplicate value is not detected by mypy at the moment
y = 1
x = 1
x = E.x
[out]
main:3: error: Duplicate value "1" in "E" unique enum definition
main:7: error: Incompatible types in assignment (expression has type "E", variable has type "int")

[case testIntEnum_assignToIntVariable]
Expand Down Expand Up @@ -1644,3 +1645,219 @@ class A(Enum):
class Inner: pass
class B(A): pass # E: Cannot inherit from final class "A"
[builtins fixtures/bool.pyi]


[case testUniqueEnumCorrectStaticValues]
from enum import Enum, IntEnum, Flag, IntFlag, unique

@unique
class EmptyEnum(Enum):
pass
@unique
class EmptyIntEnum(IntEnum):
pass
@unique
class EmptyFlag(Flag):
pass
@unique
class EmptyIntFlag(IntFlag):
pass

@unique
class NonEmptyEnum(Enum):
x = [1, 'a', (1, [1, 2])]
y = 2
z = '1'
n = None
a = (1,)
b = [1]
c = {1: 1}
d = {1}
e = ...
f = False
g = True
h = 1.5
j = 1j
k = b'1'
@unique
class NonEmptyIntEnum(IntEnum):
x = 1
y = 2
@unique
class NonEmptyFlag(Flag):
x = 'x'
y = 'y'
@unique
class NonEmptyIntFlag(IntFlag):
x = 1
y = 2
[builtins fixtures/dict.pyi]

[case testNonEnumUnique]
from enum import unique
@unique
class A:
x = 1
y = 1
[builtins fixtures/bool.pyi]

[case testEnumNonUniqueDecorator]
from enum import Enum
def dec(t):
return t
@dec
class A(Enum):
x = 1
y = 1
[builtins fixtures/bool.pyi]

[case testEnumMetaUnique]
from enum import unique, EnumMeta
@unique
class A(EnumMeta):
x = 1
y = 1
[builtins fixtures/bool.pyi]

[case testEnumDynamicValues]
from typing import List
from enum import Enum, unique

# We cannot infer these yet (possibly - never):
def y() -> int:
return 1
z = [1]

@unique
class EmptyEnum(Enum):
a = z
b = z
c = y()
d = y()
e = z[0]
f = z[0]

@unique
class ComplexValuesEnum(Enum):
a = (z, z)
b = (z, z)
c = {y()}
d = {y()}
e = [z, y()]
f = [z, y()]
d1 = {z: 1}
d2 = {z: 1}
d3 = {1: y()}
d4 = {1: y()}
d5 = {z: y()}
d6 = {z: y()}

@unique
class EmptyIntEnum(Enum): # E: Duplicate value "0" in "EmptyIntEnum" unique enum definition
a = z[0]
# This still raises, we know `1` is duplicated:
b = 0
c = 0
# This won't:
d = int()
e = int()
[builtins fixtures/dict.pyi]

[case testEnumUniqueSeveralLValues]
from enum import Enum, unique
@unique
class IntFloatEnum(Enum): # E: Duplicate value "1" in "IntFloatEnum" unique enum definition
x = y = 1
[builtins fixtures/bool.pyi]

[case testEnumUniqueUnpacking]
from enum import Enum, unique
@unique
class UniqueEnum(Enum):
x, y = (1, 2)
# TODO: should raise, but does not
# TODO: this should be handled with `Literal` types
@unique
class NonUniqueEnum(Enum):
x, y = (1, 1)
[builtins fixtures/bool.pyi]

[case testEnumDuplocateStaticValues]
from enum import Enum, unique

@unique
class IntFloatEnum(Enum):
x = 1
y = 1.0
@unique
class IntEnum(Enum):
x = 1
y = 1
@unique
class FloatEnum(Enum):
x = 1.0
y = 1.0
@unique
class ComplexEnum(Enum):
x = 1j
y = 1j
@unique
class StrEnum(Enum):
a = 'a'
b = 'a'
@unique
class BytesEnum(Enum):
a = b'a'
b = b'a'
@unique
class BytesStrEnum(Enum):
a = 'a' # ok
b = b'a'
@unique
class BoolEnum(Enum):
a = True
b = True
@unique
class BoolIntEnum(Enum):
a = True
b = 1
@unique
class NoneEnum(Enum):
a = None
b = None
@unique
class ElipsisEnum(Enum):
a = ...
b = ...
@unique
class TupleEnum(Enum):
a = (True, 1, None)
b = (True, 1, None)
@unique
class ListEnum(Enum):
a = [1, (2, 3), [4, 5]]
b = [1, (2, 3), [4, 5]]
@unique
class SetEnum(Enum):
a = {1, True, 1.0}
b = {1}
@unique
class DictEnum(Enum):
a = {1: 'a'}
b = {1: 'a'}
[out]
main:4: error: Duplicate value "1.0" in "IntFloatEnum" unique enum definition
main:8: error: Duplicate value "1" in "IntEnum" unique enum definition
main:12: error: Duplicate value "1.0" in "FloatEnum" unique enum definition
main:16: error: Duplicate value "1j" in "ComplexEnum" unique enum definition
main:20: error: Duplicate value "a" in "StrEnum" unique enum definition
main:24: error: Duplicate value "b"a"" in "BytesEnum" unique enum definition
main:32: error: Duplicate value "True" in "BoolEnum" unique enum definition
main:36: error: Duplicate value "1" in "BoolIntEnum" unique enum definition
main:40: error: Duplicate value "None" in "NoneEnum" unique enum definition
main:44: error: Duplicate value "Ellipsis" in "ElipsisEnum" unique enum definition
main:48: error: Duplicate value "(True, 1, None)" in "TupleEnum" unique enum definition
main:52: error: Duplicate value "[1, (2, 3), [4, 5]]" in "ListEnum" unique enum definition
main:56: error: Duplicate value "{1}" in "SetEnum" unique enum definition
main:60: error: Duplicate value "{1: 'a'}" in "DictEnum" unique enum definition
[builtins fixtures/dict.pyi]
1 change: 1 addition & 0 deletions test-data/unit/fixtures/dict.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class list(Sequence[T]): # needed by some test cases
def append(self, item: T) -> None: pass

class tuple(Generic[T]): pass
class set(): pass
class function: pass
class float: pass
class complex: pass
Expand Down