bpo-44242: [Enum] remove missing bits test from Flag creation (GH-26586)

Move the check for missing named flags in flag aliases from Flag creation
to a new *verify* decorator.
This commit is contained in:
Ethan Furman 2021-06-09 09:03:55 -07:00 committed by GitHub
parent 6f84656dc1
commit eea8148b7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 309 additions and 30 deletions

View File

@ -28,8 +28,8 @@ An enumeration:
* is a set of symbolic names (members) bound to unique values * is a set of symbolic names (members) bound to unique values
* can be iterated over to return its members in definition order * can be iterated over to return its members in definition order
* uses :meth:`call` syntax to return members by value * uses *call* syntax to return members by value
* uses :meth:`index` syntax to return members by name * uses *index* syntax to return members by name
Enumerations are created either by using the :keyword:`class` syntax, or by Enumerations are created either by using the :keyword:`class` syntax, or by
using function-call syntax:: using function-call syntax::
@ -91,6 +91,12 @@ Module Contents
the bitwise operators without losing their :class:`IntFlag` membership. the bitwise operators without losing their :class:`IntFlag` membership.
:class:`IntFlag` members are also subclasses of :class:`int`. :class:`IntFlag` members are also subclasses of :class:`int`.
:class:`EnumCheck`
An enumeration with the values ``CONTINUOUS``, ``NAMED_FLAGS``, and
``UNIQUE``, for use with :func:`verify` to ensure various constraints
are met by a given enumeration.
:class:`FlagBoundary` :class:`FlagBoundary`
An enumeration with the values ``STRICT``, ``CONFORM``, ``EJECT``, and An enumeration with the values ``STRICT``, ``CONFORM``, ``EJECT``, and
@ -117,9 +123,14 @@ Module Contents
Enum class decorator that ensures only one name is bound to any one value. Enum class decorator that ensures only one name is bound to any one value.
:func:`verify`
Enum class decorator that checks user-selectable constraints on an
enumeration.
.. versionadded:: 3.6 ``Flag``, ``IntFlag``, ``auto`` .. versionadded:: 3.6 ``Flag``, ``IntFlag``, ``auto``
.. versionadded:: 3.10 ``StrEnum`` .. versionadded:: 3.10 ``StrEnum``, ``EnumCheck``, ``FlagBoundary``
Data Types Data Types
@ -514,6 +525,65 @@ Data Types
Using :class:`auto` with :class:`IntFlag` results in integers that are powers Using :class:`auto` with :class:`IntFlag` results in integers that are powers
of two, starting with ``1``. of two, starting with ``1``.
.. class:: EnumCheck
*EnumCheck* contains the options used by the :func:`verify` decorator to ensure
various constraints; failed constraints result in a :exc:`TypeError`.
.. attribute:: UNIQUE
Ensure that each value has only one name::
>>> from enum import Enum, verify, UNIQUE
>>> @verify(UNIQUE)
... class Color(Enum):
... RED = 1
... GREEN = 2
... BLUE = 3
... CRIMSON = 1
Traceback (most recent call last):
...
ValueError: aliases found in <enum 'Color'>: CRIMSON -> RED
.. attribute:: CONTINUOUS
Ensure that there are no missing values between the lowest-valued member
and the highest-valued member::
>>> from enum import Enum, verify, CONTINUOUS
>>> @verify(CONTINUOUS)
... class Color(Enum):
... RED = 1
... GREEN = 2
... BLUE = 5
Traceback (most recent call last):
...
ValueError: invalid enum 'Color': missing values 3, 4
.. attribute:: NAMED_FLAGS
Ensure that any flag groups/masks contain only named flags -- useful when
values are specified instead of being generated by :func:`auto`
>>> from enum import Flag, verify, NAMED_FLAGS
>>> @verify(NAMED_FLAGS)
... class Color(Flag):
... RED = 1
... GREEN = 2
... BLUE = 4
... WHITE = 15
... NEON = 31
Traceback (most recent call last):
...
ValueError: invalid Flag 'Color': 'WHITE' is missing a named flag for value 8; 'NEON' is missing named flags for values 8, 16
.. note::
CONTINUOUS and NAMED_FLAGS are designed to work with integer-valued members.
.. versionadded:: 3.10
.. class:: FlagBoundary .. class:: FlagBoundary
*FlagBoundary* controls how out-of-range values are handled in *Flag* and its *FlagBoundary* controls how out-of-range values are handled in *Flag* and its
@ -575,7 +645,7 @@ Data Types
>>> KeepFlag(2**2 + 2**4) >>> KeepFlag(2**2 + 2**4)
KeepFlag.BLUE|0x10 KeepFlag.BLUE|0x10
.. versionadded:: 3.10 ``FlagBoundary`` .. versionadded:: 3.10
Utilites and Decorators Utilites and Decorators
@ -632,3 +702,11 @@ Utilites and Decorators
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValueError: duplicate values found in <enum 'Mistake'>: FOUR -> THREE ValueError: duplicate values found in <enum 'Mistake'>: FOUR -> THREE
.. decorator:: verify
A :keyword:`class` decorator specifically for enumerations. Members from
:class:`EnumCheck` are used to specify which constraints should be checked
on the decorated enumeration.
.. versionadded:: 3.10

