From 5f522d38139dc6d9bd7781500f811016b683b66f Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Tue, 12 Jan 2021 18:29:54 -0800 Subject: [PATCH 01/30] [Enum] fix Flag iteration, repr(), and str() Flag members are now divided by one-bit verses multi-bit, with multi-bit being treated as aliases. Iterating over a flag only returns the contained single-bit flags. repr() and str() now only show the Flags, not extra integer values; any extra integer values are either discarded (CONFORM), turned into ``int``s (EJECT) or treated as errors (STRICT). Flag classes can specify which of those three behaviors is desired: >>> class Test(Flag, boundary=CONFORM): ... ONE = 1 ... TWO = 2 ... >>> Test(5) --- Doc/library/enum.rst | 46 ++++- Lib/enum.py | 393 +++++++++++++++++++++++++++--------------- Lib/test/test_enum.py | 184 ++++++++++++++------ 3 files changed, 428 insertions(+), 195 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index c532e2caec466c..57e8721db57e7e 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -638,12 +638,22 @@ IntFlag The next variation of :class:`Enum` provided, :class:`IntFlag`, is also based on :class:`int`. The difference being :class:`IntFlag` members can be combined using the bitwise operators (&, \|, ^, ~) and the result is still an -:class:`IntFlag` member. However, as the name implies, :class:`IntFlag` +:class:`IntFlag` member, if possible. However, as the name implies, :class:`IntFlag` members also subclass :class:`int` and can be used wherever an :class:`int` is -used. Any operation on an :class:`IntFlag` member besides the bit-wise -operations will lose the :class:`IntFlag` membership. +used. + +.. note:: + + Any operation on an :class:`IntFlag` member besides the bit-wise operations will + lose the :class:`IntFlag` membership. + +.. note:: + + Bit-wise operations that result in invalid :class:`IntFlag` values will lose the + :class:`IntFlag` membership. .. versionadded:: 3.6 +.. versionchanged:: 3.10 Sample :class:`IntFlag` class:: @@ -671,21 +681,41 @@ It is also possible to name the combinations:: >>> Perm.RWX >>> ~Perm.RWX - + + >>> Perm(7) + + +.. note:: + + Named combinations are considered aliases. Aliases do not show up during + iteration, but can be returned from by-value lookups. + +.. versionchanged:: 3.10 Another important difference between :class:`IntFlag` and :class:`Enum` is that if no flags are set (the value is 0), its boolean evaluation is :data:`False`:: >>> Perm.R & Perm.X - + >>> bool(Perm.R & Perm.X) False Because :class:`IntFlag` members are also subclasses of :class:`int` they can -be combined with them:: +be combined with them (but may lose :class:`IntFlag` membership:: + + >>> Perm.X | 4 + >>> Perm.X | 8 - + 9 + +.. note:: + + The negation operator, ``~``, always returns an :class:`IntFlag` member with a + positive number:: + + >>> ~Perm.X + :class:`IntFlag` members can also be iterated over:: @@ -717,7 +747,7 @@ flags being set, the boolean evaluation is :data:`False`:: ... GREEN = auto() ... >>> Color.RED & Color.GREEN - + >>> bool(Color.RED & Color.GREEN) False diff --git a/Lib/enum.py b/Lib/enum.py index 8ca385420da029..ac907023535715 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1,5 +1,7 @@ import sys from types import MappingProxyType, DynamicClassAttribute +from operator import or_ as _or_, and_ as _and_, xor as _xor_, inv as _inv_ +from functools import reduce from builtins import property as _bltin_property @@ -8,9 +10,15 @@ 'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'auto', 'unique', 'property', + 'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', ] +# Dummy value for Enum and Flag as there are explicit checks for them +# before they have been created. +# This is also why there are checks in EnumMeta like `if Enum is not None` +Enum = Flag = EJECT = None + def _is_descriptor(obj): """ Returns True if obj is a descriptor, False otherwise. @@ -56,6 +64,75 @@ def _is_private(cls_name, name): else: return False +def _bits(num): + # return str(num) if num<=1 else bin(num>>1) + str(num&1) + if num in (0, 1): + return str(num) + negative = False + if num < 0: + negative = True + num = ~num + result = _bits(num>>1) + str(num&1) + if negative: + result = '1' + ''.join(['10'[d=='1'] for d in result]) + return result + + +def _bit_count(num): + """ + return number of set bits + + Counting bits set, Brian Kernighan's way* + + unsigned int v; // count the number of bits set in v + unsigned int c; // c accumulates the total bits set in v + for (c = 0; v; c++) + { v &= v - 1; } //clear the least significant bit set + + This method goes through as many iterations as there are set bits. So if we + have a 32-bit word with only the high bit set, then it will only go once + through the loop. + + * The C Programming Language 2nd Ed., Kernighan & Ritchie, 1988. + + This works because each subtraction "borrows" from the lowest 1-bit. For example: + + loop pass 1 loop pass 2 + ----------- ----------- + 101000 100000 + - 1 - 1 + = 100111 = 011111 + & 101000 & 100000 + = 100000 = 0 + + It is an excellent technique for Python, since the size of the integer need not + be determined beforehand. + + (from https://wiki.python.org/moin/BitManipulation) + """ + count = 0 + while(num): + num &= num - 1 + count += 1 + return(count) + +def _bit_len(num): + """ + return number of bits required to represent num + """ + length = 0 + while num: + length += 1 + num >>= 1 + return length + +def _is_single_bit(num): + """ + True if only one bit set in num (should be an int) + """ + num &= num - 1 + return num == 0 + def _make_class_unpicklable(obj): """ Make the given obj un-picklable. @@ -160,8 +237,14 @@ def __set_name__(self, enum_class, member_name): enum_member = canonical_member break else: - # no other instances found, record this member in _member_names_ - enum_class._member_names_.append(member_name) + # this could still be an alias if the value is multi-bit and the class + # is a flag class + if Flag is None or not issubclass(enum_class, Flag): + # no other instances found, record this member in _member_names_ + enum_class._member_names_.append(member_name) + elif Flag is not None and issubclass(enum_class, Flag) and _is_single_bit(value): + # no other instances found, record this member in _member_names_ + enum_class._member_names_.append(member_name) # get redirect in place before adding to _member_map_ # but check for other instances in parent classes first need_override = False @@ -193,7 +276,7 @@ def __set_name__(self, enum_class, member_name): # This may fail if value is not hashable. We can't add the value # to the map, and by-value lookups for this value will be # linear. - enum_class._value2member_map_[value] = enum_member + enum_class._value2member_map_.setdefault(value, enum_member) except TypeError: pass @@ -287,11 +370,6 @@ def update(self, members, **more_members): self[name] = value -# Dummy value for Enum as EnumMeta explicitly checks for it, but of course -# until EnumMeta finishes running the first time the Enum class doesn't exist. -# This is also why there are checks in EnumMeta like `if Enum is not None` -Enum = None - class EnumMeta(type): """ Metaclass for Enum @@ -311,7 +389,7 @@ def __prepare__(metacls, cls, bases, **kwds): ) return enum_dict - def __new__(metacls, cls, bases, classdict, **kwds): + def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): # an Enum class is final once enumeration items have been defined; it # cannot be mixed with other types (int, float, etc.) if it has an # inherited __new__ unless a new __new__ is defined (or the resulting @@ -346,15 +424,25 @@ def __new__(metacls, cls, bases, classdict, **kwds): classdict['_use_args_'] = use_args # # convert future enum members into temporary _proto_members + # and record integer values in case this will be a Flag + flag_mask = 0 for name in member_names: - classdict[name] = _proto_member(classdict[name]) + value = classdict[name] + if isinstance(value, int): + flag_mask |= value + classdict[name] = _proto_member(value) # - # house keeping structures + # house-keeping structures classdict['_member_names_'] = [] classdict['_member_map_'] = {} classdict['_value2member_map_'] = {} classdict['_member_type_'] = member_type # + # Flag structures (will be removed if final class is not a Flag + classdict['_boundary_'] = boundary or getattr(first_enum, '_boundary_', None) + classdict['_flag_mask_'] = flag_mask + classdict['_all_bits_'] = 2 ** ((flag_mask).bit_length()) - 1 + # # If a custom type is mixed into the Enum, and it does not know how # to pickle itself, pickle.dumps will succeed but pickle.loads will # fail. Rather than have the error show up later and possibly far @@ -414,6 +502,32 @@ def __new__(metacls, cls, bases, classdict, **kwds): if _order_ != enum_class._member_names_: raise TypeError('member order does not match _order_') # + # remove Flag structures if final class is not a Flag + if Flag is None or not issubclass(enum_class, Flag): + delattr(enum_class, '_boundary_') + delattr(enum_class, '_flag_mask_') + delattr(enum_class, '_all_bits_') + elif Flag is not None and issubclass(enum_class, Flag): + # ensure _all_bits_ is correct and there are no missing flags + single_bit_total = 0 + multi_bit_total = 0 + for flag in enum_class._member_map_.values(): + if _is_single_bit(flag._value_): + single_bit_total |= flag._value_ + else: + # multi-bit flags are considered aliases + multi_bit_total |= flag._value_ + missed = [] + all_bit_total = 0 + for i in range(_bit_len(max(single_bit_total, multi_bit_total))): + i = 2**i + all_bit_total |= i + if i & multi_bit_total and not i & single_bit_total: + missed.append(i) + 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 + # return enum_class def __bool__(self): @@ -761,6 +875,11 @@ def __new__(cls, value): result = None if isinstance(result, cls): return result + elif ( + Flag is not None and issubclass(cls, Flag) + and cls._boundary_ is EJECT and isinstance(result, int) + ): + return result else: ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__)) if result is None and exc is None: @@ -770,7 +889,8 @@ def __new__(cls, value): 'error in %s._missing_: returned %r instead of None or a valid member' % (cls.__name__, result) ) - exc.__context__ = ve_exc + if not isinstance(exc, ValueError): + exc.__context__ = ve_exc raise exc def _generate_next_value_(name, start, count, last_values): @@ -900,7 +1020,20 @@ def _generate_next_value_(name, start, count, last_values): def _reduce_ex_by_name(self, proto): return self.name -class Flag(Enum): +class FlagBoundary(StrEnum): + """ + control how out of range values are handled + "strict" -> error is raised [default for Flag] + "conform" -> extra bits are discarded + "eject" -> lose flag status [default for IntFlag] + """ + STRICT = auto() + CONFORM = auto() + EJECT = auto() +STRICT, CONFORM, EJECT = FlagBoundary + + +class Flag(Enum, boundary=STRICT): """ Support for flags """ @@ -926,35 +1059,59 @@ def _generate_next_value_(name, start, count, last_values): @classmethod def _missing_(cls, value): - """ - Returns member (possibly creating it) if one can be found for value. - """ - original_value = value - if value < 0: - value = ~value - possible_member = cls._create_pseudo_member_(value) - if original_value < 0: - possible_member = ~possible_member - return possible_member - - @classmethod - def _create_pseudo_member_(cls, value): """ Create a composite member iff value contains only members. """ - pseudo_member = cls._value2member_map_.get(value, None) - if pseudo_member is None: - # verify all bits are accounted for - _, extra_flags = _decompose(cls, value) - if extra_flags: - raise ValueError("%r is not a valid %s" % (value, cls.__qualname__)) + if not isinstance(value, int): + raise ValueError("%r is not a valid %s" % (value, cls.__qualname__)) + # check boundaries + # - value must be in range (e.g. -16 <-> +15, i.e. ~15 <-> 15) + # - value must not include any skipped flags (e.g. if bit 2 is not defined, then 0d10 is invalid) + neg_value = None + if ( + not ~cls._all_bits_ <= value <= cls._all_bits_ + or value & (cls._all_bits_ ^ cls._flag_mask_) + ): + if cls._boundary_ is STRICT: + invalid_as_bits = _bits(value) + length = max(len(invalid_as_bits), _bit_len(cls._flag_mask_)) + valid_as_bits = ('0' * length + _bits(cls._flag_mask_))[-length:] + invalid_as_bits = ('01'[value<0] * length + invalid_as_bits)[-length:] + raise ValueError( + "%s: invalid value: %r\n given %s\n allowed %s" % ( + cls.__name__, + value, + invalid_as_bits, + valid_as_bits, + )) + elif cls._boundary_ is CONFORM: + value = value & cls._flag_mask_ + elif cls._boundary_ is EJECT: + return value + else: + raise ValueError('unknown flag boundary: %r' % (cls._boundary_, )) + elif value < 0: + neg_value = value + value = cls._all_bits_ + 1 + value + # get members + members, _ = _decompose(cls, value) + if _: + raise ValueError('%s: _decompose(%r) --> %r, %r' % (cls.__name__, value, members, _)) + # normal Flag? + __new__ = getattr(cls, '__new_member__', None) + if cls._member_type_ is object and not __new__: # construct a singleton enum pseudo-member pseudo_member = object.__new__(cls) - pseudo_member._name_ = None + else: + pseudo_member = (__new__ or cls._member_type_.__new__)(cls, value) + if not hasattr(pseudo_member, 'value'): pseudo_member._value_ = value - # use setdefault in case another thread already created a composite - # with this value - pseudo_member = cls._value2member_map_.setdefault(value, pseudo_member) + pseudo_member._name_ = '|'.join([m._name_ for m in members]) or None + # use setdefault in case another thread already created a composite + # with this value + pseudo_member = cls._value2member_map_.setdefault(value, pseudo_member) + if neg_value is not None: + cls._value2member_map_[neg_value] = pseudo_member return pseudo_member def __contains__(self, other): @@ -971,32 +1128,25 @@ def __iter__(self): """ Returns flags in decreasing value order. """ - members, extra_flags = _decompose(self.__class__, self.value) - return (m for m in members if m._value_ != 0) + return (m for m in reversed(self.__class__) if m._value_ & self._value_) + + def __len__(self): + return _bit_count(self._value_) def __repr__(self): cls = self.__class__ if self._name_ is not None: return '<%s.%s: %r>' % (cls.__name__, self._name_, self._value_) - members, uncovered = _decompose(cls, self._value_) - return '<%s.%s: %r>' % ( - cls.__name__, - '|'.join([str(m._name_ or m._value_) for m in members]), - self._value_, - ) + else: + # only zero is unnamed by default + return '<%s: %r>' % (cls.__name__, self._value_) def __str__(self): cls = self.__class__ if self._name_ is not None: return '%s.%s' % (cls.__name__, self._name_) - members, uncovered = _decompose(cls, self._value_) - if len(members) == 1 and members[0]._name_ is None: - return '%s.%r' % (cls.__name__, members[0]._value_) else: - return '%s.%s' % ( - cls.__name__, - '|'.join([str(m._name_ or m._value_) for m in members]), - ) + return '%s.%s' % (cls.__name__, self._value_) def __bool__(self): return bool(self._value_) @@ -1017,86 +1167,53 @@ def __xor__(self, other): return self.__class__(self._value_ ^ other._value_) def __invert__(self): - members, uncovered = _decompose(self.__class__, self._value_) - inverted = self.__class__(0) - for m in self.__class__: - if m not in members and not (m._value_ & self._value_): - inverted = inverted | m - return self.__class__(inverted) + current = set(list(self)) + return self.__class__(reduce( + _or_, + [m._value_ for m in self.__class__ if m not in current], + 0, + )) -class IntFlag(int, Flag): +class IntFlag(int, Flag, boundary=EJECT): """ Support for integer-based Flags """ - @classmethod - def _missing_(cls, value): - """ - Returns member (possibly creating it) if one can be found for value. - """ - if not isinstance(value, int): - raise ValueError("%r is not a valid %s" % (value, cls.__qualname__)) - new_member = cls._create_pseudo_member_(value) - return new_member - - @classmethod - def _create_pseudo_member_(cls, value): - """ - Create a composite member iff value contains only members. - """ - pseudo_member = cls._value2member_map_.get(value, None) - if pseudo_member is None: - need_to_create = [value] - # get unaccounted for bits - _, extra_flags = _decompose(cls, value) - # timer = 10 - while extra_flags: - # timer -= 1 - bit = _high_bit(extra_flags) - flag_value = 2 ** bit - if (flag_value not in cls._value2member_map_ and - flag_value not in need_to_create - ): - need_to_create.append(flag_value) - if extra_flags == -flag_value: - extra_flags = 0 - else: - extra_flags ^= flag_value - for value in reversed(need_to_create): - # construct singleton pseudo-members - pseudo_member = int.__new__(cls, value) - pseudo_member._name_ = None - pseudo_member._value_ = value - # use setdefault in case another thread already created a composite - # with this value - pseudo_member = cls._value2member_map_.setdefault(value, pseudo_member) - return pseudo_member - def __or__(self, other): - if not isinstance(other, (self.__class__, int)): + if isinstance(other, self.__class__): + other = other._value_ + elif isinstance(other, int): + other = other + else: return NotImplemented - result = self.__class__(self._value_ | self.__class__(other)._value_) - return result + value = self._value_ + return self.__class__(value | other) def __and__(self, other): - if not isinstance(other, (self.__class__, int)): + if isinstance(other, self.__class__): + other = other._value_ + elif isinstance(other, int): + other = other + else: return NotImplemented - return self.__class__(self._value_ & self.__class__(other)._value_) + value = self._value_ + return self.__class__(value & other) def __xor__(self, other): - if not isinstance(other, (self.__class__, int)): + if isinstance(other, self.__class__): + other = other._value_ + elif isinstance(other, int): + other = other + else: return NotImplemented - return self.__class__(self._value_ ^ self.__class__(other)._value_) + value = self._value_ + return self.__class__(value ^ other) __ror__ = __or__ __rand__ = __and__ __rxor__ = __xor__ - - def __invert__(self): - result = self.__class__(~self._value_) - return result - + __invert__ = Flag.__invert__ def _high_bit(value): """ @@ -1120,30 +1237,36 @@ def unique(enumeration): return enumeration def _decompose(flag, value): - """ - Extract all members from the value. - """ - # _decompose is only called if the value is not named - not_covered = value + """Extract all members from the value.""" negative = value < 0 + if negative: + value = ~value members = [] for member in flag: - member_value = member.value - if member_value and member_value & value == member_value: + if value & member._value_: members.append(member) - not_covered &= ~member_value - if not negative: - tmp = not_covered - while tmp: - flag_value = 2 ** _high_bit(tmp) - if flag_value in flag._value2member_map_: - members.append(flag._value2member_map_[flag_value]) - not_covered &= ~flag_value - tmp &= ~flag_value - if not members and value in flag._value2member_map_: - members.append(flag._value2member_map_[value]) + value ^= member._value_ + if value: + # check value2member_map + possibles = [ + m + for v, m in list(flag._value2member_map_.items()) + if m not in flag + ] + possibles.sort(key=lambda m: m._value_, reverse=True) + for multi in possibles: + if multi._value_ & value == multi._value_: + members.append(multi) + value ^= multi._value_ + if negative: + members = [m for m in flag if m not in members] + if value: + value = ~value members.sort(key=lambda m: m._value_, reverse=True) - if len(members) > 1 and members[0].value == value: - # we have the breakdown, don't need the value member itself - members.pop(0) - return members, not_covered + return members, value + +def _power_of_two(value): + if value < 1: + return False + return value == 2 ** _high_bit(value) + diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 3ea623e9c883c0..0aa7068678c45c 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -6,6 +6,7 @@ import threading from collections import OrderedDict from enum import Enum, IntEnum, StrEnum, EnumMeta, Flag, IntFlag, unique, auto +from enum import STRICT, CONFORM, EJECT from io import StringIO from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL from test import support @@ -2264,9 +2265,12 @@ class Open(Flag): class Color(Flag): BLACK = 0 RED = 1 + ROJO = 1 GREEN = 2 BLUE = 4 PURPLE = RED|BLUE + WHITE = RED|GREEN|BLUE + BLANCO = RED|GREEN|BLUE def test_str(self): Perm = self.Perm @@ -2289,9 +2293,10 @@ def test_str(self): self.assertEqual(str(Open.AC), 'Open.AC') self.assertEqual(str(Open.RO | Open.CE), 'Open.CE') self.assertEqual(str(Open.WO | Open.CE), 'Open.CE|WO') - self.assertEqual(str(~Open.RO), 'Open.CE|AC|RW|WO') + self.assertEqual(str(~Open.RO), 'Open.CE|RW|WO') self.assertEqual(str(~Open.WO), 'Open.CE|RW') self.assertEqual(str(~Open.AC), 'Open.CE') + self.assertEqual(str(~Open.CE), 'Open.AC') self.assertEqual(str(~(Open.RO | Open.CE)), 'Open.AC') self.assertEqual(str(~(Open.WO | Open.CE)), 'Open.RW') @@ -2302,12 +2307,12 @@ def test_repr(self): self.assertEqual(repr(Perm.X), '') self.assertEqual(repr(Perm.R | Perm.W), '') self.assertEqual(repr(Perm.R | Perm.W | Perm.X), '') - self.assertEqual(repr(Perm(0)), '') + self.assertEqual(repr(Perm(0)), '') self.assertEqual(repr(~Perm.R), '') self.assertEqual(repr(~Perm.W), '') self.assertEqual(repr(~Perm.X), '') self.assertEqual(repr(~(Perm.R | Perm.W)), '') - self.assertEqual(repr(~(Perm.R | Perm.W | Perm.X)), '') + self.assertEqual(repr(~(Perm.R | Perm.W | Perm.X)), '') self.assertEqual(repr(Perm(~0)), '') Open = self.Open @@ -2316,9 +2321,10 @@ def test_repr(self): self.assertEqual(repr(Open.AC), '') self.assertEqual(repr(Open.RO | Open.CE), '') self.assertEqual(repr(Open.WO | Open.CE), '') - self.assertEqual(repr(~Open.RO), '') + self.assertEqual(repr(~Open.RO), '') self.assertEqual(repr(~Open.WO), '') self.assertEqual(repr(~Open.AC), '') + self.assertEqual(repr(~Open.CE), '') self.assertEqual(repr(~(Open.RO | Open.CE)), '') self.assertEqual(repr(~(Open.WO | Open.CE)), '') @@ -2394,6 +2400,28 @@ def test_bool(self): for f in Open: self.assertEqual(bool(f.value), bool(f)) + def test_boundary(self): + class Iron(Flag, boundary=STRICT): + ONE = 1 + TWO = 2 + FOUR = 4 + # + class Water(Flag, boundary=CONFORM): + ONE = 1 + TWO = 2 + FOUR = 4 + # + class Space(Flag, boundary=EJECT): + ONE = 1 + TWO = 2 + FOUR = 4 + # + self.assertRaisesRegex(ValueError, 'invalid value: 11', Iron, 11) + self.assertIs(Water(11), Water.ONE|Water.TWO) + self.assertEqual(Space(11), 11) + self.assertTrue(type(Space(11)) is int) + + def test_programatic_function_string(self): Perm = Flag('Perm', 'R W X') lst = list(Perm) @@ -2514,6 +2542,22 @@ def test_member_iter(self): self.assertEqual(list(Color.PURPLE), [Color.BLUE, Color.RED]) self.assertEqual(list(Color.BLUE), [Color.BLUE]) self.assertEqual(list(Color.GREEN), [Color.GREEN]) + self.assertEqual(list(Color.WHITE), [Color.BLUE, Color.GREEN, Color.RED]) + self.assertEqual(list(Color.WHITE), [Color.BLUE, Color.GREEN, Color.RED]) + + def test_member_length(self): + self.assertEqual(self.Color.__len__(self.Color.BLACK), 0) + self.assertEqual(self.Color.__len__(self.Color.GREEN), 1) + self.assertEqual(self.Color.__len__(self.Color.PURPLE), 2) + self.assertEqual(self.Color.__len__(self.Color.BLANCO), 3) + + def test_aliases(self): + Color = self.Color + self.assertEqual(Color(1).name, 'RED') + self.assertEqual(Color['ROJO'].name, 'RED') + self.assertEqual(Color(7).name, 'WHITE') + self.assertEqual(Color['BLANCO'].name, 'WHITE') + self.assertIs(Color.BLANCO, Color.WHITE) def test_auto_number(self): class Color(Flag): @@ -2532,20 +2576,6 @@ class Color(Flag): red = 'not an int' blue = auto() - def test_cascading_failure(self): - class Bizarre(Flag): - c = 3 - d = 4 - f = 6 - # Bizarre.c | Bizarre.d - name = "TestFlag.test_cascading_failure..Bizarre" - self.assertRaisesRegex(ValueError, "5 is not a valid " + name, Bizarre, 5) - self.assertRaisesRegex(ValueError, "5 is not a valid " + name, Bizarre, 5) - self.assertRaisesRegex(ValueError, "2 is not a valid " + name, Bizarre, 2) - self.assertRaisesRegex(ValueError, "2 is not a valid " + name, Bizarre, 2) - self.assertRaisesRegex(ValueError, "1 is not a valid " + name, Bizarre, 1) - self.assertRaisesRegex(ValueError, "1 is not a valid " + name, Bizarre, 1) - def test_duplicate_auto(self): class Dupes(Enum): first = primero = auto() @@ -2554,11 +2584,11 @@ class Dupes(Enum): self.assertEqual([Dupes.first, Dupes.second, Dupes.third], list(Dupes)) def test_bizarre(self): - class Bizarre(Flag): - b = 3 - c = 4 - d = 6 - self.assertEqual(repr(Bizarre(7)), '') + 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): class AllMixin: @@ -2696,9 +2726,17 @@ class Open(IntFlag): class Color(IntFlag): BLACK = 0 RED = 1 + ROJO = 1 GREEN = 2 BLUE = 4 PURPLE = RED|BLUE + WHITE = RED|GREEN|BLUE + BLANCO = RED|GREEN|BLUE + + class Skip(IntFlag): + FIRST = 1 + SECOND = 2 + EIGHTH = 8 def test_type(self): Perm = self.Perm @@ -2723,17 +2761,17 @@ def test_str(self): self.assertEqual(str(Perm.X), 'Perm.X') self.assertEqual(str(Perm.R | Perm.W), 'Perm.R|W') self.assertEqual(str(Perm.R | Perm.W | Perm.X), 'Perm.R|W|X') - self.assertEqual(str(Perm.R | 8), 'Perm.8|R') + self.assertEqual(str(Perm.R | 8), '12') self.assertEqual(str(Perm(0)), 'Perm.0') - self.assertEqual(str(Perm(8)), 'Perm.8') + self.assertEqual(str(Perm(8)), '8') self.assertEqual(str(~Perm.R), 'Perm.W|X') self.assertEqual(str(~Perm.W), 'Perm.R|X') self.assertEqual(str(~Perm.X), 'Perm.R|W') self.assertEqual(str(~(Perm.R | Perm.W)), 'Perm.X') - self.assertEqual(str(~(Perm.R | Perm.W | Perm.X)), 'Perm.-8') - self.assertEqual(str(~(Perm.R | 8)), 'Perm.W|X') + self.assertEqual(str(~(Perm.R | Perm.W | Perm.X)), 'Perm.0') + self.assertEqual(str(~(Perm.R | 8)), '-13') self.assertEqual(str(Perm(~0)), 'Perm.R|W|X') - self.assertEqual(str(Perm(~8)), 'Perm.R|W|X') + self.assertEqual(str(Perm(~8)), '-9') Open = self.Open self.assertEqual(str(Open.RO), 'Open.RO') @@ -2741,13 +2779,17 @@ def test_str(self): self.assertEqual(str(Open.AC), 'Open.AC') self.assertEqual(str(Open.RO | Open.CE), 'Open.CE') self.assertEqual(str(Open.WO | Open.CE), 'Open.CE|WO') - self.assertEqual(str(Open(4)), 'Open.4') - self.assertEqual(str(~Open.RO), 'Open.CE|AC|RW|WO') + self.assertEqual(str(Open(4)), '4') + self.assertEqual(str(~Open.RO), 'Open.CE|RW|WO') self.assertEqual(str(~Open.WO), 'Open.CE|RW') self.assertEqual(str(~Open.AC), 'Open.CE') - self.assertEqual(str(~(Open.RO | Open.CE)), 'Open.AC|RW|WO') + self.assertEqual(str(~Open.CE), 'Open.AC') + self.assertEqual(str(~(Open.RO | Open.CE)), 'Open.AC') self.assertEqual(str(~(Open.WO | Open.CE)), 'Open.RW') - self.assertEqual(str(Open(~4)), 'Open.CE|AC|RW|WO') + self.assertEqual(str(Open(~4)), '-5') + + Skip = self.Skip + self.assertEqual(str(Skip(~4)), 'Skip.EIGHTH|SECOND|FIRST') def test_repr(self): Perm = self.Perm @@ -2756,17 +2798,17 @@ def test_repr(self): self.assertEqual(repr(Perm.X), '') self.assertEqual(repr(Perm.R | Perm.W), '') self.assertEqual(repr(Perm.R | Perm.W | Perm.X), '') - self.assertEqual(repr(Perm.R | 8), '') - self.assertEqual(repr(Perm(0)), '') - self.assertEqual(repr(Perm(8)), '') - self.assertEqual(repr(~Perm.R), '') - self.assertEqual(repr(~Perm.W), '') - self.assertEqual(repr(~Perm.X), '') - self.assertEqual(repr(~(Perm.R | Perm.W)), '') - self.assertEqual(repr(~(Perm.R | Perm.W | Perm.X)), '') - self.assertEqual(repr(~(Perm.R | 8)), '') - self.assertEqual(repr(Perm(~0)), '') - self.assertEqual(repr(Perm(~8)), '') + self.assertEqual(repr(Perm.R | 8), '12') + self.assertEqual(repr(Perm(0)), '') + self.assertEqual(repr(Perm(8)), '8') + self.assertEqual(repr(~Perm.R), '') + self.assertEqual(repr(~Perm.W), '') + self.assertEqual(repr(~Perm.X), '') + self.assertEqual(repr(~(Perm.R | Perm.W)), '') + self.assertEqual(repr(~(Perm.R | Perm.W | Perm.X)), '') + self.assertEqual(repr(~(Perm.R | 8)), '-13') + self.assertEqual(repr(Perm(~0)), '') + self.assertEqual(repr(Perm(~8)), '-9') Open = self.Open self.assertEqual(repr(Open.RO), '') @@ -2774,13 +2816,16 @@ def test_repr(self): self.assertEqual(repr(Open.AC), '') self.assertEqual(repr(Open.RO | Open.CE), '') self.assertEqual(repr(Open.WO | Open.CE), '') - self.assertEqual(repr(Open(4)), '') - self.assertEqual(repr(~Open.RO), '') - self.assertEqual(repr(~Open.WO), '') - self.assertEqual(repr(~Open.AC), '') - self.assertEqual(repr(~(Open.RO | Open.CE)), '') - self.assertEqual(repr(~(Open.WO | Open.CE)), '') - self.assertEqual(repr(Open(~4)), '') + self.assertEqual(repr(Open(4)), '4') + self.assertEqual(repr(~Open.RO), '') + self.assertEqual(repr(~Open.WO), '') + self.assertEqual(repr(~Open.AC), '') + self.assertEqual(repr(~(Open.RO | Open.CE)), '') + self.assertEqual(repr(~(Open.WO | Open.CE)), '') + self.assertEqual(repr(Open(~4)), '-5') + + Skip = self.Skip + self.assertEqual(repr(Skip(~4)), '') def test_format(self): Perm = self.Perm @@ -2863,8 +2908,7 @@ def test_invert(self): RWX = Perm.R | Perm.W | Perm.X values = list(Perm) + [RW, RX, WX, RWX, Perm(0)] for i in values: - self.assertEqual(~i, ~i.value) - self.assertEqual((~i).value, ~i.value) + self.assertEqual(~i, (~i).value) self.assertIs(type(~i), Perm) self.assertEqual(~~i, i) for i in Perm: @@ -2873,6 +2917,27 @@ def test_invert(self): self.assertIs(Open.WO & ~Open.WO, Open.RO) self.assertIs((Open.WO|Open.CE) & ~Open.WO, Open.CE) + def test_boundary(self): + class Iron(Flag, boundary=STRICT): + ONE = 1 + TWO = 2 + FOUR = 4 + # + class Water(Flag, boundary=CONFORM): + ONE = 1 + TWO = 2 + FOUR = 4 + # + class Space(Flag, boundary=EJECT): + ONE = 1 + TWO = 2 + FOUR = 4 + # + self.assertRaisesRegex(ValueError, 'invalid value: 11', Iron, 11) + self.assertIs(Water(11), Water.ONE|Water.TWO) + self.assertEqual(Space(11), 11) + self.assertTrue(type(Space(11)) is int) + def test_programatic_function_string(self): Perm = IntFlag('Perm', 'R W X') lst = list(Perm) @@ -3017,6 +3082,21 @@ def test_member_iter(self): self.assertEqual(list(Color.PURPLE), [Color.BLUE, Color.RED]) self.assertEqual(list(Color.BLUE), [Color.BLUE]) self.assertEqual(list(Color.GREEN), [Color.GREEN]) + self.assertEqual(list(Color.WHITE), [Color.BLUE, Color.GREEN, Color.RED]) + + def test_member_length(self): + self.assertEqual(self.Color.__len__(self.Color.BLACK), 0) + self.assertEqual(self.Color.__len__(self.Color.GREEN), 1) + self.assertEqual(self.Color.__len__(self.Color.PURPLE), 2) + self.assertEqual(self.Color.__len__(self.Color.BLANCO), 3) + + def test_aliases(self): + Color = self.Color + self.assertEqual(Color(1).name, 'RED') + self.assertEqual(Color['ROJO'].name, 'RED') + self.assertEqual(Color(7).name, 'WHITE') + self.assertEqual(Color['BLANCO'].name, 'WHITE') + self.assertIs(Color.BLANCO, Color.WHITE) def test_bool(self): Perm = self.Perm From f7f9e728e123dcaecccda0a96d4cdff6cce5bd01 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 13 Jan 2021 17:04:03 -0800 Subject: [PATCH 02/30] add boundary KEEP for Flags (default for _convert_) some flag sets, such as ``ssl.Options`` are incomplete/inconsistent; using KEEP allows those flags to exist, and have useful repr()s, etc. also, add ``_inverted_`` attribute to Flag members to significantly speed up that operation. --- Lib/enum.py | 107 ++++++++++++++++++++++++++++++------------ Lib/test/test_enum.py | 21 +++++++-- 2 files changed, 93 insertions(+), 35 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index ac907023535715..34ab988c4c8e24 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -10,7 +10,7 @@ 'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'auto', 'unique', 'property', - 'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', + 'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP', ] @@ -65,19 +65,22 @@ def _is_private(cls_name, name): return False def _bits(num): - # return str(num) if num<=1 else bin(num>>1) + str(num&1) - if num in (0, 1): - return str(num) + if num == 0: + return '0b0' negative = False if num < 0: negative = True num = ~num - result = _bits(num>>1) + str(num&1) + digits = [] + while num: + digits.insert(0, num&1) + num >>= 1 if negative: - result = '1' + ''.join(['10'[d=='1'] for d in result]) + result = '0b1' + (''.join(['10'[d] for d in digits]).lstrip('0')) + else: + result = '0b0' + ''.join(str(d) for d in digits) return result - def _bit_count(num): """ return number of set bits @@ -130,6 +133,8 @@ def _is_single_bit(num): """ True if only one bit set in num (should be an int) """ + if num == 0: + return False num &= num - 1 return num == 0 @@ -442,6 +447,7 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): classdict['_boundary_'] = boundary or getattr(first_enum, '_boundary_', None) classdict['_flag_mask_'] = flag_mask classdict['_all_bits_'] = 2 ** ((flag_mask).bit_length()) - 1 + classdict['_inverted_'] = None # # If a custom type is mixed into the Enum, and it does not know how # to pickle itself, pickle.dumps will succeed but pickle.loads will @@ -507,6 +513,7 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): delattr(enum_class, '_boundary_') delattr(enum_class, '_flag_mask_') delattr(enum_class, '_all_bits_') + delattr(enum_class, '_inverted_') elif Flag is not None and issubclass(enum_class, Flag): # ensure _all_bits_ is correct and there are no missing flags single_bit_total = 0 @@ -524,7 +531,7 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): all_bit_total |= i if i & multi_bit_total and not i & single_bit_total: missed.append(i) - if missed: + if missed and enum_class._boundary_ is not KEEP: raise TypeError('invalid Flag %r -- missing values: %s' % (cls, ', '.join((str(i) for i in missed)))) enum_class._flag_mask_ = single_bit_total # @@ -536,7 +543,12 @@ def __bool__(self): """ return True - def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1): + def __call__( + cls, value, names=None, + *, + module=None, qualname=None, type=None, + start=1, boundary=None, + ): """ Either returns an existing member, or creates a new enum class. @@ -571,6 +583,7 @@ def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, s qualname=qualname, type=type, start=start, + boundary=boundary, ) def __contains__(cls, member): @@ -653,7 +666,12 @@ def __setattr__(cls, name, value): raise AttributeError('Cannot reassign members.') super().__setattr__(name, value) - def _create_(cls, class_name, names, *, module=None, qualname=None, type=None, start=1): + def _create_( + cls, class_name, names, + *, + module=None, qualname=None, type=None, + start=1, boundary=None, + ): """ Convenience method to create a new Enum class. @@ -703,9 +721,12 @@ def _create_(cls, class_name, names, *, module=None, qualname=None, type=None, s if qualname is not None: classdict['__qualname__'] = qualname - return metacls.__new__(metacls, class_name, bases, classdict) + return metacls.__new__( + metacls, class_name, bases, classdict, + boundary=boundary, + ) - def _convert_(cls, name, module, filter, source=None): + def _convert_(cls, name, module, filter, source=None, boundary=None): """ Create a new Enum subclass that replaces a collection of global constants """ @@ -732,7 +753,7 @@ def _convert_(cls, name, module, filter, source=None): except TypeError: # unless some values aren't comparable, in which case sort by name members.sort(key=lambda t: t[0]) - cls = cls(name, members, module=module) + cls = cls(name, members, module=module, boundary=boundary or KEEP) cls.__reduce_ex__ = _reduce_ex_by_name module_globals.update(cls.__members__) module_globals[name] = cls @@ -847,6 +868,7 @@ class Enum(metaclass=EnumMeta): Derive from this class to define new enumerations. """ + def __new__(cls, value): # all enum instances are actually created during class construction # without calling this method; this method is called by the metaclass' @@ -1026,11 +1048,13 @@ class FlagBoundary(StrEnum): "strict" -> error is raised [default for Flag] "conform" -> extra bits are discarded "eject" -> lose flag status [default for IntFlag] + "keep" -> keep flag status and all bits """ STRICT = auto() CONFORM = auto() EJECT = auto() -STRICT, CONFORM, EJECT = FlagBoundary + KEEP = auto() +STRICT, CONFORM, EJECT, KEEP = FlagBoundary class Flag(Enum, boundary=STRICT): @@ -1088,15 +1112,21 @@ def _missing_(cls, value): value = value & cls._flag_mask_ elif cls._boundary_ is EJECT: return value + elif cls._boundary_ is KEEP: + if value < 0: + value = max(cls._all_bits_+1, 2**(value.bit_length())) + value else: raise ValueError('unknown flag boundary: %r' % (cls._boundary_, )) - elif value < 0: + if value < 0: neg_value = value value = cls._all_bits_ + 1 + value # get members - members, _ = _decompose(cls, value) - if _: - raise ValueError('%s: _decompose(%r) --> %r, %r' % (cls.__name__, value, members, _)) + members, unknown = _decompose(cls, value) + if unknown and cls._boundary_ is not KEEP: + raise ValueError( + '%s: _decompose(%r) --> %r, %r' + % (cls.__name__, value, members, unknown) + ) # normal Flag? __new__ = getattr(cls, '__new_member__', None) if cls._member_type_ is object and not __new__: @@ -1106,12 +1136,19 @@ def _missing_(cls, value): pseudo_member = (__new__ or cls._member_type_.__new__)(cls, value) if not hasattr(pseudo_member, 'value'): pseudo_member._value_ = value - pseudo_member._name_ = '|'.join([m._name_ for m in members]) or None + if members: + pseudo_member._name_ = '|'.join([m._name_ for m in members]) + if unknown: + pseudo_member._name_ += '|0x%x' % unknown + else: + pseudo_member._name_ = None # use setdefault in case another thread already created a composite - # with this value - pseudo_member = cls._value2member_map_.setdefault(value, pseudo_member) - if neg_value is not None: - cls._value2member_map_[neg_value] = pseudo_member + # with this value, but only if all members are known + # note: zero is a special case -- add it + if not unknown: + pseudo_member = cls._value2member_map_.setdefault(value, pseudo_member) + if neg_value is not None: + cls._value2member_map_[neg_value] = pseudo_member return pseudo_member def __contains__(self, other): @@ -1146,7 +1183,7 @@ def __str__(self): if self._name_ is not None: return '%s.%s' % (cls.__name__, self._name_) else: - return '%s.%s' % (cls.__name__, self._value_) + return '%s(%s)' % (cls.__name__, self._value_) def __bool__(self): return bool(self._value_) @@ -1167,12 +1204,19 @@ def __xor__(self, other): return self.__class__(self._value_ ^ other._value_) def __invert__(self): - current = set(list(self)) - return self.__class__(reduce( - _or_, - [m._value_ for m in self.__class__ if m not in current], - 0, - )) + if self._inverted_ is None: + if self._boundary_ is KEEP: + # use all bits + self._inverted_ = self.__class__(~self._value_) + else: + # get flags not in this member + self._inverted_ = self.__class__(reduce( + _or_, + [m._value_ for m in self.__class__ if m not in self], + 0 + )) + self._inverted_._inverted_ = self + return self._inverted_ class IntFlag(int, Flag, boundary=EJECT): @@ -1255,6 +1299,9 @@ def _decompose(flag, value): ] possibles.sort(key=lambda m: m._value_, reverse=True) for multi in possibles: + if multi._value_ == 0: + # do not add the zero flag + continue if multi._value_ & value == multi._value_: members.append(multi) value ^= multi._value_ diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 0aa7068678c45c..964fca6eac135a 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -2279,12 +2279,12 @@ def test_str(self): self.assertEqual(str(Perm.X), 'Perm.X') self.assertEqual(str(Perm.R | Perm.W), 'Perm.R|W') self.assertEqual(str(Perm.R | Perm.W | Perm.X), 'Perm.R|W|X') - self.assertEqual(str(Perm(0)), 'Perm.0') + self.assertEqual(str(Perm(0)), 'Perm(0)') self.assertEqual(str(~Perm.R), 'Perm.W|X') self.assertEqual(str(~Perm.W), 'Perm.R|X') self.assertEqual(str(~Perm.X), 'Perm.R|W') self.assertEqual(str(~(Perm.R | Perm.W)), 'Perm.X') - self.assertEqual(str(~(Perm.R | Perm.W | Perm.X)), 'Perm.0') + self.assertEqual(str(~(Perm.R | Perm.W | Perm.X)), 'Perm(0)') self.assertEqual(str(Perm(~0)), 'Perm.R|W|X') Open = self.Open @@ -2421,6 +2421,11 @@ class Space(Flag, boundary=EJECT): self.assertEqual(Space(11), 11) self.assertTrue(type(Space(11)) is int) + def test_iter(self): + Color = self.Color + Open = self.Open + self.assertEqual(list(Color), [Color.RED, Color.GREEN, Color.BLUE]) + self.assertEqual(list(Open), [Open.WO, Open.RW, Open.CE]) def test_programatic_function_string(self): Perm = Flag('Perm', 'R W X') @@ -2762,13 +2767,13 @@ def test_str(self): self.assertEqual(str(Perm.R | Perm.W), 'Perm.R|W') self.assertEqual(str(Perm.R | Perm.W | Perm.X), 'Perm.R|W|X') self.assertEqual(str(Perm.R | 8), '12') - self.assertEqual(str(Perm(0)), 'Perm.0') + self.assertEqual(str(Perm(0)), 'Perm(0)') self.assertEqual(str(Perm(8)), '8') self.assertEqual(str(~Perm.R), 'Perm.W|X') self.assertEqual(str(~Perm.W), 'Perm.R|X') self.assertEqual(str(~Perm.X), 'Perm.R|W') self.assertEqual(str(~(Perm.R | Perm.W)), 'Perm.X') - self.assertEqual(str(~(Perm.R | Perm.W | Perm.X)), 'Perm.0') + self.assertEqual(str(~(Perm.R | Perm.W | Perm.X)), 'Perm(0)') self.assertEqual(str(~(Perm.R | 8)), '-13') self.assertEqual(str(Perm(~0)), 'Perm.R|W|X') self.assertEqual(str(Perm(~8)), '-9') @@ -2938,6 +2943,12 @@ class Space(Flag, boundary=EJECT): self.assertEqual(Space(11), 11) self.assertTrue(type(Space(11)) is int) + def test_iter(self): + Color = self.Color + Open = self.Open + self.assertEqual(list(Color), [Color.RED, Color.GREEN, Color.BLUE]) + self.assertEqual(list(Open), [Open.WO, Open.RW, Open.CE]) + def test_programatic_function_string(self): Perm = IntFlag('Perm', 'R W X') lst = list(Perm) @@ -3256,7 +3267,7 @@ class Sillier(IntEnum): Help on class Color in module %s: class Color(enum.Enum) - | Color(value, names=None, *, module=None, qualname=None, type=None, start=1) + | Color(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None) |\x20\x20 | An enumeration. |\x20\x20 From d289ba3e0551ae7a7c8cdbec0e445764812a03e3 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 13 Jan 2021 17:08:59 -0800 Subject: [PATCH 03/30] update re.RegexFlag for new Flag implementation repr() has been modified to support as closely as possible its previous output; the big difference is that inverted flags cannot be output as before because the inversion operation now always returns the comparable positive result; i.e. re.A|re.I|re.M|re.S is ~(re.L|re.U|re.S|re.T|re.DEBUG) in both of the above terms, the ``value`` is 282. re's tests have been updated to reflect the modifications to repr(). --- Lib/re.py | 31 +++++++++++-------------------- Lib/test/test_re.py | 12 +++++++----- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/Lib/re.py b/Lib/re.py index bfb7b1ccd93466..a39ff047c26b22 100644 --- a/Lib/re.py +++ b/Lib/re.py @@ -142,7 +142,7 @@ __version__ = "2.2.1" -class RegexFlag(enum.IntFlag): +class RegexFlag(enum.IntFlag, boundary=enum.KEEP): ASCII = A = sre_compile.SRE_FLAG_ASCII # assume ascii "locale" IGNORECASE = I = sre_compile.SRE_FLAG_IGNORECASE # ignore case LOCALE = L = sre_compile.SRE_FLAG_LOCALE # assume current 8-bit locale @@ -155,26 +155,17 @@ class RegexFlag(enum.IntFlag): DEBUG = sre_compile.SRE_FLAG_DEBUG # dump pattern after compilation def __repr__(self): - if self._name_ is not None: - return f're.{self._name_}' - value = self._value_ - members = [] - negative = value < 0 - if negative: - value = ~value - for m in self.__class__: - if value & m._value_: - value &= ~m._value_ - members.append(f're.{m._name_}') - if value: - members.append(hex(value)) - res = '|'.join(members) - if negative: - if len(members) > 1: - res = f'~({res})' - else: - res = f'~{res}' + res = '' + if self._name_: + member_names = self._name_.split('|') + constant = None + if member_names[-1].startswith('0x'): + constant = member_names.pop() + res = 're.' + '|re.'.join(member_names) + if constant: + res += '|%s' % constant return res + __str__ = object.__str__ globals().update(RegexFlag.__members__) diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index c1d02cfaf0dcb6..0e69401658ee3a 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -2173,14 +2173,16 @@ def test_long_pattern(self): def test_flags_repr(self): self.assertEqual(repr(re.I), "re.IGNORECASE") self.assertEqual(repr(re.I|re.S|re.X), - "re.IGNORECASE|re.DOTALL|re.VERBOSE") + "re.VERBOSE|re.DOTALL|re.IGNORECASE") self.assertEqual(repr(re.I|re.S|re.X|(1<<20)), - "re.IGNORECASE|re.DOTALL|re.VERBOSE|0x100000") - self.assertEqual(repr(~re.I), "~re.IGNORECASE") + "re.VERBOSE|re.DOTALL|re.IGNORECASE|0x100000") + self.assertEqual( + repr(~re.I), + "re.ASCII|re.DEBUG|re.VERBOSE|re.UNICODE|re.DOTALL|re.MULTILINE|re.LOCALE|re.TEMPLATE") self.assertEqual(repr(~(re.I|re.S|re.X)), - "~(re.IGNORECASE|re.DOTALL|re.VERBOSE)") + "re.ASCII|re.DEBUG|re.UNICODE|re.MULTILINE|re.LOCALE|re.TEMPLATE") self.assertEqual(repr(~(re.I|re.S|re.X|(1<<20))), - "~(re.IGNORECASE|re.DOTALL|re.VERBOSE|0x100000)") + "re.ASCII|re.DEBUG|re.UNICODE|re.MULTILINE|re.LOCALE|re.TEMPLATE|0xffe00") class ImplementationTest(unittest.TestCase): From 72dbdd70f88781c898cf7bac2af94584a94f169f Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 13 Jan 2021 17:16:35 -0800 Subject: [PATCH 04/30] remove extra white space --- Lib/enum.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 34ab988c4c8e24..e9419bd0922a9d 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1304,7 +1304,7 @@ def _decompose(flag, value): continue if multi._value_ & value == multi._value_: members.append(multi) - value ^= multi._value_ + value ^= multi._value_ if negative: members = [m for m in flag if m not in members] if value: @@ -1316,4 +1316,3 @@ def _power_of_two(value): if value < 1: return False return value == 2 ** _high_bit(value) - From 210fae7fbf4ba3d3ab3e1a356a3a9542321a3fb8 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 13 Jan 2021 17:43:39 -0800 Subject: [PATCH 05/30] test that zero-valued members are empty --- Lib/test/test_enum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 964fca6eac135a..03f9f38ac4fa6f 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -2544,6 +2544,7 @@ def test_member_contains(self): def test_member_iter(self): Color = self.Color + self.assertEqual(list(Color.BLACK), []) self.assertEqual(list(Color.PURPLE), [Color.BLUE, Color.RED]) self.assertEqual(list(Color.BLUE), [Color.BLUE]) self.assertEqual(list(Color.GREEN), [Color.GREEN]) @@ -3090,6 +3091,7 @@ def test_member_contains(self): def test_member_iter(self): Color = self.Color + self.assertEqual(list(Color.BLACK), []) self.assertEqual(list(Color.PURPLE), [Color.BLUE, Color.RED]) self.assertEqual(list(Color.BLUE), [Color.BLUE]) self.assertEqual(list(Color.GREEN), [Color.GREEN]) From 48bcd07c3738bc5db889068af8525f911aa39df7 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 13 Jan 2021 19:22:24 -0800 Subject: [PATCH 06/30] update tests to confirm CONFORM with negate --- Lib/enum.py | 4 +++- Lib/test/test_enum.py | 36 +++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index e9419bd0922a9d..a16b2d1aecfe46 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -75,8 +75,10 @@ def _bits(num): while num: digits.insert(0, num&1) num >>= 1 + if len(digits) < 4: + digits = ([0, 0, 0, 0] + digits)[-4:] if negative: - result = '0b1' + (''.join(['10'[d] for d in digits]).lstrip('0')) + result = '0b1' + (''.join(['10'[d] for d in digits])) else: result = '0b0' + ''.join(str(d) for d in digits) return result diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 03f9f38ac4fa6f..afdaf78ba4d74e 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -2404,22 +2404,23 @@ def test_boundary(self): class Iron(Flag, boundary=STRICT): ONE = 1 TWO = 2 - FOUR = 4 + EIGHT = 8 # class Water(Flag, boundary=CONFORM): ONE = 1 TWO = 2 - FOUR = 4 + EIGHT = 8 # class Space(Flag, boundary=EJECT): ONE = 1 TWO = 2 - FOUR = 4 + EIGHT = 8 # - self.assertRaisesRegex(ValueError, 'invalid value: 11', Iron, 11) - self.assertIs(Water(11), Water.ONE|Water.TWO) - self.assertEqual(Space(11), 11) - self.assertTrue(type(Space(11)) is int) + self.assertRaisesRegex(ValueError, 'invalid value: 7', Iron, 7) + self.assertIs(Water(7), Water.ONE|Water.TWO) + self.assertIs(Water(~9), Water.TWO) + self.assertEqual(Space(7), 7) + self.assertTrue(type(Space(7)) is int) def test_iter(self): Color = self.Color @@ -2924,25 +2925,26 @@ def test_invert(self): self.assertIs((Open.WO|Open.CE) & ~Open.WO, Open.CE) def test_boundary(self): - class Iron(Flag, boundary=STRICT): + class Iron(IntFlag, boundary=STRICT): ONE = 1 TWO = 2 - FOUR = 4 + EIGHT = 8 # - class Water(Flag, boundary=CONFORM): + class Water(IntFlag, boundary=CONFORM): ONE = 1 TWO = 2 - FOUR = 4 + EIGHT = 8 # - class Space(Flag, boundary=EJECT): + class Space(IntFlag, boundary=EJECT): ONE = 1 TWO = 2 - FOUR = 4 + EIGHT = 8 # - self.assertRaisesRegex(ValueError, 'invalid value: 11', Iron, 11) - self.assertIs(Water(11), Water.ONE|Water.TWO) - self.assertEqual(Space(11), 11) - self.assertTrue(type(Space(11)) is int) + self.assertRaisesRegex(ValueError, 'invalid value: 5', Iron, 5) + self.assertIs(Water(7), Water.ONE|Water.TWO) + self.assertIs(Water(~9), Water.TWO) + self.assertEqual(Space(7), 7) + self.assertTrue(type(Space(7)) is int) def test_iter(self): Color = self.Color From 1fd7471f351be3e477a52ded6a2303612cd0d06e Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 13 Jan 2021 20:16:49 -0800 Subject: [PATCH 07/30] update aenum.rst; add doctest to test_enum --- Doc/library/enum.rst | 104 ++++++++++++++++++++++++++++++++++-------- Lib/enum.py | 14 ++++-- Lib/test/test_enum.py | 8 ++++ 3 files changed, 105 insertions(+), 21 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 57e8721db57e7e..e4f685f438a0a4 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -197,7 +197,7 @@ Having two enum members with the same name is invalid:: ... Traceback (most recent call last): ... - TypeError: Attempted to reuse key: 'SQUARE' + TypeError: 'SQUARE' already defined as: 2 However, two enum members are allowed to have the same value. Given two members A and B with the same value (and A defined first), B is an alias to A. By-value @@ -422,7 +422,7 @@ any members. So this is forbidden:: ... Traceback (most recent call last): ... - TypeError: Cannot extend enumerations + TypeError: MoreColor: cannot extend enumeration 'Color' But this is allowed:: @@ -617,6 +617,7 @@ by extension, string enumerations of different types can also be compared to each other. :class:`StrEnum` exists to help avoid the problem of getting an incorrect member:: + >>> from enum import StrEnum >>> class Directions(StrEnum): ... NORTH = 'north', # notice the trailing comma ... SOUTH = 'south' @@ -712,7 +713,7 @@ be combined with them (but may lose :class:`IntFlag` membership:: .. note:: The negation operator, ``~``, always returns an :class:`IntFlag` member with a - positive number:: + positive value:: >>> ~Perm.X @@ -983,7 +984,7 @@ to handle any extra arguments:: ... BLEACHED_CORAL = () # New color, no Pantone code yet! ... >>> Swatch.SEA_GREEN - + >>> Swatch.SEA_GREEN.pantone '1246' >>> Swatch.BLEACHED_CORAL.pantone @@ -1209,11 +1210,9 @@ Private names are not converted to Enum members, but remain normal attributes. """""""""""""""""""" :class:`Enum` members are instances of their :class:`Enum` class, and are -normally accessed as ``EnumClass.member``. Under certain circumstances they -can also be accessed as ``EnumClass.member.member``, but you should never do -this as that lookup may fail or, worse, return something besides the -:class:`Enum` member you are looking for (this is another good reason to use -all-uppercase names for members):: +normally accessed as ``EnumClass.member``. In Python versions ``3.5`` to +``3.9`` you could access members from other members -- this practice was +discourages, and in ``3.10`` :class:`Enum` has returned to not allowing it:: >>> class FieldTypes(Enum): ... name = 0 @@ -1221,11 +1220,12 @@ all-uppercase names for members):: ... size = 2 ... >>> FieldTypes.value.size - - >>> FieldTypes.size.value - 2 + Traceback (most recent call last): + ... + AttributeError: FieldTypes: no attribute 'size' .. versionchanged:: 3.5 +.. versionchanged:: 3.10 Creating members that are mixed with other data types @@ -1267,14 +1267,14 @@ but not of the class:: >>> dir(Planet) ['EARTH', 'JUPITER', 'MARS', 'MERCURY', 'NEPTUNE', 'SATURN', 'URANUS', 'VENUS', '__class__', '__doc__', '__members__', '__module__'] >>> dir(Planet.EARTH) - ['__class__', '__doc__', '__module__', 'name', 'surface_gravity', 'value'] + ['__class__', '__doc__', '__module__', 'mass', 'name', 'radius', 'surface_gravity', 'value'] Combining members of ``Flag`` """"""""""""""""""""""""""""" -If a combination of Flag members is not named, the :func:`repr` will include -all named flags and all named combinations of flags that are in the value:: +Iterating over a combination of Flag members will only return the members that +are comprised of a single bit:: >>> class Color(Flag): ... RED = auto() @@ -1284,10 +1284,10 @@ all named flags and all named combinations of flags that are in the value:: ... YELLOW = RED | GREEN ... CYAN = GREEN | BLUE ... - >>> Color(3) # named combination + >>> Color(3) - >>> Color(7) # not named combination - + >>> Color(7) + ``StrEnum`` and :meth:`str.__str__` """"""""""""""""""""""""""""""""""" @@ -1299,3 +1299,71 @@ parts of Python will read the string data directly, while others will call :meth:`StrEnum.__str__` will be the same as :meth:`str.__str__` so that ``str(StrEnum.member) == StrEnum.member`` is true. +``Flag`` and ``IntFlag`` minutia +"""""""""""""""""""""""""""""""" + +The code sample:: + + >>> class Color(IntFlag): + ... BLACK = 0 + ... RED = 1 + ... GREEN = 2 + ... BLUE = 4 + ... PURPLE = RED | BLUE + ... WHITE = RED | GREEN | BLUE + ... + +- single-bit flags are canonical +- multi-bit and zero-bit flags are aliases +- only canonical flags are returned during iteration:: + + >>> list(Color.WHITE) + [, , ] + +- negating a flag or flag set returns a new flag/flag set with the + corresponding positive integer value:: + + >>> Color.GREEN + + + >>> ~Color.GREEN + + +- names of pseudo-flags are constructed from their members' names:: + + >>> (Color.RED | Color.GREEN).name + 'GREEN|RED' + +- multi-bit flags, aka aliases, can be returned from operations:: + + >>> Color.RED | Color.BLUE + + + >>> Color(7) # or Color(-1) + + +- membership / containment checking has changed slightly -- zero valued flags + are never considered to be contained:: + + >>> Color.BLACK in Color.WHITE + False + + otherwise, if all bits of one flag are in the other flag, True is returned:: + + >>> Color.PURPLE in Color.WHITE + True + +There is a new boundary mechanism that controls how out-of-range / invalid +bits are handled: ``STRICT``, ``CONFORM``, ``EJECT`', and ``KEEP``: + + * STRICT --> raises an exception when presented with invalid values + * CONFORM --> discards any invalid bits + * EJECT --> lose Flag status and become a normal int with the given value + * KEEP --> keep the extra bits + - keeps Flag status and extra bits + - they don't show up in iteration + - they do show up in repr() and str() + +The default for Flag is ``STRICT``, the default for ``IntFlag`` is ``DISCARD``, +and the default for ``_convert_`` is ``KEEP`` (see ``ssl.Options`` for an +example of when ``KEEP`` is needed). diff --git a/Lib/enum.py b/Lib/enum.py index a16b2d1aecfe46..ef51c04ac4b6f3 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -176,10 +176,10 @@ def __get__(self, instance, ownerclass=None): try: return ownerclass._member_map_[self.name] except KeyError: - raise AttributeError('%r not found in %r' % (self.name, ownerclass.__name__)) + raise AttributeError('%s: no attribute %r' % (ownerclass.__name__, self.name)) else: if self.fget is None: - raise AttributeError('%s: cannot read attribute %r' % (ownerclass.__name__, self.name)) + raise AttributeError('%s: no attribute %r' % (ownerclass.__name__, self.name)) else: return self.fget(instance) @@ -1161,13 +1161,21 @@ def __contains__(self, other): raise TypeError( "unsupported operand type(s) for 'in': '%s' and '%s'" % ( type(other).__qualname__, self.__class__.__qualname__)) + if other._value_ == 0 or self._value_ == 0: + return False return other._value_ & self._value_ == other._value_ def __iter__(self): """ Returns flags in decreasing value order. """ - return (m for m in reversed(self.__class__) if m._value_ & self._value_) + return ( + m + for m in sorted( + self.__class__, key=lambda m: m._value_, reverse=True + ) + if m._value_ & self._value_ + ) def __len__(self): return _bit_count(self._value_) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index afdaf78ba4d74e..89e619bf20b3ae 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -1,4 +1,5 @@ import enum +import doctest import inspect import pydoc import sys @@ -14,6 +15,13 @@ from test.support import threading_helper from datetime import timedelta +def load_tests(loader, tests, ignore): + tests.addTests(doctest.DocTestSuite(enum)) + tests.addTests(doctest.DocFileSuite( + '../../Doc/library/enum.rst', + optionflags=doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE, + )) + return tests # for pickle tests try: From aa425e6de2670e3a67b63e63c8ace521a0343f7b Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 14 Jan 2021 09:30:38 -0800 Subject: [PATCH 08/30] fix doc test --- Doc/library/enum.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index e4f685f438a0a4..a00671aefd21ce 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -715,8 +715,8 @@ be combined with them (but may lose :class:`IntFlag` membership:: The negation operator, ``~``, always returns an :class:`IntFlag` member with a positive value:: - >>> ~Perm.X - + >>> (~Perm.X).value == (Perm.R|Perm.W).value == 6 + True :class:`IntFlag` members can also be iterated over:: From 4786942ead0a4c0d95093476861ee8dfa99470d0 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 14 Jan 2021 14:31:31 -0800 Subject: [PATCH 09/30] optimizations --- Lib/enum.py | 104 +++++++++++++++--------------------------- Lib/test/test_enum.py | 39 +++++++++++++++- 2 files changed, 73 insertions(+), 70 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index ef51c04ac4b6f3..0ad499f37c4bd7 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -116,20 +116,10 @@ def _bit_count(num): (from https://wiki.python.org/moin/BitManipulation) """ count = 0 - while(num): + while num: num &= num - 1 count += 1 - return(count) - -def _bit_len(num): - """ - return number of bits required to represent num - """ - length = 0 - while num: - length += 1 - num >>= 1 - return length + return count def _is_single_bit(num): """ @@ -511,7 +501,7 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): raise TypeError('member order does not match _order_') # # remove Flag structures if final class is not a Flag - if Flag is None or not issubclass(enum_class, Flag): + if Flag is None and cls != 'Flag' or Flag is not None and not issubclass(enum_class, Flag): delattr(enum_class, '_boundary_') delattr(enum_class, '_flag_mask_') delattr(enum_class, '_all_bits_') @@ -526,15 +516,15 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): else: # multi-bit flags are considered aliases multi_bit_total |= flag._value_ - missed = [] - all_bit_total = 0 - for i in range(_bit_len(max(single_bit_total, multi_bit_total))): - i = 2**i - all_bit_total |= i - if i & multi_bit_total and not i & single_bit_total: + if enum_class._boundary_ is not KEEP: + missed_bits = multi_bit_total & ~single_bit_total + missed = [] + while missed_bits: + i = 2 ** (missed_bits.bit_length() - 1) missed.append(i) - if missed and enum_class._boundary_ is not KEEP: - raise TypeError('invalid Flag %r -- missing values: %s' % (cls, ', '.join((str(i) for i in missed)))) + missed_bits &= ~i + 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 # return enum_class @@ -1075,12 +1065,11 @@ def _generate_next_value_(name, start, count, last_values): """ if not count: return start if start is not None else 1 - for last_value in reversed(last_values): - try: - high_bit = _high_bit(last_value) - break - except Exception: - raise TypeError('Invalid Flag value: %r' % last_value) from None + last_value = max(last_values) + try: + high_bit = _high_bit(last_value) + except Exception: + raise TypeError('Invalid Flag value: %r' % last_value) from None return 2 ** (high_bit+1) @classmethod @@ -1100,7 +1089,7 @@ def _missing_(cls, value): ): if cls._boundary_ is STRICT: invalid_as_bits = _bits(value) - length = max(len(invalid_as_bits), _bit_len(cls._flag_mask_)) + length = max(len(invalid_as_bits), cls._flag_mask_.bit_length()) valid_as_bits = ('0' * length + _bits(cls._flag_mask_))[-length:] invalid_as_bits = ('01'[value<0] * length + invalid_as_bits)[-length:] raise ValueError( @@ -1169,13 +1158,13 @@ def __iter__(self): """ Returns flags in decreasing value order. """ - return ( - m - for m in sorted( - self.__class__, key=lambda m: m._value_, reverse=True - ) - if m._value_ & self._value_ - ) + val = self._value_ + while val: + i = 2 ** (val.bit_length() - 1) + member = self._value2member_map_.get(i) + if member is not None: + yield member + val &= ~i def __len__(self): return _bit_count(self._value_) @@ -1219,12 +1208,8 @@ def __invert__(self): # use all bits self._inverted_ = self.__class__(~self._value_) else: - # get flags not in this member - self._inverted_ = self.__class__(reduce( - _or_, - [m._value_ for m in self.__class__ if m not in self], - 0 - )) + # calculate flags not in this member + self._inverted_ = self.__class__(self._flag_mask_ ^ self._value_) self._inverted_._inverted_ = self return self._inverted_ @@ -1292,35 +1277,18 @@ def unique(enumeration): def _decompose(flag, value): """Extract all members from the value.""" - negative = value < 0 - if negative: - value = ~value members = [] - for member in flag: - if value & member._value_: + val = value + unknown = 0 + while val: + i = 2 ** (val.bit_length() - 1) + member = flag._value2member_map_.get(i) + if member is None: + unknown |= i + else: members.append(member) - value ^= member._value_ - if value: - # check value2member_map - possibles = [ - m - for v, m in list(flag._value2member_map_.items()) - if m not in flag - ] - possibles.sort(key=lambda m: m._value_, reverse=True) - for multi in possibles: - if multi._value_ == 0: - # do not add the zero flag - continue - if multi._value_ & value == multi._value_: - members.append(multi) - value ^= multi._value_ - if negative: - members = [m for m in flag if m not in members] - if value: - value = ~value - members.sort(key=lambda m: m._value_, reverse=True) - return members, value + val &= ~i + return members, unknown def _power_of_two(value): if value < 1: diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 89e619bf20b3ae..8798761d7d1c41 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -7,7 +7,7 @@ import threading from collections import OrderedDict from enum import Enum, IntEnum, StrEnum, EnumMeta, Flag, IntFlag, unique, auto -from enum import STRICT, CONFORM, EJECT +from enum import STRICT, CONFORM, EJECT, KEEP from io import StringIO from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL from test import support @@ -2409,26 +2409,38 @@ def test_bool(self): self.assertEqual(bool(f.value), bool(f)) def test_boundary(self): + self.assertIs(enum.Flag._boundary_, STRICT) class Iron(Flag, boundary=STRICT): ONE = 1 TWO = 2 EIGHT = 8 + self.assertIs(Iron._boundary_, STRICT) # class Water(Flag, boundary=CONFORM): ONE = 1 TWO = 2 EIGHT = 8 + self.assertIs(Water._boundary_, CONFORM) # class Space(Flag, boundary=EJECT): ONE = 1 TWO = 2 EIGHT = 8 + self.assertIs(Space._boundary_, EJECT) + # + class Bizarre(Flag, boundary=KEEP): + b = 3 + c = 4 + d = 6 # self.assertRaisesRegex(ValueError, 'invalid value: 7', Iron, 7) self.assertIs(Water(7), Water.ONE|Water.TWO) self.assertIs(Water(~9), Water.TWO) self.assertEqual(Space(7), 7) self.assertTrue(type(Space(7)) is int) + self.assertEqual(list(Bizarre), [Bizarre.c]) + self.assertIs(Bizarre(3), Bizarre.b) + self.assertIs(Bizarre(6), Bizarre.d) def test_iter(self): Color = self.Color @@ -2573,6 +2585,8 @@ def test_aliases(self): self.assertEqual(Color(7).name, 'WHITE') self.assertEqual(Color['BLANCO'].name, 'WHITE') self.assertIs(Color.BLANCO, Color.WHITE) + Open = self.Open + self.assertIs(Open['AC'], Open.AC) def test_auto_number(self): class Color(Flag): @@ -2599,7 +2613,7 @@ class Dupes(Enum): 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"): + with self.assertRaisesRegex(TypeError, "invalid Flag 'Bizarre' -- missing values: 2, 1"): class Bizarre(Flag): b = 3 c = 4 @@ -2933,26 +2947,38 @@ def test_invert(self): self.assertIs((Open.WO|Open.CE) & ~Open.WO, Open.CE) def test_boundary(self): + self.assertIs(enum.IntFlag._boundary_, EJECT) class Iron(IntFlag, boundary=STRICT): ONE = 1 TWO = 2 EIGHT = 8 + self.assertIs(Iron._boundary_, STRICT) # class Water(IntFlag, boundary=CONFORM): ONE = 1 TWO = 2 EIGHT = 8 + self.assertIs(Water._boundary_, CONFORM) # class Space(IntFlag, boundary=EJECT): ONE = 1 TWO = 2 EIGHT = 8 + self.assertIs(Space._boundary_, EJECT) + # + class Bizarre(IntFlag, boundary=KEEP): + b = 3 + c = 4 + d = 6 # self.assertRaisesRegex(ValueError, 'invalid value: 5', Iron, 5) self.assertIs(Water(7), Water.ONE|Water.TWO) self.assertIs(Water(~9), Water.TWO) self.assertEqual(Space(7), 7) self.assertTrue(type(Space(7)) is int) + self.assertEqual(list(Bizarre), [Bizarre.c]) + self.assertIs(Bizarre(3), Bizarre.b) + self.assertIs(Bizarre(6), Bizarre.d) def test_iter(self): Color = self.Color @@ -3120,6 +3146,8 @@ def test_aliases(self): self.assertEqual(Color(7).name, 'WHITE') self.assertEqual(Color['BLANCO'].name, 'WHITE') self.assertIs(Color.BLANCO, Color.WHITE) + Open = self.Open + self.assertIs(Open['AC'], Open.AC) def test_bool(self): Perm = self.Perm @@ -3129,6 +3157,13 @@ def test_bool(self): for f in Open: self.assertEqual(bool(f.value), bool(f)) + def test_bizarre(self): + with self.assertRaisesRegex(TypeError, "invalid Flag 'Bizarre' -- missing values: 2, 1"): + class Bizarre(IntFlag): + b = 3 + c = 4 + d = 6 + def test_multiple_mixin(self): class AllMixin: @classproperty From 45565b248ec416a17c58715fdf126b87a6441c21 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 14 Jan 2021 14:55:53 -0800 Subject: [PATCH 10/30] formatting --- Lib/enum.py | 177 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 125 insertions(+), 52 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 0ad499f37c4bd7..0eb65c3f2e9236 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -100,7 +100,8 @@ def _bit_count(num): * The C Programming Language 2nd Ed., Kernighan & Ritchie, 1988. - This works because each subtraction "borrows" from the lowest 1-bit. For example: + This works because each subtraction "borrows" from the lowest 1-bit. For + example: loop pass 1 loop pass 2 ----------- ----------- @@ -110,8 +111,8 @@ def _bit_count(num): & 101000 & 100000 = 100000 = 0 - It is an excellent technique for Python, since the size of the integer need not - be determined beforehand. + It is an excellent technique for Python, since the size of the integer need + not be determined beforehand. (from https://wiki.python.org/moin/BitManipulation) """ @@ -166,22 +167,34 @@ def __get__(self, instance, ownerclass=None): try: return ownerclass._member_map_[self.name] except KeyError: - raise AttributeError('%s: no attribute %r' % (ownerclass.__name__, self.name)) + raise AttributeError( + '%s: no attribute %r' + % (ownerclass.__name__, self.name) + ) else: if self.fget is None: - raise AttributeError('%s: no attribute %r' % (ownerclass.__name__, self.name)) + raise AttributeError( + '%s: no attribute %r' + % (ownerclass.__name__, self.name) + ) else: return self.fget(instance) def __set__(self, instance, value): if self.fset is None: - raise AttributeError("%s: cannot set attribute %r" % (self.clsname, self.name)) + raise AttributeError( + "%s: cannot set attribute %r" + % (self.clsname, self.name) + ) else: return self.fset(instance, value) def __delete__(self, instance): if self.fdel is None: - raise AttributeError("%s: cannot delete attribute %r" % (self.clsname, self.name)) + raise AttributeError( + "%s: cannot delete attribute %r" + % (self.clsname, self.name) + ) else: return self.fdel(instance) @@ -234,12 +247,19 @@ def __set_name__(self, enum_class, member_name): enum_member = canonical_member break else: - # this could still be an alias if the value is multi-bit and the class - # is a flag class - if Flag is None or not issubclass(enum_class, Flag): + # this could still be an alias if the value is multi-bit and the + # class is a flag class + if ( + Flag is None + or not issubclass(enum_class, Flag) + ): # no other instances found, record this member in _member_names_ enum_class._member_names_.append(member_name) - elif Flag is not None and issubclass(enum_class, Flag) and _is_single_bit(value): + elif ( + Flag is not None + and issubclass(enum_class, Flag) + and _is_single_bit(value) + ): # no other instances found, record this member in _member_names_ enum_class._member_names_.append(member_name) # get redirect in place before adding to _member_map_ @@ -261,8 +281,8 @@ def __set_name__(self, enum_class, member_name): redirect = property() redirect.__set_name__(enum_class, member_name) if descriptor and need_override: - # previous enum.property found, but some other inherited attribute - # is in the way; copy fget, fset, fdel to this one + # previous enum.property found, but some other inherited + # attribute is in the way; copy fget, fset, fdel to this one redirect.fget = descriptor.fget redirect.fset = descriptor.fset redirect.fdel = descriptor.fdel @@ -310,13 +330,17 @@ def __setitem__(self, key, value): '_generate_next_value_', '_missing_', '_ignore_', ): raise ValueError( - '_sunder_ names, such as %r, are reserved for future Enum use' + '_sunder_ names, such as %r, are reserved for future' + ' Enum use' % (key, ) ) if key == '_generate_next_value_': # check if members already defined as auto() if self._auto_called: - raise TypeError("_generate_next_value_ must be defined before members") + raise TypeError( + "_generate_next_value_ must be defined before" + " members" + ) setattr(self, '_generate_next_value', value) elif key == '_ignore_': if isinstance(value, str): @@ -371,6 +395,7 @@ class EnumMeta(type): """ Metaclass for Enum """ + @classmethod def __prepare__(metacls, cls, bases, **kwds): # check that previous enum members do not exist @@ -436,7 +461,10 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): classdict['_member_type_'] = member_type # # Flag structures (will be removed if final class is not a Flag - classdict['_boundary_'] = boundary or getattr(first_enum, '_boundary_', None) + classdict['_boundary_'] = ( + boundary + or getattr(first_enum, '_boundary_', None) + ) classdict['_flag_mask_'] = flag_mask classdict['_all_bits_'] = 2 ** ((flag_mask).bit_length()) - 1 classdict['_inverted_'] = None @@ -465,8 +493,9 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): exc = None enum_class = super().__new__(metacls, cls, bases, classdict, **kwds) except RuntimeError as e: - # any exceptions raised by member.__new__ will get converted to a - # RuntimeError, so get that original exception back and raise it instead + # any exceptions raised by member.__new__ will get converted to + # a RuntimeError, so get that original exception back and raise + # it instead exc = e.__cause__ or e if exc is not None: raise exc @@ -501,7 +530,10 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): raise TypeError('member order does not match _order_') # # remove Flag structures if final class is not a Flag - if Flag is None and cls != 'Flag' or Flag is not None and not issubclass(enum_class, Flag): + if ( + Flag is None and cls != 'Flag' + or Flag is not None and not issubclass(enum_class, Flag) + ): delattr(enum_class, '_boundary_') delattr(enum_class, '_flag_mask_') delattr(enum_class, '_all_bits_') @@ -524,7 +556,10 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): missed.append(i) missed_bits &= ~i if missed: - raise TypeError('invalid Flag %r -- missing values: %s' % (cls, ', '.join((str(i) for i in missed)))) + raise TypeError( + 'invalid Flag %r -- missing values: %s' + % (cls, ', '.join((str(i) for i in missed))) + ) enum_class._flag_mask_ = single_bit_total # return enum_class @@ -553,7 +588,8 @@ def __call__( `value` will be the name of the new class. `names` should be either a string of white-space/comma delimited names - (values will start at `start`), or an iterator/mapping of name, value pairs. + (values will start at `start`), or an iterator/mapping of name, value + pairs. `module` should be set to the module this class is being created in; if it is not set, an attempt to find that module will be made, but if @@ -589,7 +625,10 @@ def __delattr__(cls, attr): # nicer error message when someone tries to delete an attribute # (see issue19025). if attr in cls._member_map_: - raise AttributeError("%s: cannot delete Enum member %r." % (cls.__name__, attr)) + raise AttributeError( + "%s: cannot delete Enum member %r." + % (cls.__name__, attr) + ) super().__delattr__(attr) def __dir__(self): @@ -671,7 +710,8 @@ def _create_( * A string containing member names, separated either with spaces or commas. Values are incremented by 1 from `start`. - * An iterable of member names. Values are incremented by 1 from `start`. + * An iterable of member names. Values are incremented by 1 from + `start`. * An iterable of (member name, value) pairs. * A mapping of member name -> value pairs. """ @@ -679,18 +719,24 @@ def _create_( bases = (cls, ) if type is None else (type, cls) _, first_enum = cls._get_mixins_(cls, bases) classdict = metacls.__prepare__(class_name, bases) - + # # special processing needed for names? if isinstance(names, str): names = names.replace(',', ' ').split() - if isinstance(names, (tuple, list)) and names and isinstance(names[0], str): + if ( + isinstance(names, (tuple, list)) + and names + and isinstance(names[0], str) + ): original_names, names = names, [] last_values = [] for count, name in enumerate(original_names): - value = first_enum._generate_next_value_(name, start, count, last_values[:]) + value = first_enum._generate_next_value_( + name, start, count, last_values[:] + ) last_values.append(value) names.append((name, value)) - + # # Here, names is either an iterable of (name, value) or a mapping. for item in names: if isinstance(item, str): @@ -698,7 +744,7 @@ def _create_( else: member_name, member_value = item classdict[member_name] = member_value - + # # TODO: replace the frame hack if a blessed way to know the calling # module is ever developed if module is None: @@ -712,7 +758,7 @@ def _create_( classdict['__module__'] = module if qualname is not None: classdict['__qualname__'] = qualname - + # return metacls.__new__( metacls, class_name, bases, classdict, boundary=boundary, @@ -771,7 +817,7 @@ def _get_mixins_(class_name, bases): """ if not bases: return object, Enum - + # def _find_data_type(bases): data_types = [] for chain in bases: @@ -791,12 +837,15 @@ def _find_data_type(bases): else: candidate = base if len(data_types) > 1: - raise TypeError('%r: too many data types: %r' % (class_name, data_types)) + raise TypeError( + '%r: too many data types: %r' + % (class_name, data_types) + ) elif data_types: return data_types[0] else: return None - + # # ensure final parent class is an Enum derivative, find any concrete # data type, and check that Enum has no members first_enum = bases[-1] @@ -821,10 +870,10 @@ def _find_new_(classdict, member_type, first_enum): # by the user; also check earlier enum classes in case a __new__ was # saved as __new_member__ __new__ = classdict.get('__new__', None) - + # # should __new__ be saved as __new_member__ later? save_new = __new__ is not None - + # if __new__ is None: # check all possibles for __new_member__ before falling back to # __new__ @@ -843,7 +892,7 @@ def _find_new_(classdict, member_type, first_enum): break else: __new__ = object.__new__ - + # # if a non-object.__new__ is used then whatever value/tuple was # assigned to the enum member name will be passed to __new__ and to the # new enum member's __init__ @@ -895,12 +944,15 @@ def __new__(cls, value): ): return result else: - ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__)) + ve_exc = ValueError( + "%r is not a valid %s" % (value, cls.__qualname__) + ) if result is None and exc is None: raise ve_exc elif exc is None: exc = TypeError( - 'error in %s._missing_: returned %r instead of None or a valid member' + 'error in %s._missing_: returned %r instead of None' + ' or a valid member' % (cls.__name__, result) ) if not isinstance(exc, ValueError): @@ -954,7 +1006,7 @@ def __format__(self, format_spec): # mixed-in Enums should use the mixed-in type's __format__, otherwise # we can get strange results with the Enum name showing up instead of # the value - + # # pure Enum branch, or branch with __str__ explicitly overridden str_overridden = type(self).__str__ not in (Enum.__str__, Flag.__str__) if self._member_type_ is object or str_overridden: @@ -1004,19 +1056,25 @@ class StrEnum(str, Enum): def __new__(cls, *values): if len(values) > 3: - raise TypeError('too many arguments for str(): %r' % (values, )) + raise TypeError( + 'too many arguments for str(): %r' % (values, ) + ) if len(values) == 1: # it must be a string if not isinstance(values[0], str): raise TypeError('%r is not a string' % (values[0], )) - if len(values) > 1: + if len(values) >= 2: # check that encoding argument is a string if not isinstance(values[1], str): - raise TypeError('encoding must be a string, not %r' % (values[1], )) - if len(values) > 2: - # check that errors argument is a string - if not isinstance(values[2], str): - raise TypeError('errors must be a string, not %r' % (values[2], )) + raise TypeError( + 'encoding must be a string, not %r' % (values[1], ) + ) + if len(values) == 3: + # check that errors argument is a string + if not isinstance(values[2], str): + raise TypeError( + 'errors must be a string, not %r' % (values[2], ) + ) value = str(*values) member = str.__new__(cls, value) member._value_ = value @@ -1078,10 +1136,13 @@ def _missing_(cls, value): Create a composite member iff value contains only members. """ if not isinstance(value, int): - raise ValueError("%r is not a valid %s" % (value, cls.__qualname__)) + raise ValueError( + "%r is not a valid %s" % (value, cls.__qualname__) + ) # check boundaries # - value must be in range (e.g. -16 <-> +15, i.e. ~15 <-> 15) - # - value must not include any skipped flags (e.g. if bit 2 is not defined, then 0d10 is invalid) + # - value must not include any skipped flags (e.g. if bit 2 is not + # defined, then 0d10 is invalid) neg_value = None if ( not ~cls._all_bits_ <= value <= cls._all_bits_ @@ -1089,9 +1150,16 @@ def _missing_(cls, value): ): if cls._boundary_ is STRICT: invalid_as_bits = _bits(value) - length = max(len(invalid_as_bits), cls._flag_mask_.bit_length()) - valid_as_bits = ('0' * length + _bits(cls._flag_mask_))[-length:] - invalid_as_bits = ('01'[value<0] * length + invalid_as_bits)[-length:] + length = max( + len(invalid_as_bits), + cls._flag_mask_.bit_length() + ) + valid_as_bits = ( + ('0' * length + _bits(cls._flag_mask_))[-length:] + ) + invalid_as_bits = ( + ('01'[value<0] * length + invalid_as_bits)[-length:] + ) raise ValueError( "%s: invalid value: %r\n given %s\n allowed %s" % ( cls.__name__, @@ -1105,9 +1173,14 @@ def _missing_(cls, value): return value elif cls._boundary_ is KEEP: if value < 0: - value = max(cls._all_bits_+1, 2**(value.bit_length())) + value + value = ( + max(cls._all_bits_+1, 2**(value.bit_length())) + + value + ) else: - raise ValueError('unknown flag boundary: %r' % (cls._boundary_, )) + raise ValueError( + 'unknown flag boundary: %r' % (cls._boundary_, ) + ) if value < 0: neg_value = value value = cls._all_bits_ + 1 + value From 806c8c62f14483a2e19e35e9b45b7e15005cce4f Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 14 Jan 2021 15:07:19 -0800 Subject: [PATCH 11/30] add news entry --- .../Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst diff --git a/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst b/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst new file mode 100644 index 00000000000000..ea236f880f0c19 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst @@ -0,0 +1,12 @@ +[Enum] Flags consisting of a single bit are now considered canonical, and +will be the only flags returned from listing and iterating over a Flag class +or a Flag member. Multi-bit flags are considered aliases; they will be +returned from lookups and operations that result in their value. + +For example: + +>>> class Color(Flag): ... BLACK = 0 ... RED = 1 ... +GREEN = 2 ... BLUE = 4 ... WHITE = 7 ... >>> +list(Color) [, , ] >>> +Color(7) >>> Color.RED | Color.GREEN | Color.BLUE + >>> Color.RED & Color.GREEN From 668c9a9d14e5c6495ea75abf762c9e3a6e5d0167 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 14 Jan 2021 16:33:12 -0800 Subject: [PATCH 12/30] fix formatting of news entry --- .../2021-01-14-15-07-16.bpo-38250.1fvhOk.rst | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst b/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst index ea236f880f0c19..a8b34125f882e8 100644 --- a/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst +++ b/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst @@ -5,8 +5,18 @@ returned from lookups and operations that result in their value. For example: ->>> class Color(Flag): ... BLACK = 0 ... RED = 1 ... -GREEN = 2 ... BLUE = 4 ... WHITE = 7 ... >>> -list(Color) [, , ] >>> -Color(7) >>> Color.RED | Color.GREEN | Color.BLUE - >>> Color.RED & Color.GREEN + >>> class Color(Flag): + ... BLACK = 0 + ... RED = 1 + ... GREEN = 2 + ... BLUE = 4 + ... WHITE = 7 + ... + >>> list(Color) + [, , ] + >>> Color(7) + + >>> Color.RED | Color.GREEN | Color.BLUE + + >>> Color.RED & Color.GREEN + From 9bd9e97469c5592d43973bf3e93ea9a39f5298e1 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Tue, 19 Jan 2021 23:01:08 -0800 Subject: [PATCH 13/30] update iteration method and order Iteration is now in member definition order. If member definition order matches increasing value order, then a more efficient method of flag decomposition is used; otherwise, sort() is called on the results of that method to get definition order. --- Doc/library/enum.rst | 16 +- Lib/enum.py | 195 ++++++++---------- Lib/test/test_enum.py | 48 ++--- Lib/test/test_re.py | 10 +- Lib/types.py | 2 +- .../2021-01-14-15-07-16.bpo-38250.1fvhOk.rst | 18 -- 6 files changed, 119 insertions(+), 170 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index a00671aefd21ce..933c03ce099923 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -782,7 +782,7 @@ value:: >>> purple = Color.RED | Color.BLUE >>> list(purple) - [, ] + [, ] .. versionadded:: 3.10 @@ -1212,7 +1212,7 @@ Private names are not converted to Enum members, but remain normal attributes. :class:`Enum` members are instances of their :class:`Enum` class, and are normally accessed as ``EnumClass.member``. In Python versions ``3.5`` to ``3.9`` you could access members from other members -- this practice was -discourages, and in ``3.10`` :class:`Enum` has returned to not allowing it:: +discouraged, and in ``3.10`` :class:`Enum` has returned to not allowing it:: >>> class FieldTypes(Enum): ... name = 0 @@ -1287,7 +1287,7 @@ are comprised of a single bit:: >>> Color(3) >>> Color(7) - + ``StrEnum`` and :meth:`str.__str__` """"""""""""""""""""""""""""""""""" @@ -1311,14 +1311,14 @@ The code sample:: ... BLUE = 4 ... PURPLE = RED | BLUE ... WHITE = RED | GREEN | BLUE - ... + ... - single-bit flags are canonical - multi-bit and zero-bit flags are aliases - only canonical flags are returned during iteration:: >>> list(Color.WHITE) - [, , ] + [, , ] - negating a flag or flag set returns a new flag/flag set with the corresponding positive integer value:: @@ -1332,7 +1332,7 @@ The code sample:: - names of pseudo-flags are constructed from their members' names:: >>> (Color.RED | Color.GREEN).name - 'GREEN|RED' + 'RED|GREEN' - multi-bit flags, aka aliases, can be returned from operations:: @@ -1361,8 +1361,8 @@ bits are handled: ``STRICT``, ``CONFORM``, ``EJECT`', and ``KEEP``: * EJECT --> lose Flag status and become a normal int with the given value * KEEP --> keep the extra bits - keeps Flag status and extra bits - - they don't show up in iteration - - they do show up in repr() and str() + - extra bits do not show up in iteration + - extra bits do show up in repr() and str() The default for Flag is ``STRICT``, the default for ``IntFlag`` is ``DISCARD``, and the default for ``_convert_`` is ``KEEP`` (see ``ssl.Options`` for an diff --git a/Lib/enum.py b/Lib/enum.py index 0eb65c3f2e9236..60c805fb68bdbb 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1,8 +1,6 @@ import sys from types import MappingProxyType, DynamicClassAttribute -from operator import or_ as _or_, and_ as _and_, xor as _xor_, inv as _inv_ -from functools import reduce -from builtins import property as _bltin_property +from builtins import property as _bltin_property, bin as _bltin_bin __all__ = [ @@ -64,64 +62,6 @@ def _is_private(cls_name, name): else: return False -def _bits(num): - if num == 0: - return '0b0' - negative = False - if num < 0: - negative = True - num = ~num - digits = [] - while num: - digits.insert(0, num&1) - num >>= 1 - if len(digits) < 4: - digits = ([0, 0, 0, 0] + digits)[-4:] - if negative: - result = '0b1' + (''.join(['10'[d] for d in digits])) - else: - result = '0b0' + ''.join(str(d) for d in digits) - return result - -def _bit_count(num): - """ - return number of set bits - - Counting bits set, Brian Kernighan's way* - - unsigned int v; // count the number of bits set in v - unsigned int c; // c accumulates the total bits set in v - for (c = 0; v; c++) - { v &= v - 1; } //clear the least significant bit set - - This method goes through as many iterations as there are set bits. So if we - have a 32-bit word with only the high bit set, then it will only go once - through the loop. - - * The C Programming Language 2nd Ed., Kernighan & Ritchie, 1988. - - This works because each subtraction "borrows" from the lowest 1-bit. For - example: - - loop pass 1 loop pass 2 - ----------- ----------- - 101000 100000 - - 1 - 1 - = 100111 = 011111 - & 101000 & 100000 - = 100000 = 0 - - It is an excellent technique for Python, since the size of the integer need - not be determined beforehand. - - (from https://wiki.python.org/moin/BitManipulation) - """ - count = 0 - while num: - num &= num - 1 - count += 1 - return count - def _is_single_bit(num): """ True if only one bit set in num (should be an int) @@ -146,6 +86,37 @@ def _break_on_call_reduce(self, proto): setattr(obj, '__reduce_ex__', _break_on_call_reduce) setattr(obj, '__module__', '') +def _iter_bits_lsb(num): + while num: + b = num & (~num + 1) + yield b + num ^= b + +def bin(num, max_bits=None): + """ + Like built-in bin(), except negative values are represented in + twos-compliment, and the leading bit always indicates sign + (0=positive, 1=negative). + + >>> bin(10) + '0b0 1010' + >>> bin(~10) # ~10 is -11 + '0b1 0101' + """ + + ceiling = 2 ** (num).bit_length() + if num >= 0: + s = _bltin_bin(num + ceiling).replace('1', '0', 1) + else: + s = _bltin_bin(~num ^ (ceiling - 1) + ceiling) + sign = s[:3] + digits = s[3:] + if max_bits is not None: + if len(digits) < max_bits: + digits = (sign[-1] * max_bits + digits)[-max_bits:] + return "%s %s" % (sign, digits) + + _auto_null = object() class auto: """ @@ -153,6 +124,7 @@ class auto: """ value = _auto_null + class property(DynamicClassAttribute): """ This is a descriptor, used to define attributes that act differently @@ -240,6 +212,7 @@ def __set_name__(self, enum_class, member_name): enum_member._name_ = member_name enum_member.__objclass__ = enum_class enum_member.__init__(*args) + enum_member._sort_order_ = len(enum_class._member_names_) # If another member with the same value was already defined, the # new member becomes an alias to the existing one. for name, canonical_member in enum_class._member_map_.items(): @@ -328,6 +301,7 @@ def __setitem__(self, key, value): if key not in ( '_order_', '_create_pseudo_member_', '_generate_next_value_', '_missing_', '_ignore_', + '_iter_member_', '_iter_member_by_value_', '_iter_member_by_def_', ): raise ValueError( '_sunder_ names, such as %r, are reserved for future' @@ -369,10 +343,7 @@ def __setitem__(self, key, value): if isinstance(value, auto): if value.value == _auto_null: value.value = self._generate_next_value( - key, - 1, - len(self._member_names), - self._last_values[:], + key, 1, len(self._member_names), self._last_values[:], ) self._auto_called = True value = value.value @@ -549,18 +520,25 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): # multi-bit flags are considered aliases multi_bit_total |= flag._value_ if enum_class._boundary_ is not KEEP: - missed_bits = multi_bit_total & ~single_bit_total - missed = [] - while missed_bits: - i = 2 ** (missed_bits.bit_length() - 1) - missed.append(i) - missed_bits &= ~i + # missed_bits = multi_bit_total & ~single_bit_total + missed = list(_iter_bits_lsb(multi_bit_total & ~single_bit_total)) + # while missed_bits: + # i = 2 ** (missed_bits.bit_length() - 1) + # missed.append(i) + # missed_bits &= ~i 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 + # + # set correct __iter__ + inverted = ~enum_class(0) + if list(enum_class) != list(inverted): + if '|' in inverted._name_: + del enum_class._value2member_map_[inverted._value_] + enum_class._iter_member_ = enum_class._iter_member_by_def_ # return enum_class @@ -1130,6 +1108,28 @@ def _generate_next_value_(name, start, count, last_values): raise TypeError('Invalid Flag value: %r' % last_value) from None return 2 ** (high_bit+1) + @classmethod + def _iter_member_by_value_(cls, value): + """ + Extract all members from the value in definition (i.e. increasing value) order. + """ + for val in _iter_bits_lsb(value): + member = cls._value2member_map_.get(val) + if member is not None: + yield member + + _iter_member_ = _iter_member_by_value_ + + @classmethod + def _iter_member_by_def_(cls, value): + """ + Extract all members from the value in definition order. + """ + members = list(cls._iter_member_by_value_(value)) + members.sort(key=lambda m: m._sort_order_) + for member in members: + yield member + @classmethod def _missing_(cls, value): """ @@ -1149,23 +1149,10 @@ def _missing_(cls, value): or value & (cls._all_bits_ ^ cls._flag_mask_) ): if cls._boundary_ is STRICT: - invalid_as_bits = _bits(value) - length = max( - len(invalid_as_bits), - cls._flag_mask_.bit_length() - ) - valid_as_bits = ( - ('0' * length + _bits(cls._flag_mask_))[-length:] - ) - invalid_as_bits = ( - ('01'[value<0] * length + invalid_as_bits)[-length:] - ) + max_bits = max(value.bit_length(), cls._flag_mask_.bit_length()) raise ValueError( "%s: invalid value: %r\n given %s\n allowed %s" % ( - cls.__name__, - value, - invalid_as_bits, - valid_as_bits, + cls.__name__, value, bin(value, max_bits), bin(cls._flag_mask_, max_bits), )) elif cls._boundary_ is CONFORM: value = value & cls._flag_mask_ @@ -1184,12 +1171,13 @@ def _missing_(cls, value): if value < 0: neg_value = value value = cls._all_bits_ + 1 + value - # get members - members, unknown = _decompose(cls, value) + # get members and unknown + unknown = value & ~cls._flag_mask_ + members = list(cls._iter_member_(value)) if unknown and cls._boundary_ is not KEEP: raise ValueError( - '%s: _decompose(%r) --> %r, %r' - % (cls.__name__, value, members, unknown) + '%s(%r) --> unknown values %r [%s]' + % (cls.__name__, value, unknown, bin(unknown)) ) # normal Flag? __new__ = getattr(cls, '__new_member__', None) @@ -1229,18 +1217,12 @@ def __contains__(self, other): def __iter__(self): """ - Returns flags in decreasing value order. + Returns flags in definition order. """ - val = self._value_ - while val: - i = 2 ** (val.bit_length() - 1) - member = self._value2member_map_.get(i) - if member is not None: - yield member - val &= ~i + yield from self._iter_member_(self._value_) def __len__(self): - return _bit_count(self._value_) + return self._value_.bit_count() def __repr__(self): cls = self.__class__ @@ -1348,21 +1330,6 @@ def unique(enumeration): (enumeration, alias_details)) return enumeration -def _decompose(flag, value): - """Extract all members from the value.""" - members = [] - val = value - unknown = 0 - while val: - i = 2 ** (val.bit_length() - 1) - member = flag._value2member_map_.get(i) - if member is None: - unknown |= i - else: - members.append(member) - val &= ~i - return members, unknown - def _power_of_two(value): if value < 1: return False diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 8798761d7d1c41..dd1866dbc58e1a 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -2300,9 +2300,9 @@ def test_str(self): self.assertEqual(str(Open.WO), 'Open.WO') self.assertEqual(str(Open.AC), 'Open.AC') self.assertEqual(str(Open.RO | Open.CE), 'Open.CE') - self.assertEqual(str(Open.WO | Open.CE), 'Open.CE|WO') - self.assertEqual(str(~Open.RO), 'Open.CE|RW|WO') - self.assertEqual(str(~Open.WO), 'Open.CE|RW') + self.assertEqual(str(Open.WO | Open.CE), 'Open.WO|CE') + self.assertEqual(str(~Open.RO), 'Open.WO|RW|CE') + self.assertEqual(str(~Open.WO), 'Open.RW|CE') self.assertEqual(str(~Open.AC), 'Open.CE') self.assertEqual(str(~Open.CE), 'Open.AC') self.assertEqual(str(~(Open.RO | Open.CE)), 'Open.AC') @@ -2328,9 +2328,9 @@ def test_repr(self): self.assertEqual(repr(Open.WO), '') self.assertEqual(repr(Open.AC), '') self.assertEqual(repr(Open.RO | Open.CE), '') - self.assertEqual(repr(Open.WO | Open.CE), '') - self.assertEqual(repr(~Open.RO), '') - self.assertEqual(repr(~Open.WO), '') + self.assertEqual(repr(Open.WO | Open.CE), '') + self.assertEqual(repr(~Open.RO), '') + self.assertEqual(repr(~Open.WO), '') self.assertEqual(repr(~Open.AC), '') self.assertEqual(repr(~Open.CE), '') self.assertEqual(repr(~(Open.RO | Open.CE)), '') @@ -2566,11 +2566,11 @@ def test_member_contains(self): def test_member_iter(self): Color = self.Color self.assertEqual(list(Color.BLACK), []) - self.assertEqual(list(Color.PURPLE), [Color.BLUE, Color.RED]) + self.assertEqual(list(Color.PURPLE), [Color.RED, Color.BLUE]) self.assertEqual(list(Color.BLUE), [Color.BLUE]) self.assertEqual(list(Color.GREEN), [Color.GREEN]) - self.assertEqual(list(Color.WHITE), [Color.BLUE, Color.GREEN, Color.RED]) - self.assertEqual(list(Color.WHITE), [Color.BLUE, Color.GREEN, Color.RED]) + self.assertEqual(list(Color.WHITE), [Color.RED, Color.GREEN, Color.BLUE]) + self.assertEqual(list(Color.WHITE), [Color.RED, Color.GREEN, Color.BLUE]) def test_member_length(self): self.assertEqual(self.Color.__len__(self.Color.BLACK), 0) @@ -2613,7 +2613,7 @@ class Dupes(Enum): self.assertEqual([Dupes.first, Dupes.second, Dupes.third], list(Dupes)) def test_bizarre(self): - with self.assertRaisesRegex(TypeError, "invalid Flag 'Bizarre' -- missing values: 2, 1"): + with self.assertRaisesRegex(TypeError, "invalid Flag 'Bizarre' -- missing values: 1, 2"): class Bizarre(Flag): b = 3 c = 4 @@ -2741,9 +2741,9 @@ class TestIntFlag(unittest.TestCase): """Tests of the IntFlags.""" class Perm(IntFlag): - X = 1 << 0 - W = 1 << 1 R = 1 << 2 + W = 1 << 1 + X = 1 << 0 class Open(IntFlag): RO = 0 @@ -2807,10 +2807,10 @@ def test_str(self): self.assertEqual(str(Open.WO), 'Open.WO') self.assertEqual(str(Open.AC), 'Open.AC') self.assertEqual(str(Open.RO | Open.CE), 'Open.CE') - self.assertEqual(str(Open.WO | Open.CE), 'Open.CE|WO') + self.assertEqual(str(Open.WO | Open.CE), 'Open.WO|CE') self.assertEqual(str(Open(4)), '4') - self.assertEqual(str(~Open.RO), 'Open.CE|RW|WO') - self.assertEqual(str(~Open.WO), 'Open.CE|RW') + self.assertEqual(str(~Open.RO), 'Open.WO|RW|CE') + self.assertEqual(str(~Open.WO), 'Open.RW|CE') self.assertEqual(str(~Open.AC), 'Open.CE') self.assertEqual(str(~Open.CE), 'Open.AC') self.assertEqual(str(~(Open.RO | Open.CE)), 'Open.AC') @@ -2818,7 +2818,7 @@ def test_str(self): self.assertEqual(str(Open(~4)), '-5') Skip = self.Skip - self.assertEqual(str(Skip(~4)), 'Skip.EIGHTH|SECOND|FIRST') + self.assertEqual(str(Skip(~4)), 'Skip.FIRST|SECOND|EIGHTH') def test_repr(self): Perm = self.Perm @@ -2844,17 +2844,17 @@ def test_repr(self): self.assertEqual(repr(Open.WO), '') self.assertEqual(repr(Open.AC), '') self.assertEqual(repr(Open.RO | Open.CE), '') - self.assertEqual(repr(Open.WO | Open.CE), '') + self.assertEqual(repr(Open.WO | Open.CE), '') self.assertEqual(repr(Open(4)), '4') - self.assertEqual(repr(~Open.RO), '') - self.assertEqual(repr(~Open.WO), '') + self.assertEqual(repr(~Open.RO), '') + self.assertEqual(repr(~Open.WO), '') self.assertEqual(repr(~Open.AC), '') self.assertEqual(repr(~(Open.RO | Open.CE)), '') self.assertEqual(repr(~(Open.WO | Open.CE)), '') self.assertEqual(repr(Open(~4)), '-5') Skip = self.Skip - self.assertEqual(repr(Skip(~4)), '') + self.assertEqual(repr(Skip(~4)), '') def test_format(self): Perm = self.Perm @@ -3128,10 +3128,10 @@ def test_member_contains(self): def test_member_iter(self): Color = self.Color self.assertEqual(list(Color.BLACK), []) - self.assertEqual(list(Color.PURPLE), [Color.BLUE, Color.RED]) + self.assertEqual(list(Color.PURPLE), [Color.RED, Color.BLUE]) self.assertEqual(list(Color.BLUE), [Color.BLUE]) self.assertEqual(list(Color.GREEN), [Color.GREEN]) - self.assertEqual(list(Color.WHITE), [Color.BLUE, Color.GREEN, Color.RED]) + self.assertEqual(list(Color.WHITE), [Color.RED, Color.GREEN, Color.BLUE]) def test_member_length(self): self.assertEqual(self.Color.__len__(self.Color.BLACK), 0) @@ -3158,7 +3158,7 @@ def test_bool(self): self.assertEqual(bool(f.value), bool(f)) def test_bizarre(self): - with self.assertRaisesRegex(TypeError, "invalid Flag 'Bizarre' -- missing values: 2, 1"): + with self.assertRaisesRegex(TypeError, "invalid Flag 'Bizarre' -- missing values: 1, 2"): class Bizarre(IntFlag): b = 3 c = 4 @@ -3466,7 +3466,7 @@ def test_inspect_classify_class_attrs(self): class MiscTestCase(unittest.TestCase): def test__all__(self): - support.check__all__(self, enum) + support.check__all__(self, enum, not_exported={'bin'}) # These are unordered here on purpose to ensure that declaration order diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index 0e69401658ee3a..bd689582523c32 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -2173,16 +2173,16 @@ def test_long_pattern(self): def test_flags_repr(self): self.assertEqual(repr(re.I), "re.IGNORECASE") self.assertEqual(repr(re.I|re.S|re.X), - "re.VERBOSE|re.DOTALL|re.IGNORECASE") + "re.IGNORECASE|re.DOTALL|re.VERBOSE") self.assertEqual(repr(re.I|re.S|re.X|(1<<20)), - "re.VERBOSE|re.DOTALL|re.IGNORECASE|0x100000") + "re.IGNORECASE|re.DOTALL|re.VERBOSE|0x100000") self.assertEqual( repr(~re.I), - "re.ASCII|re.DEBUG|re.VERBOSE|re.UNICODE|re.DOTALL|re.MULTILINE|re.LOCALE|re.TEMPLATE") + "re.ASCII|re.LOCALE|re.UNICODE|re.MULTILINE|re.DOTALL|re.VERBOSE|re.TEMPLATE|re.DEBUG") self.assertEqual(repr(~(re.I|re.S|re.X)), - "re.ASCII|re.DEBUG|re.UNICODE|re.MULTILINE|re.LOCALE|re.TEMPLATE") + "re.ASCII|re.LOCALE|re.UNICODE|re.MULTILINE|re.TEMPLATE|re.DEBUG") self.assertEqual(repr(~(re.I|re.S|re.X|(1<<20))), - "re.ASCII|re.DEBUG|re.UNICODE|re.MULTILINE|re.LOCALE|re.TEMPLATE|0xffe00") + "re.ASCII|re.LOCALE|re.UNICODE|re.MULTILINE|re.TEMPLATE|re.DEBUG|0xffe00") class ImplementationTest(unittest.TestCase): diff --git a/Lib/types.py b/Lib/types.py index 532f4806fc0226..b61f382b6b9273 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -155,7 +155,7 @@ class DynamicClassAttribute: class's __getattr__ method; this is done by raising AttributeError. This allows one to have properties active on an instance, and have virtual - attributes on the class with the same name (see Enum for an example). + attributes on the class with the same name. """ def __init__(self, fget=None, fset=None, fdel=None, doc=None): diff --git a/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst b/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst index a8b34125f882e8..2fe4880ded7a86 100644 --- a/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst +++ b/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst @@ -2,21 +2,3 @@ will be the only flags returned from listing and iterating over a Flag class or a Flag member. Multi-bit flags are considered aliases; they will be returned from lookups and operations that result in their value. - -For example: - - >>> class Color(Flag): - ... BLACK = 0 - ... RED = 1 - ... GREEN = 2 - ... BLUE = 4 - ... WHITE = 7 - ... - >>> list(Color) - [, , ] - >>> Color(7) - - >>> Color.RED | Color.GREEN | Color.BLUE - - >>> Color.RED & Color.GREEN - From 18bcbacaab7026eadf3550eb8fab9fa7439d3dc8 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Tue, 19 Jan 2021 23:08:56 -0800 Subject: [PATCH 14/30] add John Belmonte --- Misc/ACKS | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/ACKS b/Misc/ACKS index 136266965a869b..29ef9864f98271 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -141,6 +141,7 @@ Stefan Behnel Reimer Behrends Ben Bell Thomas Bellman +John Belmonte Alexander “Саша” Belopolsky Eli Bendersky Nikhil Benesch From f1c45843b7e36f3dbfa2e0f323279c45c8cf2dbf Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 20 Jan 2021 11:47:06 -0800 Subject: [PATCH 15/30] more bit-fiddling improvements --- Lib/enum.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 60c805fb68bdbb..a36c6f91a74f18 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -522,10 +522,6 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): if enum_class._boundary_ is not KEEP: # missed_bits = multi_bit_total & ~single_bit_total missed = list(_iter_bits_lsb(multi_bit_total & ~single_bit_total)) - # while missed_bits: - # i = 2 ** (missed_bits.bit_length() - 1) - # missed.append(i) - # missed_bits &= ~i if missed: raise TypeError( 'invalid Flag %r -- missing values: %s' @@ -1113,10 +1109,8 @@ def _iter_member_by_value_(cls, value): """ Extract all members from the value in definition (i.e. increasing value) order. """ - for val in _iter_bits_lsb(value): - member = cls._value2member_map_.get(val) - if member is not None: - yield member + for val in _iter_bits_lsb(value & cls._flag_mask_): + yield cls._value2member_map_.get(val) _iter_member_ = _iter_member_by_value_ From e3713aaceb7238d79496f3c21cc4051abe9d1c33 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 20 Jan 2021 11:58:30 -0800 Subject: [PATCH 16/30] use pop() instead of "del" new composite members aren't always added to _value2member_map_ -- this ensures the operation succeeds --- Lib/enum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/enum.py b/Lib/enum.py index a36c6f91a74f18..71d0a80c905092 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -533,7 +533,7 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): inverted = ~enum_class(0) if list(enum_class) != list(inverted): if '|' in inverted._name_: - del enum_class._value2member_map_[inverted._value_] + enum_class._value2member_map_.pop(inverted._value_, None) enum_class._iter_member_ = enum_class._iter_member_by_def_ # return enum_class From c4ec21165c88c7034bb019e06acbc332e8534250 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 20 Jan 2021 12:08:08 -0800 Subject: [PATCH 17/30] update DynamicClassAttribute __doc__ --- Lib/types.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/types.py b/Lib/types.py index b61f382b6b9273..c509b242d5d8f3 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -155,7 +155,12 @@ class DynamicClassAttribute: class's __getattr__ method; this is done by raising AttributeError. This allows one to have properties active on an instance, and have virtual - attributes on the class with the same name. + attributes on the class with the same name. (Enum used this between Python + versions 3.4 - 3.9 .) + + Subclass from this to use a different method of accessing virtual atributes + and still be treated properly by the inspect module. (Enum uses this since + Python 3.10 .) """ def __init__(self, fget=None, fset=None, fdel=None, doc=None): From 00b2bfe72474edac387108aef80b7b69b5efd40b Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 20 Jan 2021 14:41:56 -0800 Subject: [PATCH 18/30] remove formatting changes --- Lib/enum.py | 126 +++++++++++++++++----------------------------------- 1 file changed, 40 insertions(+), 86 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 71d0a80c905092..b391ef48e530d7 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -124,7 +124,6 @@ class auto: """ value = _auto_null - class property(DynamicClassAttribute): """ This is a descriptor, used to define attributes that act differently @@ -139,10 +138,7 @@ def __get__(self, instance, ownerclass=None): try: return ownerclass._member_map_[self.name] except KeyError: - raise AttributeError( - '%s: no attribute %r' - % (ownerclass.__name__, self.name) - ) + raise AttributeError('%r not found in %r' % (self.name, ownerclass.__name__)) else: if self.fget is None: raise AttributeError( @@ -154,19 +150,13 @@ def __get__(self, instance, ownerclass=None): def __set__(self, instance, value): if self.fset is None: - raise AttributeError( - "%s: cannot set attribute %r" - % (self.clsname, self.name) - ) + raise AttributeError("%s: cannot set attribute %r" % (self.clsname, self.name)) else: return self.fset(instance, value) def __delete__(self, instance): if self.fdel is None: - raise AttributeError( - "%s: cannot delete attribute %r" - % (self.clsname, self.name) - ) + raise AttributeError("%s: cannot delete attribute %r" % (self.clsname, self.name)) else: return self.fdel(instance) @@ -254,8 +244,8 @@ def __set_name__(self, enum_class, member_name): redirect = property() redirect.__set_name__(enum_class, member_name) if descriptor and need_override: - # previous enum.property found, but some other inherited - # attribute is in the way; copy fget, fset, fdel to this one + # previous enum.property found, but some other inherited attribute + # is in the way; copy fget, fset, fdel to this one redirect.fget = descriptor.fget redirect.fset = descriptor.fset redirect.fdel = descriptor.fdel @@ -304,17 +294,14 @@ def __setitem__(self, key, value): '_iter_member_', '_iter_member_by_value_', '_iter_member_by_def_', ): raise ValueError( - '_sunder_ names, such as %r, are reserved for future' - ' Enum use' + '_sunder_ names, such as %r, are reserved for future Enum use' % (key, ) ) if key == '_generate_next_value_': # check if members already defined as auto() if self._auto_called: raise TypeError( - "_generate_next_value_ must be defined before" - " members" - ) + "_generate_next_value_ must be defined before members") setattr(self, '_generate_next_value', value) elif key == '_ignore_': if isinstance(value, str): @@ -343,7 +330,10 @@ def __setitem__(self, key, value): if isinstance(value, auto): if value.value == _auto_null: value.value = self._generate_next_value( - key, 1, len(self._member_names), self._last_values[:], + key, + 1, + len(self._member_names), + self._last_values[:], ) self._auto_called = True value = value.value @@ -366,7 +356,6 @@ class EnumMeta(type): """ Metaclass for Enum """ - @classmethod def __prepare__(metacls, cls, bases, **kwds): # check that previous enum members do not exist @@ -425,7 +414,7 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): flag_mask |= value classdict[name] = _proto_member(value) # - # house-keeping structures + # house keeping structures classdict['_member_names_'] = [] classdict['_member_map_'] = {} classdict['_value2member_map_'] = {} @@ -464,9 +453,8 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): exc = None enum_class = super().__new__(metacls, cls, bases, classdict, **kwds) except RuntimeError as e: - # any exceptions raised by member.__new__ will get converted to - # a RuntimeError, so get that original exception back and raise - # it instead + # any exceptions raised by member.__new__ will get converted to a + # RuntimeError, so get that original exception back and raise it instead exc = e.__cause__ or e if exc is not None: raise exc @@ -544,12 +532,7 @@ def __bool__(self): """ return True - def __call__( - cls, value, names=None, - *, - module=None, qualname=None, type=None, - start=1, boundary=None, - ): + def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None): """ Either returns an existing member, or creates a new enum class. @@ -562,8 +545,7 @@ def __call__( `value` will be the name of the new class. `names` should be either a string of white-space/comma delimited names - (values will start at `start`), or an iterator/mapping of name, value - pairs. + (values will start at `start`), or an iterator/mapping of name, value pairs. `module` should be set to the module this class is being created in; if it is not set, an attempt to find that module will be made, but if @@ -671,12 +653,7 @@ def __setattr__(cls, name, value): raise AttributeError('Cannot reassign members.') super().__setattr__(name, value) - def _create_( - cls, class_name, names, - *, - module=None, qualname=None, type=None, - start=1, boundary=None, - ): + def _create_(cls, class_name, names, *, module=None, qualname=None, type=None, start=1, boundary=None): """ Convenience method to create a new Enum class. @@ -684,8 +661,7 @@ def _create_( * A string containing member names, separated either with spaces or commas. Values are incremented by 1 from `start`. - * An iterable of member names. Values are incremented by 1 from - `start`. + * An iterable of member names. Values are incremented by 1 from `start`. * An iterable of (member name, value) pairs. * A mapping of member name -> value pairs. """ @@ -697,20 +673,14 @@ def _create_( # special processing needed for names? if isinstance(names, str): names = names.replace(',', ' ').split() - if ( - isinstance(names, (tuple, list)) - and names - and isinstance(names[0], str) - ): + if isinstance(names, (tuple, list)) and names and isinstance(names[0], str)): original_names, names = names, [] last_values = [] for count, name in enumerate(original_names): - value = first_enum._generate_next_value_( - name, start, count, last_values[:] - ) + value = first_enum._generate_next_value_(name, start, count, last_values[:]) last_values.append(value) names.append((name, value)) - # + # Here, names is either an iterable of (name, value) or a mapping. for item in names: if isinstance(item, str): @@ -718,7 +688,7 @@ def _create_( else: member_name, member_value = item classdict[member_name] = member_value - # + # TODO: replace the frame hack if a blessed way to know the calling # module is ever developed if module is None: @@ -732,11 +702,8 @@ def _create_( classdict['__module__'] = module if qualname is not None: classdict['__qualname__'] = qualname - # - return metacls.__new__( - metacls, class_name, bases, classdict, - boundary=boundary, - ) + + return metacls.__new__(metacls, class_name, bases, classdict, boundary=boundary) def _convert_(cls, name, module, filter, source=None, boundary=None): """ @@ -791,7 +758,7 @@ def _get_mixins_(class_name, bases): """ if not bases: return object, Enum - # + def _find_data_type(bases): data_types = [] for chain in bases: @@ -811,15 +778,12 @@ def _find_data_type(bases): else: candidate = base if len(data_types) > 1: - raise TypeError( - '%r: too many data types: %r' - % (class_name, data_types) - ) + raise TypeError('%r: too many data types: %r' % (class_name, data_types)) elif data_types: return data_types[0] else: return None - # + # ensure final parent class is an Enum derivative, find any concrete # data type, and check that Enum has no members first_enum = bases[-1] @@ -844,10 +808,10 @@ def _find_new_(classdict, member_type, first_enum): # by the user; also check earlier enum classes in case a __new__ was # saved as __new_member__ __new__ = classdict.get('__new__', None) - # + # should __new__ be saved as __new_member__ later? save_new = __new__ is not None - # + if __new__ is None: # check all possibles for __new_member__ before falling back to # __new__ @@ -866,7 +830,7 @@ def _find_new_(classdict, member_type, first_enum): break else: __new__ = object.__new__ - # + # if a non-object.__new__ is used then whatever value/tuple was # assigned to the enum member name will be passed to __new__ and to the # new enum member's __init__ @@ -918,15 +882,11 @@ def __new__(cls, value): ): return result else: - ve_exc = ValueError( - "%r is not a valid %s" % (value, cls.__qualname__) - ) + ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__)) if result is None and exc is None: raise ve_exc elif exc is None: - exc = TypeError( - 'error in %s._missing_: returned %r instead of None' - ' or a valid member' + exc = TypeError('error in %s._missing_: returned %r instead of None or a valid member' % (cls.__name__, result) ) if not isinstance(exc, ValueError): @@ -980,7 +940,7 @@ def __format__(self, format_spec): # mixed-in Enums should use the mixed-in type's __format__, otherwise # we can get strange results with the Enum name showing up instead of # the value - # + # pure Enum branch, or branch with __str__ explicitly overridden str_overridden = type(self).__str__ not in (Enum.__str__, Flag.__str__) if self._member_type_ is object or str_overridden: @@ -1030,25 +990,19 @@ class StrEnum(str, Enum): def __new__(cls, *values): if len(values) > 3: - raise TypeError( - 'too many arguments for str(): %r' % (values, ) - ) + raise TypeError('too many arguments for str(): %r' % (values, )) if len(values) == 1: # it must be a string if not isinstance(values[0], str): raise TypeError('%r is not a string' % (values[0], )) - if len(values) >= 2: + if len(values) > 1: # check that encoding argument is a string if not isinstance(values[1], str): - raise TypeError( - 'encoding must be a string, not %r' % (values[1], ) - ) - if len(values) == 3: - # check that errors argument is a string - if not isinstance(values[2], str): - raise TypeError( - 'errors must be a string, not %r' % (values[2], ) - ) + raise TypeError('encoding must be a string, not %r' % (values[1], )) + if len(values) > 2: + # check that errors argument is a string + if not isinstance(values[2], str): + raise TypeError('errors must be a string, not %r' % (values[2], )) value = str(*values) member = str.__new__(cls, value) member._value_ = value From 9f432c3358cc0db3e09be584c9a97aaa705ac878 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 20 Jan 2021 14:43:16 -0800 Subject: [PATCH 19/30] remove extra parens --- Lib/enum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/enum.py b/Lib/enum.py index b391ef48e530d7..b9992485954527 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -673,7 +673,7 @@ def _create_(cls, class_name, names, *, module=None, qualname=None, type=None, s # special processing needed for names? if isinstance(names, str): names = names.replace(',', ' ').split() - if isinstance(names, (tuple, list)) and names and isinstance(names[0], str)): + if isinstance(names, (tuple, list)) and names and isinstance(names[0], str): original_names, names = names, [] last_values = [] for count, name in enumerate(original_names): From 15c060a2245c201f87371617e067768c296cc7b9 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 20 Jan 2021 14:52:15 -0800 Subject: [PATCH 20/30] remove formatting changes also disable doctests until formatting changes are added back --- Lib/enum.py | 20 +++++++------------- Lib/test/test_enum.py | 14 +++++++------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index b9992485954527..1ca9da85440d1e 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -138,19 +138,16 @@ def __get__(self, instance, ownerclass=None): try: return ownerclass._member_map_[self.name] except KeyError: - raise AttributeError('%r not found in %r' % (self.name, ownerclass.__name__)) + raise AttributeError('%r not found in %r' % (self.name, ownerclass.__name__)) else: if self.fget is None: - raise AttributeError( - '%s: no attribute %r' - % (ownerclass.__name__, self.name) - ) + raise AttributeError('%s: cannot read attribute %r' % (ownerclass.__name__, self.name)) else: return self.fget(instance) def __set__(self, instance, value): if self.fset is None: - raise AttributeError("%s: cannot set attribute %r" % (self.clsname, self.name)) + raise AttributeError("%s: cannot set attribute %r" % (self.clsname, self.name)) else: return self.fset(instance, value) @@ -581,10 +578,7 @@ def __delattr__(cls, attr): # nicer error message when someone tries to delete an attribute # (see issue19025). if attr in cls._member_map_: - raise AttributeError( - "%s: cannot delete Enum member %r." - % (cls.__name__, attr) - ) + raise AttributeError("%s: cannot delete Enum member %r." % (cls.__name__, attr)) super().__delattr__(attr) def __dir__(self): @@ -669,7 +663,7 @@ def _create_(cls, class_name, names, *, module=None, qualname=None, type=None, s bases = (cls, ) if type is None else (type, cls) _, first_enum = cls._get_mixins_(cls, bases) classdict = metacls.__prepare__(class_name, bases) - # + # special processing needed for names? if isinstance(names, str): names = names.replace(',', ' ').split() @@ -847,7 +841,6 @@ class Enum(metaclass=EnumMeta): Derive from this class to define new enumerations. """ - def __new__(cls, value): # all enum instances are actually created during class construction # without calling this method; this method is called by the metaclass' @@ -886,7 +879,8 @@ def __new__(cls, value): if result is None and exc is None: raise ve_exc elif exc is None: - exc = TypeError('error in %s._missing_: returned %r instead of None or a valid member' + exc = TypeError( + 'error in %s._missing_: returned %r instead of None or a valid member' % (cls.__name__, result) ) if not isinstance(exc, ValueError): diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index dd1866dbc58e1a..b3d589843ca558 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -15,13 +15,13 @@ from test.support import threading_helper from datetime import timedelta -def load_tests(loader, tests, ignore): - tests.addTests(doctest.DocTestSuite(enum)) - tests.addTests(doctest.DocFileSuite( - '../../Doc/library/enum.rst', - optionflags=doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE, - )) - return tests +# def load_tests(loader, tests, ignore): +# tests.addTests(doctest.DocTestSuite(enum)) +# tests.addTests(doctest.DocFileSuite( +# '../../Doc/library/enum.rst', +# optionflags=doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE, +# )) +# return tests # for pickle tests try: From 86d76698c1fcae2b6bc7ef876300710615995841 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 20 Jan 2021 14:55:51 -0800 Subject: [PATCH 21/30] remove formatting --- Lib/enum.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 1ca9da85440d1e..02dc293f72cf49 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -297,8 +297,7 @@ def __setitem__(self, key, value): if key == '_generate_next_value_': # check if members already defined as auto() if self._auto_called: - raise TypeError( - "_generate_next_value_ must be defined before members") + raise TypeError("_generate_next_value_ must be defined before members") setattr(self, '_generate_next_value', value) elif key == '_ignore_': if isinstance(value, str): From 55915df64a7c5e5e7051162de8bb14ec1570a678 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 21 Jan 2021 21:13:06 -0800 Subject: [PATCH 22/30] simplify determination of member iteration --- Lib/enum.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 02dc293f72cf49..9ca351ec44b9c4 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -514,10 +514,8 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): enum_class._flag_mask_ = single_bit_total # # set correct __iter__ - inverted = ~enum_class(0) - if list(enum_class) != list(inverted): - if '|' in inverted._name_: - enum_class._value2member_map_.pop(inverted._value_, None) + member_list = [m._value_ for m in enum_class] + if member_list != sorted(member_list): enum_class._iter_member_ = enum_class._iter_member_by_def_ # return enum_class From 95bf9c84aa79e4198e779290a96a1eba35dc2c68 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Mon, 25 Jan 2021 10:27:09 -0800 Subject: [PATCH 23/30] add note about next auto() value for Enum and Flag --- Doc/library/enum.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 933c03ce099923..558612366cf996 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -1175,6 +1175,14 @@ Supported ``_sunder_`` names :class:`auto` to get an appropriate value for an enum member; may be overridden + .. note:: + + For standard :class:`Enum` classes the next value chosen is the last value seen + incremented by one. + + For :class:`Flag`-type classes the next value chosen will be the next highest + power-of-two, regardless of the last value seen. + .. versionadded:: 3.6 ``_missing_``, ``_order_``, ``_generate_next_value_`` .. versionadded:: 3.7 ``_ignore_`` From 41ac1cecf1677e86e582183c114185e0b3cee484 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Mon, 25 Jan 2021 10:27:43 -0800 Subject: [PATCH 24/30] local name optimizations --- Lib/enum.py | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 9ca351ec44b9c4..b320440fe4dffe 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -498,11 +498,12 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): single_bit_total = 0 multi_bit_total = 0 for flag in enum_class._member_map_.values(): - if _is_single_bit(flag._value_): - single_bit_total |= flag._value_ + flag_value = flag._value_ + if _is_single_bit(flag_value): + single_bit_total |= flag_value else: # multi-bit flags are considered aliases - multi_bit_total |= flag._value_ + multi_bit_total |= flag_value if enum_class._boundary_ is not KEEP: # missed_bits = multi_bit_total & ~single_bit_total missed = list(_iter_bits_lsb(multi_bit_total & ~single_bit_total)) @@ -1064,10 +1065,10 @@ def _iter_member_by_def_(cls, value): """ Extract all members from the value in definition order. """ - members = list(cls._iter_member_by_value_(value)) - members.sort(key=lambda m: m._sort_order_) - for member in members: - yield member + yield from sorted( + cls._iter_member_by_value_(value), + key=lambda m: m._sort_order_, + ) @classmethod def _missing_(cls, value): @@ -1082,25 +1083,27 @@ def _missing_(cls, value): # - value must be in range (e.g. -16 <-> +15, i.e. ~15 <-> 15) # - value must not include any skipped flags (e.g. if bit 2 is not # defined, then 0d10 is invalid) + flag_mask = cls._flag_mask_ + all_bits = cls._all_bits_ neg_value = None if ( - not ~cls._all_bits_ <= value <= cls._all_bits_ - or value & (cls._all_bits_ ^ cls._flag_mask_) + not ~all_bits <= value <= all_bits + or value & (all_bits ^ flag_mask) ): if cls._boundary_ is STRICT: - max_bits = max(value.bit_length(), cls._flag_mask_.bit_length()) + max_bits = max(value.bit_length(), flag_mask.bit_length()) raise ValueError( "%s: invalid value: %r\n given %s\n allowed %s" % ( - cls.__name__, value, bin(value, max_bits), bin(cls._flag_mask_, max_bits), + cls.__name__, value, bin(value, max_bits), bin(flag_mask, max_bits), )) elif cls._boundary_ is CONFORM: - value = value & cls._flag_mask_ + value = value & flag_mask elif cls._boundary_ is EJECT: return value elif cls._boundary_ is KEEP: if value < 0: value = ( - max(cls._all_bits_+1, 2**(value.bit_length())) + max(all_bits+1, 2**(value.bit_length())) + value ) else: @@ -1109,10 +1112,10 @@ def _missing_(cls, value): ) if value < 0: neg_value = value - value = cls._all_bits_ + 1 + value + value = all_bits + 1 + value # get members and unknown - unknown = value & ~cls._flag_mask_ - members = list(cls._iter_member_(value)) + unknown = value & ~flag_mask + member_value = value & flag_mask if unknown and cls._boundary_ is not KEEP: raise ValueError( '%s(%r) --> unknown values %r [%s]' @@ -1127,8 +1130,13 @@ def _missing_(cls, value): pseudo_member = (__new__ or cls._member_type_.__new__)(cls, value) if not hasattr(pseudo_member, 'value'): pseudo_member._value_ = value - if members: - pseudo_member._name_ = '|'.join([m._name_ for m in members]) + if member_value: + # pseudo_member._name_ = '|'.join([m._name_ for m in members]) + # if unknown: + # pseudo_member._name_ += '|0x%x' % unknown + pseudo_member._name_ = '|'.join([ + m._name_ for m in cls._iter_member_(member_value) + ]) if unknown: pseudo_member._name_ += '|0x%x' % unknown else: From 3ea814e21c2a8f2e0d010faaf4615ee67563246a Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Mon, 25 Jan 2021 10:28:48 -0800 Subject: [PATCH 25/30] remove commented-out code --- Lib/enum.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index b320440fe4dffe..bd44137ed21be0 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1131,9 +1131,6 @@ def _missing_(cls, value): if not hasattr(pseudo_member, 'value'): pseudo_member._value_ = value if member_value: - # pseudo_member._name_ = '|'.join([m._name_ for m in members]) - # if unknown: - # pseudo_member._name_ += '|0x%x' % unknown pseudo_member._name_ = '|'.join([ m._name_ for m in cls._iter_member_(member_value) ]) From 4983558af7b1d14d12eeea038b2f63811bc4c354 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Mon, 25 Jan 2021 10:30:24 -0800 Subject: [PATCH 26/30] add test for next auto() and _order_ auto() for flags returns the first power of two not used _order_ has any names that are aliases removed before checking against _member_names_ --- Lib/test/test_enum.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index b3d589843ca558..692476b3aa3096 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -2578,6 +2578,24 @@ def test_member_length(self): self.assertEqual(self.Color.__len__(self.Color.PURPLE), 2) self.assertEqual(self.Color.__len__(self.Color.BLANCO), 3) + def test_number_reset_and_order_cleanup(self): + class Confused(Flag): + _settings_ = AutoValue + _order_ = 'ONE TWO FOUR DOS EIGHT SIXTEEN' + ONE = auto() + TWO = auto() + FOUR = auto() + DOS = 2 + EIGHT = auto() + SIXTEEN = auto() + self.assertEqual( + list(Confused), + [Confused.ONE, Confused.TWO, Confused.FOUR, Confused.EIGHT, Confused.SIXTEEN]) + self.assertIs(Confused.TWO, Confused.DOS) + self.assertEqual(Confused.DOS._value_, 2) + self.assertEqual(Confused.EIGHT._value_, 8) + self.assertEqual(Confused.SIXTEEN._value_, 16) + def test_aliases(self): Color = self.Color self.assertEqual(Color(1).name, 'RED') From 651da184f1ae6ff5c067a658130473fb13e09422 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Mon, 25 Jan 2021 11:07:42 -0800 Subject: [PATCH 27/30] raise TypeError if _value_ not added in custom new also fix _order_ tests --- Lib/enum.py | 42 ++++++++++++++++++++++++++++++++++++++---- Lib/test/test_enum.py | 26 ++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index bd44137ed21be0..3f3e9c4576c106 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -194,7 +194,12 @@ def __set_name__(self, enum_class, member_name): if enum_class._member_type_ is object: enum_member._value_ = value else: - enum_member._value_ = enum_class._member_type_(*args) + try: + enum_member._value_ = enum_class._member_type_(*args) + except Exception as exc: + raise TypeError( + '_value_ not set in __new__, unable to create it' + ) from None value = enum_member._value_ enum_member._name_ = member_name enum_member.__objclass__ = enum_class @@ -478,11 +483,17 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): enum_class.__new__ = Enum.__new__ # # py3 support for definition order (helps keep py2/py3 code in sync) + # + # _order_ checking is spread out into three/four steps + # - if enum_class is a Flag: + # - remove any non-single-bit flags from _order_ + # - remove any aliases from _order_ + # - check that _order_ and _member_names_ match + # + # step 1: ensure we have a list if _order_ is not None: if isinstance(_order_, str): _order_ = _order_.replace(',', ' ').split() - if _order_ != enum_class._member_names_: - raise TypeError('member order does not match _order_') # # remove Flag structures if final class is not a Flag if ( @@ -505,7 +516,6 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): # multi-bit flags are considered aliases multi_bit_total |= flag_value if enum_class._boundary_ is not KEEP: - # missed_bits = multi_bit_total & ~single_bit_total missed = list(_iter_bits_lsb(multi_bit_total & ~single_bit_total)) if missed: raise TypeError( @@ -518,6 +528,30 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): member_list = [m._value_ for m in enum_class] if member_list != sorted(member_list): enum_class._iter_member_ = enum_class._iter_member_by_def_ + if _order_: + # _order_ step 2: remove any items from _order_ that are not single-bit + _order_ = [ + o + for o in _order_ + if o not in enum_class._member_map_ or _is_single_bit(enum_class[o]._value_) + ] + # + if _order_: + # _order_ step 3: remove aliases from _order_ + _order_ = [ + o + for o in _order_ + if ( + o not in enum_class._member_map_ + or + (o in enum_class._member_map_ and o in enum_class._member_names_) + )] + # _order_ step 4: verify that _order_ and _member_names_ match + if _order_ != enum_class._member_names_: + raise TypeError( + 'member order does not match _order_:\n%r\n%r' + % (enum_class._member_names_, _order_) + ) # return enum_class diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 692476b3aa3096..2e96c8d56d9735 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -2135,7 +2135,30 @@ class ThirdFailedStrEnum(StrEnum): one = '1' two = b'2', 'ascii', 9 - + def test_missing_value_error(self): + with self.assertRaisesRegex(TypeError, "_value_ not set in __new__"): + class Combined(str, Enum): + # + def __new__(cls, value, sequence): + enum = str.__new__(cls, value) + if '(' in value: + fis_name, segment = value.split('(', 1) + segment = segment.strip(' )') + else: + fis_name = value + segment = None + enum.fis_name = fis_name + enum.segment = segment + enum.sequence = sequence + return enum + # + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self._name_) + # + key_type = 'An$(1,2)', 0 + company_id = 'An$(3,2)', 1 + code = 'An$(5,1)', 2 + description = 'Bn$', 3 @unittest.skipUnless( sys.version_info[:2] == (3, 9), @@ -2580,7 +2603,6 @@ def test_member_length(self): def test_number_reset_and_order_cleanup(self): class Confused(Flag): - _settings_ = AutoValue _order_ = 'ONE TWO FOUR DOS EIGHT SIXTEEN' ONE = auto() TWO = auto() From 6e99d485737c71bef77abb8979c607dc6f38024c Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Mon, 25 Jan 2021 11:23:58 -0800 Subject: [PATCH 28/30] enable doc tests, update formatting --- Doc/library/enum.rst | 4 +++- Lib/enum.py | 35 +++++++++++++++++++++-------------- Lib/test/test_enum.py | 14 +++++++------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 558612366cf996..39940db61b6703 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -1198,7 +1198,9 @@ and raise an error if the two do not match:: ... Traceback (most recent call last): ... - TypeError: member order does not match _order_ + TypeError: member order does not match _order_: + ['RED', 'BLUE', 'GREEN'] + ['RED', 'GREEN', 'BLUE'] .. note:: diff --git a/Lib/enum.py b/Lib/enum.py index 3f3e9c4576c106..d4b11521ab27f3 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -138,22 +138,30 @@ def __get__(self, instance, ownerclass=None): try: return ownerclass._member_map_[self.name] except KeyError: - raise AttributeError('%r not found in %r' % (self.name, ownerclass.__name__)) + raise AttributeError( + '%s: no attribute %r' % (ownerclass.__name__, self.name) + ) else: if self.fget is None: - raise AttributeError('%s: cannot read attribute %r' % (ownerclass.__name__, self.name)) + raise AttributeError( + '%s: no attribute %r' % (ownerclass.__name__, self.name) + ) else: return self.fget(instance) def __set__(self, instance, value): if self.fset is None: - raise AttributeError("%s: cannot set attribute %r" % (self.clsname, self.name)) + raise AttributeError( + "%s: cannot set attribute %r" % (self.clsname, self.name) + ) else: return self.fset(instance, value) def __delete__(self, instance): if self.fdel is None: - raise AttributeError("%s: cannot delete attribute %r" % (self.clsname, self.name)) + raise AttributeError( + "%s: cannot delete attribute %r" % (self.clsname, self.name) + ) else: return self.fdel(instance) @@ -331,10 +339,7 @@ def __setitem__(self, key, value): if isinstance(value, auto): if value.value == _auto_null: value.value = self._generate_next_value( - key, - 1, - len(self._member_names), - self._last_values[:], + key, 1, len(self._member_names), self._last_values[:], ) self._auto_called = True value = value.value @@ -357,6 +362,7 @@ class EnumMeta(type): """ Metaclass for Enum """ + @classmethod def __prepare__(metacls, cls, bases, **kwds): # check that previous enum members do not exist @@ -415,7 +421,7 @@ def __new__(metacls, cls, bases, classdict, boundary=None, **kwds): flag_mask |= value classdict[name] = _proto_member(value) # - # house keeping structures + # house-keeping structures classdict['_member_names_'] = [] classdict['_member_map_'] = {} classdict['_value2member_map_'] = {} @@ -873,6 +879,7 @@ class Enum(metaclass=EnumMeta): Derive from this class to define new enumerations. """ + def __new__(cls, value): # all enum instances are actually created during class construction # without calling this method; this method is called by the metaclass' @@ -1021,14 +1028,14 @@ def __new__(cls, *values): # it must be a string if not isinstance(values[0], str): raise TypeError('%r is not a string' % (values[0], )) - if len(values) > 1: + if len(values) >= 2: # check that encoding argument is a string if not isinstance(values[1], str): raise TypeError('encoding must be a string, not %r' % (values[1], )) - if len(values) > 2: - # check that errors argument is a string - if not isinstance(values[2], str): - raise TypeError('errors must be a string, not %r' % (values[2], )) + if len(values) == 3: + # check that errors argument is a string + if not isinstance(values[2], str): + raise TypeError('errors must be a string, not %r' % (values[2])) value = str(*values) member = str.__new__(cls, value) member._value_ = value diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 2e96c8d56d9735..daca2e3c83f271 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -15,13 +15,13 @@ from test.support import threading_helper from datetime import timedelta -# def load_tests(loader, tests, ignore): -# tests.addTests(doctest.DocTestSuite(enum)) -# tests.addTests(doctest.DocFileSuite( -# '../../Doc/library/enum.rst', -# optionflags=doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE, -# )) -# return tests +def load_tests(loader, tests, ignore): + tests.addTests(doctest.DocTestSuite(enum)) + tests.addTests(doctest.DocFileSuite( + '../../Doc/library/enum.rst', + optionflags=doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE, + )) + return tests # for pickle tests try: From b52c5a2d8aef05914ead91e2f0404b54c6187f5b Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Mon, 25 Jan 2021 11:49:12 -0800 Subject: [PATCH 29/30] fix note --- Doc/library/enum.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 39940db61b6703..b27c5527c7f7c4 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -1175,7 +1175,7 @@ Supported ``_sunder_`` names :class:`auto` to get an appropriate value for an enum member; may be overridden - .. note:: +.. note:: For standard :class:`Enum` classes the next value chosen is the last value seen incremented by one. From 8d7b2724fbec9eaa670dcfe3f7a942f31494cc5c Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Mon, 25 Jan 2021 13:24:00 -0800 Subject: [PATCH 30/30] Update 2021-01-14-15-07-16.bpo-38250.1fvhOk.rst --- .../NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst b/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst index 2fe4880ded7a86..e5a72468370fba 100644 --- a/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst +++ b/Misc/NEWS.d/next/Library/2021-01-14-15-07-16.bpo-38250.1fvhOk.rst @@ -2,3 +2,4 @@ will be the only flags returned from listing and iterating over a Flag class or a Flag member. Multi-bit flags are considered aliases; they will be returned from lookups and operations that result in their value. +Iteration for both Flag and Flag members is in definition order.