diff --git a/Lib/enum.py b/Lib/enum.py index 0be1b60cd6d8aa..9f63c5820116a1 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -36,7 +36,7 @@ def _is_sunder(name): def _make_class_unpicklable(cls): """Make the given class un-picklable.""" def _break_on_call_reduce(self, proto): - raise TypeError('%r cannot be pickled' % self) + raise TypeError(f'{self!r} cannot be pickled') cls.__reduce_ex__ = _break_on_call_reduce cls.__module__ = '' @@ -51,14 +51,13 @@ class auto: class _EnumDict(dict): """Track enum member order and ensure member names are not reused. - EnumMeta will use the names found in self._member_names as the + EnumMeta will use the names found in self.members as the enumeration member names. """ def __init__(self): super().__init__() - self._member_names = [] - self._last_values = [] + self.members = {} self._ignore = [] def __setitem__(self, key, value): @@ -71,10 +70,11 @@ def __setitem__(self, key, value): """ if _is_sunder(key): - if key not in ( + if (key in {'_name_', '_value_'} and Enum is not None) or key not in { '_order_', '_create_pseudo_member_', '_generate_next_value_', '_missing_', '_ignore_', - ): + '_name_', '_value_', + }: raise ValueError('_names_ are reserved for future Enum use') if key == '_generate_next_value_': setattr(self, '_generate_next_value', value) @@ -84,29 +84,46 @@ def __setitem__(self, key, value): else: value = list(value) self._ignore = value - already = set(value) & set(self._member_names) + already = set(value) & self.members.keys() if already: - raise ValueError('_ignore_ cannot specify already set names: %r' % (already, )) + raise ValueError(f'_ignore_ cannot specify already set names: {already!r}') elif _is_dunder(key): if key == '__order__': key = '_order_' - elif key in self._member_names: + elif key in self.members: # descriptor overwriting an enum? - raise TypeError('Attempted to reuse key: %r' % key) + raise TypeError(f'Attempted to reuse key: {key!r}') elif key in self._ignore: pass elif not _is_descriptor(value): if key in self: # enum overwriting a descriptor? - raise TypeError('%r already defined as: %r' % (key, self[key])) + raise TypeError(f'{key!r} already defined as: {self[key]!r}') if isinstance(value, auto): if value.value == _auto_null: - value.value = self._generate_next_value(key, 1, len(self._member_names), self._last_values[:]) + value.value = self._generate_next_value(key, 1, len(self.members), list(self.members.values())) value = value.value - self._member_names.append(key) - self._last_values.append(value) + self.members[key] = value super().__setitem__(key, value) + @property + def _member_names(self): + import warnings + warnings.warn( + '_member_names is deprecated and will be removed in 3.10, use ' + '_members instead.', DeprecationWarning, stacklevel=2 + ) + return list(self.members) + + @property + def _last_values(self): + import warnings + warnings.warn( + '_last_values is deprecated and will be removed in 3.10, use ' + '_members instead.', DeprecationWarning, stacklevel=2 + ) + return list(self.members.values()) + # 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. @@ -143,38 +160,42 @@ def __new__(metacls, cls, bases, classdict): # save enum items into separate mapping so they don't get baked into # the new class - enum_members = {k: classdict[k] for k in classdict._member_names} - for name in classdict._member_names: + enum_members = classdict.members + for name in enum_members: del classdict[name] # adjust the sunders _order_ = classdict.pop('_order_', None) # check for illegal enum names (any others?) - invalid_names = set(enum_members) & {'mro', ''} + invalid_names = enum_members.keys() & {'mro', ''} if invalid_names: - raise ValueError('Invalid enum member name: {0}'.format( - ','.join(invalid_names))) + raise ValueError(f"Invalid enum member name: {','.join(invalid_names)}") # create a default docstring if one has not been provided if '__doc__' not in classdict: classdict['__doc__'] = 'An enumeration.' + # convert _EnumDict() to dict() as it not used anymore and builtin dict() is faster + classdict = {**classdict} # create our new Enum type enum_class = super().__new__(metacls, cls, bases, classdict) - enum_class._member_names_ = [] # names in definition order enum_class._member_map_ = {} # name->value map + enum_class._unique_member_map_ = {} # name->unique value map enum_class._member_type_ = member_type - # save DynamicClassAttribute attributes from super classes so we know - # if we can take the shortcut of storing members in the class dict - dynamic_attributes = {k for c in enum_class.mro() + dynamic_attributes = {k: v for c in enum_class.mro() for k, v in c.__dict__.items() if isinstance(v, DynamicClassAttribute)} # Reverse value->name map for hashable values. enum_class._value2member_map_ = {} + # used to speedup __str__, __repr__ and __invert__ calls when applicable + enum_class._repr_ = None + enum_class._str_ = None + enum_class._invert_ = 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 @@ -187,8 +208,8 @@ def __new__(metacls, cls, bases, classdict): # pickle over __reduce__, and it handles all pickle protocols. if '__reduce_ex__' not in classdict: if member_type is not object: - methods = ('__getnewargs_ex__', '__getnewargs__', - '__reduce_ex__', '__reduce__') + methods = {'__getnewargs_ex__', '__getnewargs__', + '__reduce_ex__', '__reduce__'} if not any(m in member_type.__dict__ for m in methods): _make_class_unpicklable(enum_class) @@ -196,8 +217,7 @@ def __new__(metacls, cls, bases, classdict): # we instantiate first instead of checking for duplicates first in case # a custom __new__ is doing something funky with the values -- such as # auto-numbering ;) - for member_name in classdict._member_names: - value = enum_members[member_name] + for member_name, value in enum_members.items(): if not isinstance(value, tuple): args = (value, ) else: @@ -206,32 +226,40 @@ def __new__(metacls, cls, bases, classdict): args = (args, ) # wrap it one more time if not use_args: enum_member = __new__(enum_class) - if not hasattr(enum_member, '_value_'): + if not hasattr(enum_member, 'value'): enum_member._value_ = value else: enum_member = __new__(enum_class, *args) - if not hasattr(enum_member, '_value_'): + if not hasattr(enum_member, 'value'): if member_type is object: enum_member._value_ = value else: enum_member._value_ = member_type(*args) - value = enum_member._value_ + + value = enum_member.value enum_member._name_ = member_name + # setting protected attributes enum_member.__objclass__ = enum_class enum_member.__init__(*args) # 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(): - if canonical_member._value_ == enum_member._value_: + if canonical_member.value == enum_member.value: enum_member = canonical_member break else: # Aliases don't appear in member names (only in __members__). - enum_class._member_names_.append(member_name) - # performance boost for any member that would not shadow - # a DynamicClassAttribute - if member_name not in dynamic_attributes: + enum_class._unique_member_map_[member_name] = enum_member + + dynamic_attr = dynamic_attributes.get(member_name) + if dynamic_attr is not None: + # Setting attrs respectively to dynamic attribute so access member_name + # through a class will be routed to enum_member + # setattr(enum_class, dynamic_attr.class_attr_name, enum_member) + dynamic_attr.set_class_attr(enum_class, enum_member) + else: setattr(enum_class, member_name, enum_member) + # now add to _member_map_ enum_class._member_map_[member_name] = enum_member try: @@ -242,9 +270,18 @@ def __new__(metacls, cls, bases, classdict): except TypeError: pass + # after all members created, cache result of this + # methods for immutable values + for member in enum_class._value2member_map_.copy().values(): + for static_attr in ('__repr__', '__str__', '__invert__'): + method = getattr(member, static_attr, None) + if method is None: + continue + setattr(member, static_attr[1:-1], method()) + # double check that repr and friends are not the mixin's or various # things break (such as pickle) - for name in ('__repr__', '__str__', '__format__', '__reduce_ex__'): + for name in {'__repr__', '__str__', '__format__', '__reduce_ex__'}: class_method = getattr(enum_class, name) obj_method = getattr(member_type, name, None) enum_method = getattr(first_enum, name, None) @@ -264,7 +301,7 @@ def __new__(metacls, cls, bases, classdict): if _order_ is not None: if isinstance(_order_, str): _order_ = _order_.replace(',', ' ').split() - if _order_ != enum_class._member_names_: + if _order_ != list(enum_class._unique_member_map_): raise TypeError('member order does not match _order_') return enum_class @@ -307,47 +344,30 @@ def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, s def __contains__(cls, member): if not isinstance(member, Enum): - raise TypeError( - "unsupported operand type(s) for 'in': '%s' and '%s'" % ( - type(member).__qualname__, cls.__class__.__qualname__)) - return isinstance(member, cls) and member._name_ in cls._member_map_ + raise TypeError(f"unsupported operand type(s) for 'in': " + f"{type(member).__qualname__!r} and {cls.__class__.__qualname__!r}") + return isinstance(member, cls) and member.name in cls._member_map_ 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." % cls.__name__) + raise AttributeError(f'{cls.__name__}: cannot delete Enum member.') super().__delattr__(attr) def __dir__(self): - return (['__class__', '__doc__', '__members__', '__module__'] + - self._member_names_) - - def __getattr__(cls, name): - """Return the enum member matching `name` - - We use __getattr__ instead of descriptors or inserting into the enum - class' __dict__ in order to support `name` and `value` being both - properties for enum members (which live in the class' __dict__) and - enum members themselves. - - """ - if _is_dunder(name): - raise AttributeError(name) - try: - return cls._member_map_[name] - except KeyError: - raise AttributeError(name) from None + attrs = ['__class__', '__doc__', '__members__', '__module__'] + attrs.extend(self._unique_member_map_) + return attrs def __getitem__(cls, name): return cls._member_map_[name] def __iter__(cls): - return (cls._member_map_[name] for name in cls._member_names_) + return iter(cls._unique_member_map_.values()) def __len__(cls): - return len(cls._member_names_) + return len(cls._unique_member_map_) @property def __members__(cls): @@ -360,10 +380,10 @@ def __members__(cls): return MappingProxyType(cls._member_map_) def __repr__(cls): - return "" % cls.__name__ + return f'' def __reversed__(cls): - return (cls._member_map_[name] for name in reversed(cls._member_names_)) + return reversed(cls._unique_member_map_.values()) def __setattr__(cls, name, value): """Block attempts to reassign Enum members. @@ -373,8 +393,7 @@ def __setattr__(cls, name, value): resulting in an inconsistent Enumeration. """ - member_map = cls.__dict__.get('_member_map_', {}) - if name in member_map: + if name in cls.__dict__.get('_member_map_', {}): raise AttributeError('Cannot reassign members.') super().__setattr__(name, value) @@ -464,6 +483,15 @@ def _convert_(cls, name, module, filter, source=None): module_globals[name] = cls return cls + @property + def _member_names_(cls): + import warnings + warnings.warn( + '_member_names_ is deprecated and will be removed in 3.10, use ' + '_unique_members_map_ instead.', DeprecationWarning, stacklevel=2 + ) + return list(cls._unique_member_map_) + @staticmethod def _get_mixins_(bases): """Returns the type for creating enum members, and the first inherited @@ -492,7 +520,7 @@ def _find_data_type(bases): raise TypeError("new enumerations should be created as " "`EnumName([mixin_type, ...] [data_type,] enum_type)`") member_type = _find_data_type(bases) or object - if first_enum._member_names_: + if first_enum._unique_member_map_: raise TypeError("Cannot extend enumerations") return member_type, first_enum @@ -516,15 +544,16 @@ def _find_new_(classdict, member_type, first_enum): if __new__ is None: # check all possibles for __new_member__ before falling back to # __new__ + ignore_targets = { + None, + None.__new__, + object.__new__, + Enum.__new__, + } for method in ('__new_member__', '__new__'): for possible in (member_type, first_enum): target = getattr(possible, method, None) - if target not in { - None, - None.__new__, - object.__new__, - Enum.__new__, - }: + if target not in ignore_targets: __new__ = target break if __new__ is not None: @@ -552,7 +581,9 @@ def __new__(cls, value): # all enum instances are actually created during class construction # without calling this method; this method is called by the metaclass' # __call__ (i.e. Color(3) ), and by pickle - if type(value) is cls: + + # using .__class__ instead of type() as it 2x faster + if value.__class__ is cls: # For lookups like Color(Color.RED) return value # by-value search for a matching enum member @@ -564,30 +595,67 @@ def __new__(cls, value): pass except TypeError: # not there, now do long search -- O(n) behavior - for member in cls._member_map_.values(): - if member._value_ == value: + for member in cls._unique_member_map_.values(): + if member.value == value: return member # still not found -- try _missing_ hook + + # TODO: Maybe remove try/except block and setting __context__ in this case? try: - exc = None result = cls._missing_(value) except Exception as e: - exc = e - result = None + # Huge boost for standard enum + if cls._missing_ is Enum._missing_: + raise + else: + e.__context__ = ValueError(f'{value!r} is not a valid {cls.__qualname__}') + raise + if isinstance(result, cls): return result + + ve_exc = ValueError(f'{value!r} is not a valid {cls.__qualname__}') + if result is None: + raise ve_exc else: - 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' - % (cls.__name__, result) - ) + exc = TypeError( + f'error in {cls.__name__}._missing_: returned {result!r} instead of None or a valid member' + ) exc.__context__ = ve_exc raise exc + @property + def _name_(self): + import warnings + warnings.warn( + 'getting name through _name_ attr is deprecated and will be removed in 3.10 ' + 'use name attr instead.', DeprecationWarning, stacklevel=2 + ) + return self.name + + @property + def _value_(self): + import warnings + warnings.warn( + 'getting value through _value_ attr is deprecated and will be removed in 3.10 ' + 'use value attr instead.', DeprecationWarning, stacklevel=2 + ) + return self.value + + @_value_.setter + def _value_(self, value): + self._repr_ = self._str_ = None + if '_invert_' in self.__dict__: + self._invert_ = None + object.__setattr__(self, 'value', value) + + @_name_.setter + def _name_(self, name): + self._repr_ = self._str_ = None + if '_invert_' in self.__dict__: + self._invert_ = None + object.__setattr__(self, 'name', name) + def _generate_next_value_(name, start, count, last_values): for last_value in reversed(last_values): try: @@ -599,14 +667,13 @@ def _generate_next_value_(name, start, count, last_values): @classmethod def _missing_(cls, value): - raise ValueError("%r is not a valid %s" % (value, cls.__qualname__)) + raise ValueError(f'{value!r} is not a valid {cls.__qualname__}') def __repr__(self): - return "<%s.%s: %r>" % ( - self.__class__.__name__, self._name_, self._value_) + return f'<{self.__class__.__name__}.{self.name}: {self.value!r}>' def __str__(self): - return "%s.%s" % (self.__class__.__name__, self._name_) + return f'{self.__class__.__name__}.{self.name}' def __dir__(self): added_behavior = [ @@ -615,7 +682,7 @@ def __dir__(self): for m in cls.__dict__ if m[0] != '_' and m not in self._member_map_ ] - return (['__class__', '__doc__', '__module__'] + added_behavior) + return (['__class__', '__doc__', '__module__', 'name', 'value'] + added_behavior) def __format__(self, format_spec): # mixed-in Enums should use the mixed-in type's __format__, otherwise @@ -630,31 +697,24 @@ def __format__(self, format_spec): # mix-in branch else: cls = self._member_type_ - val = self._value_ + val = self.value return cls.__format__(val, format_spec) def __hash__(self): - return hash(self._name_) + return hash(self.name) def __reduce_ex__(self, proto): - return self.__class__, (self._value_, ) - - # DynamicClassAttribute is used to provide access to the `name` and - # `value` properties of enum members while keeping some measure of - # protection from modification, while still allowing for an enumeration - # to have members named `name` and `value`. This works because enumeration - # members are not set directly on the enum class -- __getattr__ is - # used to look them up. + return self.__class__, (self.value, ) - @DynamicClassAttribute - def name(self): - """The name of the Enum member.""" - return self._name_ + def __setattr__(self, key, value): + if key in {'name', 'value'}: + raise AttributeError("Can't set attribute") + object.__setattr__(self, key, value) - @DynamicClassAttribute - def value(self): - """The value of the Enum member.""" - return self._value_ + def __delattr__(self, key): + if key in {'name', 'value'}: + raise AttributeError("Can't del attribute") + object.__delattr__(self, key) class IntEnum(int, Enum): @@ -683,7 +743,7 @@ def _generate_next_value_(name, start, count, last_values): high_bit = _high_bit(last_value) break except Exception: - raise TypeError('Invalid Flag value: %r' % last_value) from None + raise TypeError(f'Invalid Flag value: {last_value!r}') from None return 2 ** (high_bit+1) @classmethod @@ -706,11 +766,13 @@ def _create_pseudo_member_(cls, value): # 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__)) + raise ValueError(f'{value!r} is not a valid {cls.__qualname__}') # construct a singleton enum pseudo-member pseudo_member = object.__new__(cls) pseudo_member._name_ = None pseudo_member._value_ = value + object.__setattr__(pseudo_member, 'name', None) + object.__setattr__(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) @@ -718,60 +780,70 @@ def _create_pseudo_member_(cls, value): def __contains__(self, other): if not isinstance(other, self.__class__): - raise TypeError( - "unsupported operand type(s) for 'in': '%s' and '%s'" % ( - type(other).__qualname__, self.__class__.__qualname__)) - return other._value_ & self._value_ == other._value_ + raise TypeError(f"unsupported operand type(s) for 'in': " + f"{type(other).__qualname__!r} and {self.__class__.__qualname__!r}") + return other.value & self.value == other.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_, - ) + if self.name is not None: + return f'<{cls.__name__}.{self.name}: {self.value!r}>' + cached = self._repr_ + if cached is not None: + return cached + members, uncovered = _decompose(cls, self.value) + members = '|'.join([str(m.name or m.value) for m in members]) + self._repr_ = result = f"<{cls.__name__}.{members}: {self.value!r}>" + return result 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_) + if self.name is not None: + return f'{cls.__name__}.{self.name}' + cached = self._str_ + if cached is not None: + return cached + members, uncovered = _decompose(cls, self.value) + if len(members) == 1 and members[0].name is None: + self._str_ = result = f'{cls.__name__}.{members[0].value!r}' else: - return '%s.%s' % ( - cls.__name__, - '|'.join([str(m._name_ or m._value_) for m in members]), - ) + self._str_ = result = f"{cls.__name__}.{'|'.join([str(m.name or m.value) for m in members])}" + return result def __bool__(self): - return bool(self._value_) + return bool(self.value) def __or__(self, other): - if not isinstance(other, self.__class__): + cls = self.__class__ + if not isinstance(other, cls): return NotImplemented - return self.__class__(self._value_ | other._value_) + return cls(self.value | other.value) def __and__(self, other): - if not isinstance(other, self.__class__): + cls = self.__class__ + if not isinstance(other, cls): return NotImplemented - return self.__class__(self._value_ & other._value_) + return cls(self.value & other.value) def __xor__(self, other): - if not isinstance(other, self.__class__): + cls = self.__class__ + if not isinstance(other, cls): return NotImplemented - return self.__class__(self._value_ ^ other._value_) + return cls(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_): + cached = self._invert_ + if cached is not None: + return cached + cls = self.__class__ + members, uncovered = _decompose(cls, self.value) + inverted = cls(0) + for m in cls: + if m not in members and not (m.value & self.value): inverted = inverted | m - return self.__class__(inverted) + self._invert_ = result = cls(inverted) + result._invert_ = self + return result class IntFlag(int, Flag): @@ -780,7 +852,7 @@ class IntFlag(int, Flag): @classmethod def _missing_(cls, value): if not isinstance(value, int): - raise ValueError("%r is not a valid %s" % (value, cls.__qualname__)) + raise ValueError(f'{value!r} is not a valid {cls.__qualname__}') new_member = cls._create_pseudo_member_(value) return new_member @@ -788,7 +860,9 @@ def _missing_(cls, value): def _create_pseudo_member_(cls, value): pseudo_member = cls._value2member_map_.get(value, None) if pseudo_member is None: - need_to_create = [value] + # using dict with flag values as keys for faster lookups + # can't use set() because it is not a sequence + need_to_create = {value: ...} # get unaccounted for bits _, extra_flags = _decompose(cls, value) # timer = 10 @@ -799,7 +873,7 @@ def _create_pseudo_member_(cls, value): if (flag_value not in cls._value2member_map_ and flag_value not in need_to_create ): - need_to_create.append(flag_value) + need_to_create[flag_value] = ... if extra_flags == -flag_value: extra_flags = 0 else: @@ -815,27 +889,33 @@ def _create_pseudo_member_(cls, value): return pseudo_member def __or__(self, other): - if not isinstance(other, (self.__class__, int)): + cls = self.__class__ + if not isinstance(other, (cls, int)): return NotImplemented - result = self.__class__(self._value_ | self.__class__(other)._value_) + result = cls(self.value | cls(other).value) return result def __and__(self, other): - if not isinstance(other, (self.__class__, int)): + cls = self.__class__ + if not isinstance(other, (cls, int)): return NotImplemented - return self.__class__(self._value_ & self.__class__(other)._value_) + return cls(self.value & cls(other).value) def __xor__(self, other): - if not isinstance(other, (self.__class__, int)): + cls = self.__class__ + if not isinstance(other, (cls, int)): return NotImplemented - return self.__class__(self._value_ ^ self.__class__(other)._value_) + return cls(self.value ^ cls(other).value) __ror__ = __or__ __rand__ = __and__ __rxor__ = __xor__ def __invert__(self): - result = self.__class__(~self._value_) + cached = self._invert_ + if cached is not None: + return cached + self._invert_ = result = self.__class__(~self.value) return result @@ -850,10 +930,8 @@ def unique(enumeration): if name != member.name: duplicates.append((name, member.name)) if duplicates: - alias_details = ', '.join( - ["%s -> %s" % (alias, name) for (alias, name) in duplicates]) - raise ValueError('duplicate values found in %r: %s' % - (enumeration, alias_details)) + alias_details = ', '.join([f'{alias} -> {name}' for alias, name in duplicates]) + raise ValueError(f'duplicate values found in {enumeration!r}: {alias_details}') return enumeration def _decompose(flag, value): @@ -877,7 +955,7 @@ def _decompose(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) + 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) diff --git a/Lib/inspect.py b/Lib/inspect.py index 608ca9551160e3..6d87092887cd85 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2420,7 +2420,7 @@ class _ParameterKind(enum.IntEnum): VAR_KEYWORD = 4 def __str__(self): - return self._name_ + return self.name @property def description(self): diff --git a/Lib/re.py b/Lib/re.py index 8f1d55ddf7d69d..e3ff81f4a8a03e 100644 --- a/Lib/re.py +++ b/Lib/re.py @@ -153,17 +153,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_ + 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 & m.value: + value &= ~m.value + members.append(f're.{m.name}') if value: members.append(hex(value)) res = '|'.join(members) diff --git a/Lib/test/test_dynamicclassattribute.py b/Lib/test/test_dynamicclassattribute.py index 9f694d9eb46771..16b1e2b5097176 100644 --- a/Lib/test/test_dynamicclassattribute.py +++ b/Lib/test/test_dynamicclassattribute.py @@ -295,6 +295,36 @@ def spam(self): self.assertEqual(Foo.__dict__['spam'].__doc__, "a new docstring") +class TestSetClassAttr(unittest.TestCase): + def test_set_class_attr(self): + class Foo: + def __init__(self, value): + self._value = value + self._spam = 'spam' + + @DynamicClassAttribute + def value(self): + return self._value + + spam = DynamicClassAttribute( + lambda s: s._spam, + alias='my_shiny_spam' + ) + + self.assertFalse(hasattr(Foo, 'value')) + self.assertFalse(hasattr(Foo, 'name')) + + foo_bar = Foo('bar') + value_desc = Foo.__dict__['value'] + value_desc.set_class_attr(Foo, foo_bar) + self.assertIs(Foo.value, foo_bar) + self.assertEqual(Foo.value.value, 'bar') + + foo_baz = Foo('baz') + Foo.my_shiny_spam = foo_baz + self.assertIs(Foo.spam, foo_baz) + self.assertEqual(Foo.spam.spam, 'spam') + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index ec1cfeab12d7b6..2a476dbd367878 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -1,6 +1,8 @@ import enum import inspect import pydoc +import warnings + import sys import unittest import threading @@ -14,6 +16,8 @@ # for pickle tests +from unittest import mock + try: class Stooges(Enum): LARRY = 1 @@ -200,7 +204,7 @@ def wowser(self): ) self.assertEqual( set(dir(Test.this)), - set(['__class__', '__doc__', '__module__', 'name', 'value', 'wowser']), + set(['__class__', '__doc__', '__module__', 'wowser', 'name', 'value']), ) def test_dir_on_sub_with_behavior_on_super(self): @@ -212,7 +216,7 @@ class SubEnum(SuperEnum): sample = 5 self.assertEqual( set(dir(SubEnum.sample)), - set(['__class__', '__doc__', '__module__', 'name', 'value', 'invisible']), + set(['__class__', '__doc__', '__module__', 'invisible', 'name', 'value']), ) def test_enum_in_enum_out(self): @@ -234,6 +238,7 @@ def test_enum(self): self.assertEqual( [Season.SPRING, Season.SUMMER, Season.AUTUMN, Season.WINTER], lst) + self.assertEqual(repr(Season), "") for i, season in enumerate('SPRING SUMMER AUTUMN WINTER'.split(), 1): e = Season(i) self.assertEqual(e, getattr(Season, season)) @@ -326,7 +331,7 @@ class RealLogic(Enum): true = True false = False def __bool__(self): - return bool(self._value_) + return bool(self.value) self.assertTrue(RealLogic.true) self.assertFalse(RealLogic.false) # mixed Enums depend on mixed-in type @@ -1079,9 +1084,9 @@ def test_multiple_mixin_mro(self): class auto_enum(type(Enum)): def __new__(metacls, cls, bases, classdict): temp = type(classdict)() - names = set(classdict._member_names) + names = classdict.members.keys() i = 0 - for k in classdict._member_names: + for k in classdict.members: v = classdict[k] if v is Ellipsis: v = i @@ -1435,7 +1440,7 @@ class NEI(NamedInt, Enum): x = ('the-x', 1) y = ('the-y', 2) def __reduce_ex__(self, proto): - return getattr, (self.__class__, self._name_) + return getattr, (self.__class__, self.name) self.assertIs(NEI.__new__, Enum.__new__) self.assertEqual(repr(NEI.x + NEI.y), "NamedInt('(the-x + the-y)', 3)") @@ -1470,7 +1475,7 @@ def __new__(cls): obj._value_ = value return obj def __int__(self): - return int(self._value_) + return int(self.value) self.assertEqual( list(AutoNumber), [AutoNumber.first, AutoNumber.second, AutoNumber.third], @@ -1487,7 +1492,7 @@ def __new__(cls): obj._value_ = value return obj def __int__(self): - return int(self._value_) + return int(self.value) class Color(AutoNumber): red = () green = () @@ -1519,19 +1524,19 @@ def test_ordered_mixin(self): class OrderedEnum(Enum): def __ge__(self, other): if self.__class__ is other.__class__: - return self._value_ >= other._value_ + return self.value >= other.value return NotImplemented def __gt__(self, other): if self.__class__ is other.__class__: - return self._value_ > other._value_ + return self.value > other.value return NotImplemented def __le__(self, other): if self.__class__ is other.__class__: - return self._value_ <= other._value_ + return self.value <= other.value return NotImplemented def __lt__(self, other): if self.__class__ is other.__class__: - return self._value_ < other._value_ + return self.value < other.value return NotImplemented class Grade(OrderedEnum): A = 5 @@ -1799,7 +1804,7 @@ def MAX(cls): return max class StrMixin: def __str__(self): - return self._name_.lower() + return self.name.lower() class SomeEnum(Enum): def behavior(self): return 'booyah' @@ -2294,7 +2299,7 @@ def ALL(cls): return all_value class StrMixin: def __str__(self): - return self._name_.lower() + return self.name.lower() class Color(AllMixin, Flag): RED = auto() GREEN = auto() @@ -2338,7 +2343,7 @@ class TestFlag(Flag): def __eq__(self, other): return self is other def __hash__(self): - return hash(self._value_) + return hash(self.value) # have multiple threads competing to complete the composite members seen = set() failed = False @@ -2712,7 +2717,7 @@ def ALL(cls): return all_value class StrMixin: def __str__(self): - return self._name_.lower() + return self.name.lower() class Color(AllMixin, IntFlag): RED = auto() GREEN = auto() @@ -2756,7 +2761,7 @@ class TestFlag(IntFlag): def __eq__(self, other): return self is other def __hash__(self): - return hash(self._value_) + return hash(self.value) # have multiple threads competing to complete the composite members seen = set() failed = False @@ -2866,15 +2871,6 @@ class Color(enum.Enum) | red = |\x20\x20 | ---------------------------------------------------------------------- - | Data descriptors inherited from enum.Enum: - |\x20\x20 - | name - | The name of the Enum member. - |\x20\x20 - | value - | The value of the Enum member. - |\x20\x20 - | ---------------------------------------------------------------------- | Readonly properties inherited from enum.EnumMeta: |\x20\x20 | __members__ @@ -2943,9 +2939,7 @@ def test_inspect_getmembers(self): ('__module__', __name__), ('blue', self.Color.blue), ('green', self.Color.green), - ('name', Enum.__dict__['name']), ('red', self.Color.red), - ('value', Enum.__dict__['value']), )) result = dict(inspect.getmembers(self.Color)) self.assertEqual(values.keys(), result.keys()) @@ -2977,10 +2971,6 @@ def test_inspect_classify_class_attrs(self): defining_class=self.Color, object=self.Color.green), Attribute(name='red', kind='data', defining_class=self.Color, object=self.Color.red), - Attribute(name='name', kind='data', - defining_class=Enum, object=Enum.__dict__['name']), - Attribute(name='value', kind='data', - defining_class=Enum, object=Enum.__dict__['value']), ] values.sort(key=lambda item: item.name) result = list(inspect.classify_class_attrs(self.Color)) @@ -3012,7 +3002,7 @@ class TestIntEnumConvert(unittest.TestCase): def test_convert_value_lookup_priority(self): test_type = enum.IntEnum._convert_( 'UnittestConvert', - ('test.test_enum', '__main__')[__name__=='__main__'], + __name__, filter=lambda x: x.startswith('CONVERT_TEST_')) # We don't want the reverse lookup value to vary when there are # multiple possible names for a given value. It should always @@ -3022,7 +3012,7 @@ def test_convert_value_lookup_priority(self): def test_convert(self): test_type = enum.IntEnum._convert_( 'UnittestConvert', - ('test.test_enum', '__main__')[__name__=='__main__'], + __name__, filter=lambda x: x.startswith('CONVERT_TEST_')) # Ensure that test_type has all of the desired names and values. self.assertEqual(test_type.CONVERT_TEST_NAME_F, @@ -3042,7 +3032,7 @@ def test_convert_warn(self): with self.assertWarns(DeprecationWarning): enum.IntEnum._convert( 'UnittestConvert', - ('test.test_enum', '__main__')[__name__=='__main__'], + __name__, filter=lambda x: x.startswith('CONVERT_TEST_')) @unittest.skipUnless(sys.version_info >= (3, 9), @@ -3051,9 +3041,128 @@ def test_convert_raise(self): with self.assertRaises(AttributeError): enum.IntEnum._convert( 'UnittestConvert', - ('test.test_enum', '__main__')[__name__=='__main__'], + __name__, filter=lambda x: x.startswith('CONVERT_TEST_')) +class TestEnumNamesDeprecation(unittest.TestCase): + @unittest.skipUnless(sys.version_info[:2] == (3, 9), + '_member_names_ was deprecated in 3.9') + def test_member_names_warn(self): + with self.assertWarns(DeprecationWarning): + _ = enum.IntEnum._member_names_ + + @unittest.skipUnless(sys.version_info >= (3, 10), '_member_names_ was removed in 3.10') + def test_member_names_raise(self): + with self.assertRaises(AttributeError): + _ = enum.IntEnum._member_names_ + + +class TestEnumDictAttrsDeprecation(unittest.TestCase): + @unittest.skipUnless(sys.version_info[:2] == (3, 9), + '_member_names was deprecated in 3.9') + def test_member_names_warn(self): + with self.assertWarns(DeprecationWarning): + _ = enum._EnumDict()._member_names + + with self.assertWarns(DeprecationWarning): + _ = enum._EnumDict()._last_values + + @unittest.skipUnless(sys.version_info >= (3, 10), '_member_names was removed in 3.10') + def test_member_names_raise(self): + with self.assertRaises(AttributeError): + _ = enum._EnumDict()._member_names + + with self.assertRaises(AttributeError): + _ = enum._EnumDict()._last_values + + +class TestNameValueAttrsDeprecation(unittest.TestCase): + + class SquareFoo(Enum): + a = 1 + b = 2 + + def __new__(cls, value): + member = object.__new__(cls) + member._value_ = value ** 2 + return member + + @unittest.skipUnless(sys.version_info[:2] == (3, 9), + '_name_ and _value_ attr access was deprecated in 3.9') + def test_get_warns(self): + with self.assertWarns(DeprecationWarning): + self.assertEqual(self.SquareFoo.b._value_, 4) + + @unittest.skipUnless(sys.version_info >= (3, 9), + '_name_ and _value_ attr access was deprecated in 3.9') + def test_set_not_warns(self): + for sunder_attr, public_attr in ('_name_', 'name'), ('_value_', 'value'): + with warnings.catch_warnings(record=True) as caught_warnings: + setattr(self.SquareFoo.b, sunder_attr, 'foo') + + self.assertEqual(caught_warnings, [], + msg=f'Unexpected warnings while setting {sunder_attr}') + self.assertEqual(getattr(self.SquareFoo.b, public_attr), 'foo', + msg=f'{public_attr} and {sunder_attr} mismatch') + + @unittest.skipUnless(sys.version_info >= (3, 10), + '_name_ and _value_ attr access was removed in 3.10') + def test_get_raise(self): + for attr in ('_name_', '_value_'): + with self.assertRaises(AttributeError): + getattr(self.SquareFoo.b, attr) + + +class TestStaticAttrs(unittest.TestCase): + + def test_invert_cache(self): + class Foo(Flag): + a = 1 + b = 2 + c = 3 + + with mock.patch('enum._decompose') as decompose_mock: + self.assertIs(Foo.a._invert_, Foo.b) + self.assertIs(Foo.b._invert_, Foo.a) + self.assertIsNotNone(Foo.c._invert_) + self.assertIs(~(~Foo.a), Foo.a) + self.assertFalse(decompose_mock.called) + + def test_repr_str_cache(self): + class Foo(Flag): + a = 1 + b = 2 + c = 3 + + with mock.patch('enum._decompose') as decompose_mock: + no_name = ~Foo.c + self.assertFalse(decompose_mock.called) + no_name_repr = repr(no_name) + no_name_str = str(no_name) + self.assertEqual(no_name_repr, '') + self.assertEqual(no_name_str, 'Foo.0') + + with mock.patch('enum._decompose') as decompose_mock: + self.assertIs(repr(no_name), no_name_repr) + self.assertIs(str(no_name), no_name_str) + self.assertFalse(decompose_mock.called) + + def test_cache_invalidate(self): + class Foo(Flag): + a = 1 + b = 2 + c = 3 + + self.assertIs(Foo.a._invert_, Foo.b) + Foo.a._value_ = 3 + self.assertIsNone(Foo.a._invert_) + self.assertIsNone(Foo.a._str_) + self.assertIsNone(Foo.a._repr_) + self.assertIs(~Foo.a, ~Foo.c) + self.assertEqual(repr(Foo.a), '') + self.assertEqual(str(Foo.a), 'Foo.a') + + if __name__ == '__main__': unittest.main() diff --git a/Lib/types.py b/Lib/types.py index ea3c0b29d5d4dd..6c3cdb1313b247 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -150,29 +150,35 @@ class DynamicClassAttribute: """Route attribute access on a class to __getattr__. This is a descriptor, used to define attributes that act differently when - accessed through an instance and through a class. Instance access remains - normal, but access to an attribute through a class will be routed to the - class's __getattr__ method; this is done by raising AttributeError. + accessed through an instance and through a class. + + Access on instance behaves like a @property, but access on class + will be routed to the given alias ('_cls_attr_' + attr_name by default) + You can set class attribute through descriptor using set_class_attr or directly using alias + If class attr is not set, AttributeError will be raised, routing to __getattr__ method of class type) 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). """ - def __init__(self, fget=None, fset=None, fdel=None, doc=None): + def __init__(self, fget=None, fset=None, fdel=None, doc=None, alias=None): self.fget = fget self.fset = fset self.fdel = fdel + # next two lines make DynamicClassAttribute act the same as property self.__doc__ = doc or fget.__doc__ self.overwrite_doc = doc is None # support for abstract methods self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False)) + # define name for class attributes + self.alias = alias - def __get__(self, instance, ownerclass=None): + def __get__(self, instance, ownerclass): if instance is None: if self.__isabstractmethod__: return self - raise AttributeError() + return getattr(ownerclass, self.alias) elif self.fget is None: raise AttributeError("unreadable attribute") return self.fget(instance) @@ -187,6 +193,13 @@ def __delete__(self, instance): raise AttributeError("can't delete attribute") self.fdel(instance) + def __set_name__(self, ownerclass, alias): + if self.alias is None: + self.alias = f'_cls_attr_{alias}' + + def set_class_attr(self, cls, value): + setattr(cls, self.alias, value) + def getter(self, fget): fdoc = fget.__doc__ if self.overwrite_doc else None result = type(self)(fget, self.fset, self.fdel, fdoc or self.__doc__) diff --git a/Misc/NEWS.d/next/Library/2019-12-20-12-31-41.bpo-39102.zqgDii.rst b/Misc/NEWS.d/next/Library/2019-12-20-12-31-41.bpo-39102.zqgDii.rst new file mode 100644 index 00000000000000..16738a4143ed46 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-12-20-12-31-41.bpo-39102.zqgDii.rst @@ -0,0 +1,2 @@ +Significantly improve speed of accessing ``Enum`` attributes and slightly improve speed of trying values. +Remove ``EnumMeta.__getattr__``, remove ``Enum.name`` and ``Enum.value`` ``DynamicAttributes`` (they set as enum members attributes on its creation), fasten ``Enum.__new__`` a bit. Fasten ``DynamicClassAttribute`` by adding ability to preset class attributes with ``DynamicClassAttribute.set_class_attr`` \ No newline at end of file