View File

@ -6,10 +6,10 @@ from builtins import property as _bltin_property, bin as _bltin_bin
__all__ = [ __all__ = [
'EnumType', 'EnumMeta', 'EnumType', 'EnumMeta',
'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag',
'auto', 'unique', 'auto', 'unique', 'property', 'verify',
'property',
'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP', 'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP',
'global_flag_repr', 'global_enum_repr', 'global_enum', 'global_flag_repr', 'global_enum_repr', 'global_enum',
'EnumCheck', 'CONTINUOUS', 'NAMED_FLAGS', 'UNIQUE',
] ]
@ -89,6 +89,9 @@ def _make_class_unpicklable(obj):
setattr(obj, '__module__', '<unknown>') setattr(obj, '__module__', '<unknown>')
def _iter_bits_lsb(num): def _iter_bits_lsb(num):
# num must be an integer
if isinstance(num, Enum):
num = num.value
while num: while num:
b = num & (~num + 1) b = num & (~num + 1)
yield b yield b
@ -538,13 +541,6 @@ class EnumType(type):
else: else:
# multi-bit flags are considered aliases # multi-bit flags are considered aliases
multi_bit_total |= flag_value multi_bit_total |= flag_value
if enum_class._boundary_ is not KEEP:
missed = list(_iter_bits_lsb(multi_bit_total & ~single_bit_total))
if missed:
raise TypeError(
'invalid Flag %r -- missing values: %s'
% (cls, ', '.join((str(i) for i in missed)))
)
enum_class._flag_mask_ = single_bit_total enum_class._flag_mask_ = single_bit_total
# #
# set correct __iter__ # set correct __iter__
@ -688,7 +684,10 @@ class EnumType(type):
return MappingProxyType(cls._member_map_) return MappingProxyType(cls._member_map_)
def __repr__(cls): def __repr__(cls):
return "<enum %r>" % cls.__name__ if Flag is not None and issubclass(cls, Flag):
return "<flag %r>" % cls.__name__
else:
return "<enum %r>" % cls.__name__
def __reversed__(cls): def __reversed__(cls):
""" """
@ -1303,7 +1302,8 @@ class Flag(Enum, boundary=STRICT):
else: else:
# calculate flags not in this member # calculate flags not in this member
self._inverted_ = self.__class__(self._flag_mask_ ^ self._value_) self._inverted_ = self.__class__(self._flag_mask_ ^ self._value_)
self._inverted_._inverted_ = self if isinstance(self._inverted_, self.__class__):
self._inverted_._inverted_ = self
return self._inverted_ return self._inverted_
@ -1561,6 +1561,91 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
return enum_class return enum_class
return convert_class return convert_class
@_simple_enum(StrEnum)
class EnumCheck:
"""
various conditions to check an enumeration for
"""
CONTINUOUS = "no skipped integer values"
NAMED_FLAGS = "multi-flag aliases may not contain unnamed flags"
UNIQUE = "one name per value"
CONTINUOUS, NAMED_FLAGS, UNIQUE = EnumCheck
class verify:
"""
Check an enumeration for various constraints. (see EnumCheck)
"""
def __init__(self, *checks):
self.checks = checks
def __call__(self, enumeration):
checks = self.checks
cls_name = enumeration.__name__
if Flag is not None and issubclass(enumeration, Flag):
enum_type = 'flag'
elif issubclass(enumeration, Enum):
enum_type = 'enum'
else:
raise TypeError("the 'verify' decorator only works with Enum and Flag")
for check in checks:
if check is UNIQUE:
# check for duplicate names
duplicates = []
for name, member in enumeration.__members__.items():
if name != member.name:
duplicates.append((name, member.name))
if duplicates:
alias_details = ', '.join(
["%s -> %s" % (alias, name) for (alias, name) in duplicates])
raise ValueError('aliases found in %r: %s' %
(enumeration, alias_details))
elif check is CONTINUOUS:
values = set(e.value for e in enumeration)
if len(values) < 2:
continue
low, high = min(values), max(values)
missing = []
if enum_type == 'flag':
# check for powers of two
for i in range(_high_bit(low)+1, _high_bit(high)):
if 2**i not in values:
missing.append(2**i)
elif enum_type == 'enum':
# check for powers of one
for i in range(low+1, high):
if i not in values:
missing.append(i)
else:
raise Exception('verify: unknown type %r' % enum_type)
if missing:
raise ValueError('invalid %s %r: missing values %s' % (
enum_type, cls_name, ', '.join((str(m) for m in missing)))
)
elif check is NAMED_FLAGS:
# examine each alias and check for unnamed flags
member_names = enumeration._member_names_
member_values = [m.value for m in enumeration]
missing = []
for name, alias in enumeration._member_map_.items():
if name in member_names:
# not an alias
continue
values = list(_iter_bits_lsb(alias.value))
missed = [v for v in values if v not in member_values]
if missed:
plural = ('', 's')[len(missed) > 1]
a = ('a ', '')[len(missed) > 1]
missing.append('%r is missing %snamed flag%s for value%s %s' % (
name, a, plural, plural,
', '.join(str(v) for v in missed)
))
if missing:
raise ValueError(
'invalid Flag %r: %s'
% (cls_name, '; '.join(missing))
)
return enumeration
def _test_simple_enum(checked_enum, simple_enum): def _test_simple_enum(checked_enum, simple_enum):
""" """
A function that can be used to test an enum created with :func:`_simple_enum` A function that can be used to test an enum created with :func:`_simple_enum`

