diff options
Diffstat (limited to 'Lib/enum.py')
-rw-r--r-- | Lib/enum.py | 502 |
1 files changed, 337 insertions, 165 deletions
diff --git a/Lib/enum.py b/Lib/enum.py index 8ca3854..d4b1152 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1,6 +1,6 @@ import sys from types import MappingProxyType, DynamicClassAttribute -from builtins import property as _bltin_property +from builtins import property as _bltin_property, bin as _bltin_bin __all__ = [ @@ -8,9 +8,15 @@ __all__ = [ 'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'auto', 'unique', 'property', + 'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP', ] +# 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 +62,15 @@ def _is_private(cls_name, name): else: return False +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 + def _make_class_unpicklable(obj): """ Make the given obj un-picklable. @@ -71,6 +86,37 @@ def _make_class_unpicklable(obj): setattr(obj, '__reduce_ex__', _break_on_call_reduce) setattr(obj, '__module__', '<unknown>') +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: """ @@ -92,22 +138,30 @@ class property(DynamicClassAttribute): 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) @@ -148,11 +202,17 @@ class _proto_member: 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 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(): @@ -160,8 +220,21 @@ class _proto_member: 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 +266,7 @@ class _proto_member: # 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 @@ -228,6 +301,7 @@ class _EnumDict(dict): 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 Enum use' @@ -265,10 +339,7 @@ class _EnumDict(dict): 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 @@ -287,15 +358,11 @@ class _EnumDict(dict): 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 """ + @classmethod def __prepare__(metacls, cls, bases, **kwds): # check that previous enum members do not exist @@ -311,7 +378,7 @@ class EnumMeta(type): ) 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 +413,29 @@ class EnumMeta(type): 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 + 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 # fail. Rather than have the error show up later and possibly far @@ -408,11 +489,75 @@ class EnumMeta(type): 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() + # + # 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) + ): + 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 + multi_bit_total = 0 + for flag in enum_class._member_map_.values(): + 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 + if enum_class._boundary_ is not KEEP: + missed = list(_iter_bits_lsb(multi_bit_total & ~single_bit_total)) + if missed: + raise TypeError( + 'invalid Flag %r -- missing values: %s' + % (cls, ', '.join((str(i) for i in missed))) + ) + enum_class._flag_mask_ = single_bit_total + # + # set correct __iter__ + 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_') + raise TypeError( + 'member order does not match _order_:\n%r\n%r' + % (enum_class._member_names_, _order_) + ) # return enum_class @@ -422,7 +567,7 @@ class EnumMeta(type): """ 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. @@ -457,6 +602,7 @@ class EnumMeta(type): qualname=qualname, type=type, start=start, + boundary=boundary, ) def __contains__(cls, member): @@ -539,7 +685,7 @@ class EnumMeta(type): 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. @@ -589,9 +735,9 @@ class EnumMeta(type): 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 """ @@ -618,7 +764,7 @@ class EnumMeta(type): 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 @@ -733,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' @@ -761,6 +908,11 @@ class Enum(metaclass=EnumMeta): 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 +922,8 @@ class Enum(metaclass=EnumMeta): '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): @@ -875,14 +1028,14 @@ class StrEnum(str, Enum): # 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 @@ -900,7 +1053,22 @@ class StrEnum(str, Enum): 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] + "keep" -> keep flag status and all bits + """ + STRICT = auto() + CONFORM = auto() + EJECT = auto() + KEEP = auto() +STRICT, CONFORM, EJECT, KEEP = FlagBoundary + + +class Flag(Enum, boundary=STRICT): """ Support for flags """ @@ -916,45 +1084,108 @@ class Flag(Enum): """ 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 - def _missing_(cls, value): + def _iter_member_by_value_(cls, value): """ - Returns member (possibly creating it) if one can be found for value. + Extract all members from the value in definition (i.e. increasing value) order. """ - 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 + for val in _iter_bits_lsb(value & cls._flag_mask_): + yield cls._value2member_map_.get(val) + + _iter_member_ = _iter_member_by_value_ @classmethod - def _create_pseudo_member_(cls, value): + def _iter_member_by_def_(cls, value): + """ + Extract all members from the value in definition order. + """ + yield from sorted( + cls._iter_member_by_value_(value), + key=lambda m: m._sort_order_, + ) + + @classmethod + def _missing_(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) + flag_mask = cls._flag_mask_ + all_bits = cls._all_bits_ + neg_value = None + if ( + not ~all_bits <= value <= all_bits + or value & (all_bits ^ flag_mask) + ): + if cls._boundary_ is STRICT: + 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(flag_mask, max_bits), + )) + elif cls._boundary_ is CONFORM: + value = value & flag_mask + elif cls._boundary_ is EJECT: + return value + elif cls._boundary_ is KEEP: + if value < 0: + value = ( + max(all_bits+1, 2**(value.bit_length())) + + value + ) + else: + raise ValueError( + 'unknown flag boundary: %r' % (cls._boundary_, ) + ) + if value < 0: + neg_value = value + value = all_bits + 1 + value + # get members and unknown + 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]' + % (cls.__name__, value, unknown, bin(unknown)) + ) + # 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 + if member_value: + pseudo_member._name_ = '|'.join([ + m._name_ for m in cls._iter_member_(member_value) + ]) + 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, 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): @@ -965,38 +1196,33 @@ class Flag(Enum): 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. + Returns flags in definition order. """ - members, extra_flags = _decompose(self.__class__, self.value) - return (m for m in members if m._value_ != 0) + yield from self._iter_member_(self._value_) + + def __len__(self): + return self._value_.bit_count() 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 +1243,56 @@ class Flag(Enum): 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) + if self._inverted_ is None: + if self._boundary_ is KEEP: + # use all bits + self._inverted_ = self.__class__(~self._value_) + else: + # calculate flags not in this member + self._inverted_ = self.__class__(self._flag_mask_ ^ self._value_) + self._inverted_._inverted_ = self + return self._inverted_ -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): """ @@ -1119,31 +1315,7 @@ def unique(enumeration): (enumeration, alias_details)) 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 - negative = value < 0 - members = [] - for member in flag: - member_value = member.value - if member_value and member_value & 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]) - 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 +def _power_of_two(value): + if value < 1: + return False + return value == 2 ** _high_bit(value) |