From cad12cce5a28186a740995c275260dd0f2e4b70a Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Fri, 9 Aug 2019 14:11:04 +0300 Subject: [PATCH 01/23] Added class attributes access trough DynamicClassProperty (first attempt) --- Lib/enum.py | 40 +++++++++++++--------------------------- Lib/types.py | 22 ++++++++++++++++------ 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 8f82a6da99588f..0046b2fb082eab 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -166,9 +166,7 @@ def __new__(metacls, cls, bases, classdict): enum_class._member_map_ = {} # name->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)} @@ -199,11 +197,11 @@ def __new__(metacls, cls, bases, classdict): for member_name in classdict._member_names: value = enum_members[member_name] if not isinstance(value, tuple): - args = (value, ) + args = (value,) else: args = value - if member_type is tuple: # special case for tuple enums - args = (args, ) # wrap it one more time + if member_type is tuple: # special case for tuple enums + args = (args,) # wrap it one more time if not use_args: enum_member = __new__(enum_class) if not hasattr(enum_member, '_value_'): @@ -228,10 +226,15 @@ def __new__(metacls, cls, bases, classdict): 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: + + if dynamic_attr := dynamic_attributes.get(member_name, False): + # 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: @@ -324,22 +327,6 @@ 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 - def __getitem__(cls, name): return cls._member_map_[name] @@ -373,8 +360,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) diff --git a/Lib/types.py b/Lib/types.py index ea3c0b29d5d4dd..e99017d8ac07a0 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -150,29 +150,36 @@ 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. + + Instance access remains normal, but access to an attribute through a class + will be routed to the same arg but with the _cls_attr prefix + (if this attr is not present, 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, name=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.instance_attr_name = name or fget.__name__ + self.class_attr_name = f'_cls_attr{self.instance_attr_name}' - 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.class_attr_name) elif self.fget is None: raise AttributeError("unreadable attribute") return self.fget(instance) @@ -187,6 +194,9 @@ def __delete__(self, instance): raise AttributeError("can't delete attribute") self.fdel(instance) + def set_class_attr(self, cls, value): + setattr(cls, self.class_attr_name, 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__) From d6a48c33f045b5a2fea502cf23773839dc697ed2 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Fri, 9 Aug 2019 14:11:04 +0300 Subject: [PATCH 02/23] Added class attributes access trough DynamicClassProperty (first attempt) --- Lib/enum.py | 40 +++++++++++++--------------------------- Lib/types.py | 22 ++++++++++++++++------ 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 0be1b60cd6d8aa..e620d6d964af80 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -166,9 +166,7 @@ def __new__(metacls, cls, bases, classdict): enum_class._member_map_ = {} # name->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)} @@ -199,11 +197,11 @@ def __new__(metacls, cls, bases, classdict): for member_name in classdict._member_names: value = enum_members[member_name] if not isinstance(value, tuple): - args = (value, ) + args = (value,) else: args = value - if member_type is tuple: # special case for tuple enums - args = (args, ) # wrap it one more time + if member_type is tuple: # special case for tuple enums + args = (args,) # wrap it one more time if not use_args: enum_member = __new__(enum_class) if not hasattr(enum_member, '_value_'): @@ -228,10 +226,15 @@ def __new__(metacls, cls, bases, classdict): 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: + + if dynamic_attr := dynamic_attributes.get(member_name, False): + # 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: @@ -324,22 +327,6 @@ 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 - def __getitem__(cls, name): return cls._member_map_[name] @@ -373,8 +360,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) diff --git a/Lib/types.py b/Lib/types.py index ea3c0b29d5d4dd..e99017d8ac07a0 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -150,29 +150,36 @@ 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. + + Instance access remains normal, but access to an attribute through a class + will be routed to the same arg but with the _cls_attr prefix + (if this attr is not present, 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, name=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.instance_attr_name = name or fget.__name__ + self.class_attr_name = f'_cls_attr{self.instance_attr_name}' - 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.class_attr_name) elif self.fget is None: raise AttributeError("unreadable attribute") return self.fget(instance) @@ -187,6 +194,9 @@ def __delete__(self, instance): raise AttributeError("can't delete attribute") self.fdel(instance) + def set_class_attr(self, cls, value): + setattr(cls, self.class_attr_name, 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__) From 923902e454c644565e42669e8530b428b93154c3 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Fri, 20 Dec 2019 03:38:49 +0300 Subject: [PATCH 03/23] Replaced dynamic attributes Enum.name and .value to instance attrs --- Lib/enum.py | 33 +++++++++++++++++---------------- Lib/test/test_enum.py | 21 +++------------------ 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index e620d6d964af80..6e582f96a1c13d 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -215,6 +215,9 @@ def __new__(metacls, cls, bases, classdict): enum_member._value_ = member_type(*args) value = enum_member._value_ enum_member._name_ = member_name + # setting protected attributes + object.__setattr__(enum_member, 'name', member_name) + object.__setattr__(enum_member, 'value', value) enum_member.__objclass__ = enum_class enum_member.__init__(*args) # If another member with the same value was already defined, the @@ -227,7 +230,8 @@ def __new__(metacls, cls, bases, classdict): # Aliases don't appear in member names (only in __members__). enum_class._member_names_.append(member_name) - if dynamic_attr := dynamic_attributes.get(member_name, False): + 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) @@ -625,22 +629,15 @@ def __hash__(self): 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. + def __setattr__(self, key, value): + if key in {'name', 'value'}: + raise AttributeError("Can't set attribute") + object.__setattr__(self, key, value) - @DynamicClassAttribute - def name(self): - """The name of the Enum member.""" - return self._name_ - - @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): @@ -697,6 +694,8 @@ def _create_pseudo_member_(cls, value): 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) @@ -795,6 +794,8 @@ def _create_pseudo_member_(cls, value): pseudo_member = int.__new__(cls, value) 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) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index ec1cfeab12d7b6..07b3e6af15d199 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -185,7 +185,7 @@ def test_dir_on_item(self): Season = self.Season self.assertEqual( set(dir(Season.WINTER)), - set(['__class__', '__doc__', '__module__', 'name', 'value']), + set(['__class__', '__doc__', '__module__']), ) def test_dir_with_added_behavior(self): @@ -200,7 +200,7 @@ def wowser(self): ) self.assertEqual( set(dir(Test.this)), - set(['__class__', '__doc__', '__module__', 'name', 'value', 'wowser']), + set(['__class__', '__doc__', '__module__', 'wowser']), ) def test_dir_on_sub_with_behavior_on_super(self): @@ -212,7 +212,7 @@ class SubEnum(SuperEnum): sample = 5 self.assertEqual( set(dir(SubEnum.sample)), - set(['__class__', '__doc__', '__module__', 'name', 'value', 'invisible']), + set(['__class__', '__doc__', '__module__', 'invisible']), ) def test_enum_in_enum_out(self): @@ -2866,15 +2866,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 +2934,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 +2966,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)) From 8f2f90c188f6e3c2cacbca5d0bc9f74399cde034 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Fri, 20 Dec 2019 04:02:33 +0300 Subject: [PATCH 04/23] Improve values check speed (e.g. Color(3)) --- Lib/enum.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 6e582f96a1c13d..88c49bee559401 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -542,7 +542,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 @@ -558,23 +560,29 @@ def __new__(cls, value): 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("%r is not a valid %s" % (value, cls.__qualname__)) + raise + if isinstance(result, cls): return result + + ve_exc = ValueError("%r is not a valid %s" % (value, 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( + 'error in %s._missing_: returned %r instead of None or a valid member' + % (cls.__name__, result) + ) exc.__context__ = ve_exc raise exc From ed4332006969236ff23c95cf84b3f6cab44074b7 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Fri, 20 Dec 2019 04:14:18 +0300 Subject: [PATCH 05/23] Replace tuples with sets for faster lookups --- Lib/enum.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 88c49bee559401..f7f3d62458bf62 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -71,10 +71,10 @@ def __setitem__(self, key, value): """ if _is_sunder(key): - if key not in ( + if key not in { '_order_', '_create_pseudo_member_', '_generate_next_value_', '_missing_', '_ignore_', - ): + }: raise ValueError('_names_ are reserved for future Enum use') if key == '_generate_next_value_': setattr(self, '_generate_next_value', value) @@ -151,7 +151,7 @@ def __new__(metacls, cls, bases, classdict): _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))) @@ -185,8 +185,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) @@ -251,7 +251,7 @@ def __new__(metacls, cls, bases, classdict): # 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) @@ -506,15 +506,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: From 2c0372a45f330528ceb7c300eb644fdbfb70eb33 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Fri, 20 Dec 2019 04:21:34 +0300 Subject: [PATCH 06/23] Used cls variable instead if multiple self.__class__ --- Lib/enum.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index f7f3d62458bf62..045fc8c277a941 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -745,27 +745,31 @@ def __bool__(self): 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__: + 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) + return cls(inverted) class IntFlag(int, Flag): @@ -811,20 +815,23 @@ 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__ From 48d75d0f34a13455ff213677fc5fc4ff23de10b6 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Fri, 20 Dec 2019 15:00:51 +0300 Subject: [PATCH 07/23] Fix whitespace in types --- Lib/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/types.py b/Lib/types.py index e99017d8ac07a0..a7c9c7a1cd33de 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -196,7 +196,7 @@ def __delete__(self, instance): def set_class_attr(self, cls, value): setattr(cls, self.class_attr_name, 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__) From ce84f10644870a35848536d0fe6697ded1a8ab6c Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2019 12:31:42 +0000 Subject: [PATCH 08/23] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2019-12-20-12-31-41.bpo-39102.zqgDii.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2019-12-20-12-31-41.bpo-39102.zqgDii.rst 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 From 0d1996ff52562b7df8c07a392853086e385d3200 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Sat, 21 Dec 2019 06:08:46 +0300 Subject: [PATCH 09/23] Rename DynamicClassAttribute.__init__(name -> alias), add tests --- Lib/test/test_dynamicclassattribute.py | 30 ++++++++++++++++++++++++++ Lib/types.py | 15 +++++++------ 2 files changed, 39 insertions(+), 6 deletions(-) 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/types.py b/Lib/types.py index a7c9c7a1cd33de..f4924239b779ef 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -152,16 +152,16 @@ class DynamicClassAttribute: 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 same arg but with the _cls_attr prefix - (if this attr is not present, AttributeError will be raised, + Access on instance behaves like a @property, but access on class + will be routed to the given alias ('_cls_attr_' + attr_name) by default + If such attr not present in class, 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, name=None): + def __init__(self, fget=None, fset=None, fdel=None, doc=None, alias=None): self.fget = fget self.fset = fset self.fdel = fdel @@ -172,8 +172,7 @@ def __init__(self, fget=None, fset=None, fdel=None, doc=None, name=None): # support for abstract methods self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False)) # define name for class attributes - self.instance_attr_name = name or fget.__name__ - self.class_attr_name = f'_cls_attr{self.instance_attr_name}' + self.class_attr_name = alias def __get__(self, instance, ownerclass): if instance is None: @@ -194,6 +193,10 @@ def __delete__(self, instance): raise AttributeError("can't delete attribute") self.fdel(instance) + def __set_name__(self, ownerclass, name): + if self.class_attr_name is None: + self.class_attr_name = f'_cls_attr_{name}' + def set_class_attr(self, cls, value): setattr(cls, self.class_attr_name, value) From 2f6b977c05dd062687a90d10b9c733c2474ebf24 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Sat, 21 Dec 2019 07:32:16 +0300 Subject: [PATCH 10/23] DynamicClassAttribute.class_attr_name -> alias --- Lib/types.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Lib/types.py b/Lib/types.py index f4924239b779ef..6c3cdb1313b247 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -153,9 +153,9 @@ class DynamicClassAttribute: 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 - If such attr not present in class, AttributeError will be raised, - routing to __getattr__ method of class type) + 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). @@ -172,13 +172,13 @@ def __init__(self, fget=None, fset=None, fdel=None, doc=None, alias=None): # support for abstract methods self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False)) # define name for class attributes - self.class_attr_name = alias + self.alias = alias def __get__(self, instance, ownerclass): if instance is None: if self.__isabstractmethod__: return self - return getattr(ownerclass, self.class_attr_name) + return getattr(ownerclass, self.alias) elif self.fget is None: raise AttributeError("unreadable attribute") return self.fget(instance) @@ -193,12 +193,12 @@ def __delete__(self, instance): raise AttributeError("can't delete attribute") self.fdel(instance) - def __set_name__(self, ownerclass, name): - if self.class_attr_name is None: - self.class_attr_name = f'_cls_attr_{name}' + 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.class_attr_name, value) + setattr(cls, self.alias, value) def getter(self, fget): fdoc = fget.__doc__ if self.overwrite_doc else None From 1cedc4c6142656e2db1a9aa0971594bae5b5af4b Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Sat, 21 Dec 2019 04:35:05 +0300 Subject: [PATCH 11/23] Use _unique_member_map_ instead of _member_names_ to store unique enum member --- Lib/enum.py | 32 ++++++++++++++++++++++---------- Lib/test/test_enum.py | 12 ++++++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 045fc8c277a941..c57453ac51b9ec 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -162,8 +162,9 @@ def __new__(metacls, cls, bases, 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_names = [] # names in definition order enum_class._member_map_ = {} # name->value map + enum_class._unique_member_map_ = {} enum_class._member_type_ = member_type dynamic_attributes = {k: v for c in enum_class.mro() @@ -228,7 +229,8 @@ def __new__(metacls, cls, bases, classdict): break else: # Aliases don't appear in member names (only in __members__). - enum_class._member_names_.append(member_name) + enum_class._unique_member_map_[member_name] = enum_member + enum_class._member_names.append(member_name) dynamic_attr = dynamic_attributes.get(member_name) if dynamic_attr is not None: @@ -271,7 +273,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 @@ -328,17 +330,18 @@ def __delattr__(cls, attr): super().__delattr__(attr) def __dir__(self): - return (['__class__', '__doc__', '__members__', '__module__'] + - self._member_names_) + 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): @@ -354,7 +357,7 @@ def __repr__(cls): return "" % cls.__name__ 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. @@ -454,6 +457,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 cls._member_names + @staticmethod def _get_mixins_(bases): """Returns the type for creating enum members, and the first inherited @@ -482,7 +494,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 @@ -557,7 +569,7 @@ def __new__(cls, value): pass except TypeError: # not there, now do long search -- O(n) behavior - for member in cls._member_map_.values(): + for member in cls._unique_member_map_.values(): if member._value_ == value: return member # still not found -- try _missing_ hook diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 07b3e6af15d199..9fe1c7edc07836 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -3040,5 +3040,17 @@ def test_convert_raise(self): filter=lambda x: x.startswith('CONVERT_TEST_')) +class TestEnumNamesDeprecation(unittest.TestCase): + @unittest.skipUnless(sys.version_info[:2] == (3, 9), + '_convert was deprecated in 3.9') + def test_convert_warn(self): + with self.assertWarns(DeprecationWarning): + _ = enum.IntEnum._member_names_ + + @unittest.skipUnless(sys.version_info >= (3, 10), '_convert was removed in 3.10') + def test_convert_raise(self): + with self.assertRaises(AttributeError): + _ = enum.IntEnum._member_names_ + if __name__ == '__main__': unittest.main() From 8304b6f2cbde67f6b323a0e9a1008615957b5891 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Sat, 21 Dec 2019 05:05:03 +0300 Subject: [PATCH 12/23] Use members dict instead of _member_names and _last_values on _EnumDict --- Lib/enum.py | 39 +++++++++++++++++++++++++++------------ Lib/test/test_enum.py | 24 ++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index c57453ac51b9ec..babe75f1f9660f 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -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): @@ -84,13 +83,13 @@ 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, )) 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) elif key in self._ignore: @@ -101,11 +100,28 @@ def __setitem__(self, key, value): raise TypeError('%r already defined as: %r' % (key, self[key])) 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 @@ -143,8 +159,8 @@ 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 @@ -195,8 +211,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: diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 9fe1c7edc07836..c24321c343eca6 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -1079,9 +1079,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 @@ -3052,5 +3052,25 @@ def test_convert_raise(self): with self.assertRaises(AttributeError): _ = enum.IntEnum._member_names_ + +class TestEnumDictAttrsDeprecation(unittest.TestCase): + @unittest.skipUnless(sys.version_info[:2] == (3, 9), + '_convert was deprecated in 3.9') + def test_convert_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), '_convert was removed in 3.10') + def test_convert_raise(self): + with self.assertRaises(AttributeError): + _ = enum._EnumDict()._member_names + + with self.assertRaises(AttributeError): + _ = enum._EnumDict()._last_values + + if __name__ == '__main__': unittest.main() From 4051208cf6bf11d8c8e756e2e61391747b9cfcad Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Sat, 21 Dec 2019 07:49:28 +0300 Subject: [PATCH 13/23] Restore original formatting --- Lib/enum.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index babe75f1f9660f..e49d80c47b451d 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -213,11 +213,11 @@ def __new__(metacls, cls, bases, classdict): # auto-numbering ;) for member_name, value in enum_members.items(): if not isinstance(value, tuple): - args = (value,) + args = (value, ) else: args = value - if member_type is tuple: # special case for tuple enums - args = (args,) # wrap it one more time + if member_type is tuple: # special case for tuple enums + args = (args, ) # wrap it one more time if not use_args: enum_member = __new__(enum_class) if not hasattr(enum_member, '_value_'): From a90a4aa29db65b4c9866831e1d85eebf32f6da21 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Sat, 21 Dec 2019 08:43:30 +0300 Subject: [PATCH 14/23] Fix whitespaces --- Lib/enum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/enum.py b/Lib/enum.py index e49d80c47b451d..f5a3ad3374e184 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -104,7 +104,7 @@ def __setitem__(self, key, value): value = value.value self.members[key] = value super().__setitem__(key, value) - + @property def _member_names(self): import warnings From 4436cbfaa2f94fd6a45a8c5a237099c3efecd007 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Sat, 21 Dec 2019 17:33:38 +0300 Subject: [PATCH 15/23] Use dict instead of list to check values in _create_pseudo_member_ --- Lib/enum.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index f5a3ad3374e184..69e3b88ad54478 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -813,7 +813,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 @@ -824,7 +826,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: From 45be0c6cb8e0818ad53eaf3ba0233da5118cd4ff Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Sun, 22 Dec 2019 07:47:16 +0300 Subject: [PATCH 16/23] Deprecate getting values through _value_ and _name_ Just realized that _value_ and _name_ attrs remained independent from name and value and could be different. This commit fixes it. --- Lib/enum.py | 100 ++++++++++++++++++++++++++---------------- Lib/inspect.py | 2 +- Lib/re.py | 12 ++--- Lib/test/test_enum.py | 89 ++++++++++++++++++++++++++----------- 4 files changed, 134 insertions(+), 69 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 69e3b88ad54478..7f9166d8bf592e 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -70,9 +70,10 @@ 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_': @@ -220,26 +221,25 @@ 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 - object.__setattr__(enum_member, 'name', member_name) - object.__setattr__(enum_member, 'value', value) 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: @@ -334,7 +334,7 @@ def __contains__(cls, member): 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_ + 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 @@ -585,7 +585,7 @@ def __new__(cls, value): except TypeError: # not there, now do long search -- O(n) behavior for member in cls._unique_member_map_.values(): - if member._value_ == value: + if member.value == value: return member # still not found -- try _missing_ hook @@ -614,6 +614,32 @@ def __new__(cls, value): 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): + object.__setattr__(self, 'value', value) + + @_name_.setter + def _name_(self, name): + object.__setattr__(self, 'name', name) + def _generate_next_value_(name, start, count, last_values): for last_value in reversed(last_values): try: @@ -629,10 +655,10 @@ def _missing_(cls, value): def __repr__(self): return "<%s.%s: %r>" % ( - self.__class__.__name__, self._name_, self._value_) + self.__class__.__name__, self.name, self.value) def __str__(self): - return "%s.%s" % (self.__class__.__name__, self._name_) + return "%s.%s" % (self.__class__.__name__, self.name) def __dir__(self): added_behavior = [ @@ -656,14 +682,14 @@ 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_, ) + return self.__class__, (self.value, ) def __setattr__(self, key, value): if key in {'name', 'value'}: @@ -742,59 +768,59 @@ def __contains__(self, other): raise TypeError( "unsupported operand type(s) for 'in': '%s' and '%s'" % ( type(other).__qualname__, self.__class__.__qualname__)) - return other._value_ & self._value_ == other._value_ + 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_) + 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_, + '|'.join([str(m.name or m.value) for m in members]), + 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_) + 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]), + '|'.join([str(m.name or m.value) for m in members]), ) def __bool__(self): - return bool(self._value_) + return bool(self.value) def __or__(self, other): cls = self.__class__ if not isinstance(other, cls): return NotImplemented - return cls(self._value_ | other._value_) + return cls(self.value | other.value) def __and__(self, other): cls = self.__class__ if not isinstance(other, cls): return NotImplemented - return cls(self._value_ & other._value_) + return cls(self.value & other.value) def __xor__(self, other): cls = self.__class__ if not isinstance(other, cls): return NotImplemented - return cls(self._value_ ^ other._value_) + return cls(self.value ^ other.value) def __invert__(self): cls = self.__class__ - members, uncovered = _decompose(cls, self._value_) + members, uncovered = _decompose(cls, self.value) inverted = cls(0) for m in cls: - if m not in members and not (m._value_ & self._value_): + if m not in members and not (m.value & self.value): inverted = inverted | m return cls(inverted) @@ -847,27 +873,27 @@ def __or__(self, other): cls = self.__class__ if not isinstance(other, (cls, int)): return NotImplemented - result = cls(self._value_ | cls(other)._value_) + result = cls(self.value | cls(other).value) return result def __and__(self, other): cls = self.__class__ if not isinstance(other, (cls, int)): return NotImplemented - return cls(self._value_ & cls(other)._value_) + return cls(self.value & cls(other).value) def __xor__(self, other): cls = self.__class__ if not isinstance(other, (cls, int)): return NotImplemented - return cls(self._value_ ^ cls(other)._value_) + return cls(self.value ^ cls(other).value) __ror__ = __or__ __rand__ = __and__ __rxor__ = __xor__ def __invert__(self): - result = self.__class__(~self._value_) + result = self.__class__(~self.value) return result @@ -909,7 +935,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_enum.py b/Lib/test/test_enum.py index c24321c343eca6..1f9d605ff7e50f 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 @@ -326,7 +328,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 @@ -1435,7 +1437,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 +1472,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 +1489,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 +1521,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 +1801,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 +2296,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 +2340,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 +2714,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 +2758,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 @@ -2997,7 +2999,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 @@ -3007,7 +3009,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, @@ -3027,7 +3029,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), @@ -3036,35 +3038,35 @@ 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), - '_convert was deprecated in 3.9') - def test_convert_warn(self): + '_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), '_convert was removed in 3.10') - def test_convert_raise(self): + @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), - '_convert was deprecated in 3.9') - def test_convert_warn(self): + '_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), '_convert was removed in 3.10') - def test_convert_raise(self): + @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 @@ -3072,5 +3074,42 @@ def test_convert_raise(self): _ = 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) + + if __name__ == '__main__': unittest.main() From eabb7e3783d27271fbf278983362af1d48f191d3 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Mon, 23 Dec 2019 01:20:49 +0300 Subject: [PATCH 17/23] Add 'name' and 'value' to Enum.__dir__ --- Lib/enum.py | 2 +- Lib/test/test_enum.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 7f9166d8bf592e..7880b45bb7d50d 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -667,7 +667,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 diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 1f9d605ff7e50f..b5a46dbd0ca390 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -187,7 +187,7 @@ def test_dir_on_item(self): Season = self.Season self.assertEqual( set(dir(Season.WINTER)), - set(['__class__', '__doc__', '__module__']), + set(['__class__', '__doc__', '__module__', 'name', 'value']), ) def test_dir_with_added_behavior(self): @@ -202,7 +202,7 @@ def wowser(self): ) self.assertEqual( set(dir(Test.this)), - set(['__class__', '__doc__', '__module__', 'wowser']), + set(['__class__', '__doc__', '__module__', 'wowser', 'name', 'value']), ) def test_dir_on_sub_with_behavior_on_super(self): @@ -214,7 +214,7 @@ class SubEnum(SuperEnum): sample = 5 self.assertEqual( set(dir(SubEnum.sample)), - set(['__class__', '__doc__', '__module__', 'invisible']), + set(['__class__', '__doc__', '__module__', 'invisible', 'name', 'value']), ) def test_enum_in_enum_out(self): From fab4c97a9b7fb06447480ae84c22da1d47add0f3 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Mon, 23 Dec 2019 11:02:44 +0300 Subject: [PATCH 18/23] Remove '_member_names' and return 'list(cls._unique_member_map_)' instead --- Lib/enum.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 7880b45bb7d50d..a284b29426aa9b 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -179,9 +179,8 @@ def __new__(metacls, cls, bases, 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_ = {} + enum_class._unique_member_map_ = {} # name->unique value map enum_class._member_type_ = member_type dynamic_attributes = {k: v for c in enum_class.mro() @@ -245,7 +244,6 @@ def __new__(metacls, cls, bases, classdict): else: # Aliases don't appear in member names (only in __members__). enum_class._unique_member_map_[member_name] = enum_member - enum_class._member_names.append(member_name) dynamic_attr = dynamic_attributes.get(member_name) if dynamic_attr is not None: @@ -479,7 +477,7 @@ def _member_names_(cls): '_member_names_ is deprecated and will be removed in 3.10, use ' '_unique_members_map_ instead.', DeprecationWarning, stacklevel=2 ) - return cls._member_names + return list(cls._unique_member_map_) @staticmethod def _get_mixins_(bases): From 54e1eff6784bc5896f0cdeea1e4641d0f6fba920 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Mon, 23 Dec 2019 11:24:49 +0300 Subject: [PATCH 19/23] Use builtin dict() instead of _EnumDict for class creation --- Lib/enum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/enum.py b/Lib/enum.py index a284b29426aa9b..883620051231a6 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -177,6 +177,8 @@ def __new__(metacls, cls, bases, classdict): 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_map_ = {} # name->value map From 378dc88edf4b753c562dff7b83b7155900a315ff Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Mon, 23 Dec 2019 21:14:25 +0300 Subject: [PATCH 20/23] Use f-strings instead of %s formatting --- Lib/enum.py | 69 +++++++++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 42 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 883620051231a6..83052b4d3eff2e 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__ = '' @@ -86,19 +86,19 @@ def __setitem__(self, key, value): self._ignore = value 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.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.members), list(self.members.values())) @@ -170,8 +170,7 @@ def __new__(metacls, cls, bases, classdict): # check for illegal enum names (any others?) 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: @@ -331,17 +330,15 @@ 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__)) + 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): @@ -369,7 +366,7 @@ def __members__(cls): return MappingProxyType(cls._member_map_) def __repr__(cls): - return "" % cls.__name__ + return f'" % ( - 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 = [ @@ -728,7 +723,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 @@ -751,7 +746,7 @@ 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 @@ -765,34 +760,26 @@ 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__)) + 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) + return f'<{cls.__name__}.{self.name}: {self.value!r}>' 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, - ) + return f"<{cls.__name__}.{'|'.join([str(m.name or m.value) for m in members])}: {self.value!r}>" def __str__(self): cls = self.__class__ if self.name is not None: - return '%s.%s' % (cls.__name__, self.name) + return f'{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) + return f'{cls.__name__}.{members[0].value!r}' else: - return '%s.%s' % ( - cls.__name__, - '|'.join([str(m.name or m.value) for m in members]), - ) + return f"{cls.__name__}.{'|'.join([str(m.name or m.value) for m in members])}" def __bool__(self): return bool(self.value) @@ -831,7 +818,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 @@ -908,10 +895,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): From ecd41fe93478f8478f3596519d50943060ae16c0 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Tue, 24 Dec 2019 12:56:24 +0300 Subject: [PATCH 21/23] Fix missing '>' in repr, add test for that --- Lib/enum.py | 2 +- Lib/test/test_enum.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/enum.py b/Lib/enum.py index 83052b4d3eff2e..ddb5bdb9e0bd8e 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -366,7 +366,7 @@ def __members__(cls): return MappingProxyType(cls._member_map_) def __repr__(cls): - return f'' def __reversed__(cls): return reversed(cls._unique_member_map_.values()) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index b5a46dbd0ca390..f1796eb4d8289b 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -236,6 +236,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)) From 948d3de1d6c6ba6b6b45b6952c13bd3a1540a3a0 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Tue, 24 Dec 2019 13:54:31 +0300 Subject: [PATCH 22/23] Add cache for repr, str and invert on Flag and IntFlag Benchmark: Testing with 10000 repeats, result is average of 100 tests: >>> str(~NewFlag.baz) # 5.6313761773697415 ms 'NewFlag.0' >>> str(~OldFlag.baz) # 146.97604789150913 ms 'OldFlag.0' >>> repr(~NewFlag.baz) # 4.422742834372443 ms '' >>> repr(~OldFlag.baz) # 151.49518529891247 ms '' >>> ~(~NewFlag.foo) # 3.465736084655711 ms >>> ~(~OldFlag.foo) # 177.88357201820338 ms NewFlag: total: 13.5198551 ms, average: 4.4194400 ms (Fastest) OldFlag: total: 476.3548052 ms, average: 158.2196475 ms, ~ x35.80 times slower than NewFlag --- Lib/enum.py | 47 +++++++++++++++++++++++++++++++++----- Lib/test/test_enum.py | 52 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index ddb5bdb9e0bd8e..4004cc51a251f8 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -191,6 +191,11 @@ def __new__(metacls, cls, bases, classdict): # 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 @@ -265,6 +270,15 @@ 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__'}: @@ -630,10 +644,16 @@ def _value_(self): @_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): @@ -768,18 +788,27 @@ def __repr__(self): cls = self.__class__ 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) - return f"<{cls.__name__}.{'|'.join([str(m.name or m.value) for m in members])}: {self.value!r}>" + 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 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: - return f'{cls.__name__}.{members[0].value!r}' + self._str_ = result = f'{cls.__name__}.{members[0].value!r}' else: - return f"{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) @@ -803,13 +832,18 @@ def __xor__(self, other): return cls(self.value ^ other.value) def __invert__(self): + 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 cls(inverted) + self._invert_ = result = cls(inverted) + result._invert_ = self + return result class IntFlag(int, Flag): @@ -880,7 +914,10 @@ def __xor__(self, other): __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 diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index f1796eb4d8289b..2a476dbd367878 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -16,6 +16,8 @@ # for pickle tests +from unittest import mock + try: class Stooges(Enum): LARRY = 1 @@ -3112,5 +3114,55 @@ def test_get_raise(self): 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() From a668e2a1b863f2d11994b5db81663e3e41a80020 Mon Sep 17 00:00:00 2001 From: MrMrRobat Date: Tue, 24 Dec 2019 13:55:53 +0300 Subject: [PATCH 23/23] Remove redundant name and value setattr --- Lib/enum.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 4004cc51a251f8..9f63c5820116a1 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -883,8 +883,6 @@ def _create_pseudo_member_(cls, value): pseudo_member = int.__new__(cls, value) 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)