View File

@ -9,6 +9,7 @@ import threading
from collections import OrderedDict from collections import OrderedDict
from enum import Enum, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto from enum import Enum, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto
from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum
from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS
from io import StringIO from io import StringIO
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
from test import support from test import support
@ -2774,13 +2775,6 @@ class TestFlag(unittest.TestCase):
third = auto() third = auto()
self.assertEqual([Dupes.first, Dupes.second, Dupes.third], list(Dupes)) self.assertEqual([Dupes.first, Dupes.second, Dupes.third], list(Dupes))
def test_bizarre(self):
with self.assertRaisesRegex(TypeError, "invalid Flag 'Bizarre' -- missing values: 1, 2"):
class Bizarre(Flag):
b = 3
c = 4
d = 6
def test_multiple_mixin(self): def test_multiple_mixin(self):
class AllMixin: class AllMixin:
@classproperty @classproperty
@ -3345,12 +3339,6 @@ class TestIntFlag(unittest.TestCase):
for f in Open: for f in Open:
self.assertEqual(bool(f.value), bool(f)) self.assertEqual(bool(f.value), bool(f))
def test_bizarre(self):
with self.assertRaisesRegex(TypeError, "invalid Flag 'Bizarre' -- missing values: 1, 2"):
class Bizarre(IntFlag):
b = 3
c = 4
d = 6
def test_multiple_mixin(self): def test_multiple_mixin(self):
class AllMixin: class AllMixin:
@ -3459,6 +3447,7 @@ class TestUnique(unittest.TestCase):
one = 1 one = 1
two = 'dos' two = 'dos'
tres = 4.0 tres = 4.0
#
@unique @unique
class Cleaner(IntEnum): class Cleaner(IntEnum):
single = 1 single = 1
@ -3484,12 +3473,137 @@ class TestUnique(unittest.TestCase):
turkey = 3 turkey = 3
def test_unique_with_name(self): def test_unique_with_name(self):
@unique @verify(UNIQUE)
class Silly(Enum): class Silly(Enum):
one = 1 one = 1
two = 'dos' two = 'dos'
name = 3 name = 3
@unique #
@verify(UNIQUE)
class Sillier(IntEnum):
single = 1
name = 2
triple = 3
value = 4
class TestVerify(unittest.TestCase):
def test_continuous(self):
@verify(CONTINUOUS)
class Auto(Enum):
FIRST = auto()
SECOND = auto()
THIRD = auto()
FORTH = auto()
#
@verify(CONTINUOUS)
class Manual(Enum):
FIRST = 3
SECOND = 4
THIRD = 5
FORTH = 6
#
with self.assertRaisesRegex(ValueError, 'invalid enum .Missing.: missing values 5, 6, 7, 8, 9, 10, 12'):
@verify(CONTINUOUS)
class Missing(Enum):
FIRST = 3
SECOND = 4
THIRD = 11
FORTH = 13
#
with self.assertRaisesRegex(ValueError, 'invalid flag .Incomplete.: missing values 32'):
@verify(CONTINUOUS)
class Incomplete(Flag):
FIRST = 4
SECOND = 8
THIRD = 16
FORTH = 64
#
with self.assertRaisesRegex(ValueError, 'invalid flag .StillIncomplete.: missing values 16'):
@verify(CONTINUOUS)
class StillIncomplete(Flag):
FIRST = 4
SECOND = 8
THIRD = 11
FORTH = 32
def test_composite(self):
class Bizarre(Flag):
b = 3
c = 4
d = 6
self.assertEqual(list(Bizarre), [Bizarre.c])
self.assertEqual(Bizarre.b.value, 3)
self.assertEqual(Bizarre.c.value, 4)
self.assertEqual(Bizarre.d.value, 6)
with self.assertRaisesRegex(
ValueError,
"invalid Flag 'Bizarre': 'b' is missing named flags for values 1, 2; 'd' is missing a named flag for value 2",
):
@verify(NAMED_FLAGS)
class Bizarre(Flag):
b = 3
c = 4
d = 6
#
class Bizarre(IntFlag):
b = 3
c = 4
d = 6
self.assertEqual(list(Bizarre), [Bizarre.c])
self.assertEqual(Bizarre.b.value, 3)
self.assertEqual(Bizarre.c.value, 4)
self.assertEqual(Bizarre.d.value, 6)
with self.assertRaisesRegex(
ValueError,
"invalid Flag 'Bizarre': 'b' is missing named flags for values 1, 2; 'd' is missing a named flag for value 2",
):
@verify(NAMED_FLAGS)
class Bizarre(IntFlag):
b = 3
c = 4
d = 6
def test_unique_clean(self):
@verify(UNIQUE)
class Clean(Enum):
one = 1
two = 'dos'
tres = 4.0
#
@verify(UNIQUE)
class Cleaner(IntEnum):
single = 1
double = 2
triple = 3
def test_unique_dirty(self):
with self.assertRaisesRegex(ValueError, 'tres.*one'):
@verify(UNIQUE)
class Dirty(Enum):
one = 1
two = 'dos'
tres = 1
with self.assertRaisesRegex(
ValueError,
'double.*single.*turkey.*triple',
):
@verify(UNIQUE)
class Dirtier(IntEnum):
single = 1
double = 1
triple = 3
turkey = 3
def test_unique_with_name(self):
@verify(UNIQUE)
class Silly(Enum):
one = 1
two = 'dos'
name = 3
#
@verify(UNIQUE)
class Sillier(IntEnum): class Sillier(IntEnum):
single = 1 single = 1
name = 2 name = 2

View File

@ -0,0 +1,2 @@
Remove missing flag check from Enum creation and move into a ``verify``
decorator.