gh-112328: [Enum] Make some private attributes public. (GH-112514)

* [Enum] Make some private attributes public.

- ``_EnumDict`` --> ``EnumDict``
- ``EnumDict._member_names`` --> ``EnumDict.member_names``
- ``Enum._add_alias_``
- ``Enum._add_value_alias_``

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
This commit is contained in:
Ethan Furman 2023-12-05 08:27:36 -08:00 committed by GitHub
parent 563ccded6e
commit de6bca9564
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 301 additions and 117 deletions

View file

@ -4,7 +4,7 @@ from types import MappingProxyType, DynamicClassAttribute
__all__ = [
'EnumType', 'EnumMeta',
'EnumType', 'EnumMeta', 'EnumDict',
'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum',
'auto', 'unique', 'property', 'verify', 'member', 'nonmember',
'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP',
@ -313,45 +313,8 @@ class _proto_member:
):
# no other instances found, record this member in _member_names_
enum_class._member_names_.append(member_name)
# if necessary, get redirect in place and then add it to _member_map_
found_descriptor = None
descriptor_type = None
class_type = None
for base in enum_class.__mro__[1:]:
attr = base.__dict__.get(member_name)
if attr is not None:
if isinstance(attr, (property, DynamicClassAttribute)):
found_descriptor = attr
class_type = base
descriptor_type = 'enum'
break
elif _is_descriptor(attr):
found_descriptor = attr
descriptor_type = descriptor_type or 'desc'
class_type = class_type or base
continue
else:
descriptor_type = 'attr'
class_type = base
if found_descriptor:
redirect = property()
redirect.member = enum_member
redirect.__set_name__(enum_class, member_name)
if descriptor_type in ('enum','desc'):
# earlier descriptor found; copy fget, fset, fdel to this one.
redirect.fget = getattr(found_descriptor, 'fget', None)
redirect._get = getattr(found_descriptor, '__get__', None)
redirect.fset = getattr(found_descriptor, 'fset', None)
redirect._set = getattr(found_descriptor, '__set__', None)
redirect.fdel = getattr(found_descriptor, 'fdel', None)
redirect._del = getattr(found_descriptor, '__delete__', None)
redirect._attr_type = descriptor_type
redirect._cls_type = class_type
setattr(enum_class, member_name, redirect)
else:
setattr(enum_class, member_name, enum_member)
# now add to _member_map_ (even aliases)
enum_class._member_map_[member_name] = enum_member
enum_class._add_member_(member_name, enum_member)
try:
# This may fail if value is not hashable. We can't add the value
# to the map, and by-value lookups for this value will be
@ -360,9 +323,10 @@ class _proto_member:
except TypeError:
# keep track of the value in a list so containment checks are quick
enum_class._unhashable_values_.append(value)
enum_class._unhashable_values_map_.setdefault(member_name, []).append(value)
class _EnumDict(dict):
class EnumDict(dict):
"""
Track enum member order and ensure member names are not reused.
@ -371,7 +335,7 @@ class _EnumDict(dict):
"""
def __init__(self):
super().__init__()
self._member_names = {} # use a dict to keep insertion order
self._member_names = {} # use a dict -- faster look-up than a list, and keeps insertion order since 3.7
self._last_values = []
self._ignore = []
self._auto_called = False
@ -393,6 +357,7 @@ class _EnumDict(dict):
'_order_',
'_generate_next_value_', '_numeric_repr_', '_missing_', '_ignore_',
'_iter_member_', '_iter_member_by_value_', '_iter_member_by_def_',
'_add_alias_', '_add_value_alias_',
):
raise ValueError(
'_sunder_ names, such as %r, are reserved for future Enum use'
@ -468,6 +433,10 @@ class _EnumDict(dict):
self._last_values.append(value)
super().__setitem__(key, value)
@property
def member_names(self):
return list(self._member_names)
def update(self, members, **more_members):
try:
for name in members.keys():
@ -478,6 +447,8 @@ class _EnumDict(dict):
for name, value in more_members.items():
self[name] = value
_EnumDict = EnumDict # keep private name for backwards compatibility
class EnumType(type):
"""
@ -489,7 +460,7 @@ class EnumType(type):
# check that previous enum members do not exist
metacls._check_for_existing_members_(cls, bases)
# create the namespace dict
enum_dict = _EnumDict()
enum_dict = EnumDict()
enum_dict._cls_name = cls
# inherit previous flags and _generate_next_value_ function
member_type, first_enum = metacls._get_mixins_(cls, bases)
@ -552,6 +523,7 @@ class EnumType(type):
classdict['_member_map_'] = {}
classdict['_value2member_map_'] = {}
classdict['_unhashable_values_'] = []
classdict['_unhashable_values_map_'] = {}
classdict['_member_type_'] = member_type
# now set the __repr__ for the value
classdict['_value_repr_'] = metacls._find_data_repr_(cls, bases)
@ -754,7 +726,10 @@ class EnumType(type):
"""
if isinstance(value, cls):
return True
return value in cls._value2member_map_ or value in cls._unhashable_values_
try:
return value in cls._value2member_map_
except TypeError:
return value in cls._unhashable_values_
def __delattr__(cls, attr):
# nicer error message when someone tries to delete an attribute
@ -1050,7 +1025,57 @@ class EnumType(type):
else:
use_args = True
return __new__, save_new, use_args
EnumMeta = EnumType
def _add_member_(cls, name, member):
# _value_ structures are not updated
if name in cls._member_map_:
if cls._member_map_[name] is not member:
raise NameError('%r is already bound: %r' % (name, cls._member_map_[name]))
return
#
# if necessary, get redirect in place and then add it to _member_map_
found_descriptor = None
descriptor_type = None
class_type = None
for base in cls.__mro__[1:]:
attr = base.__dict__.get(name)
if attr is not None:
if isinstance(attr, (property, DynamicClassAttribute)):
found_descriptor = attr
class_type = base
descriptor_type = 'enum'
break
elif _is_descriptor(attr):
found_descriptor = attr
descriptor_type = descriptor_type or 'desc'
class_type = class_type or base
continue
else:
descriptor_type = 'attr'
class_type = base
if found_descriptor:
redirect = property()
redirect.member = member
redirect.__set_name__(cls, name)
if descriptor_type in ('enum', 'desc'):
# earlier descriptor found; copy fget, fset, fdel to this one.
redirect.fget = getattr(found_descriptor, 'fget', None)
redirect._get = getattr(found_descriptor, '__get__', None)
redirect.fset = getattr(found_descriptor, 'fset', None)
redirect._set = getattr(found_descriptor, '__set__', None)
redirect.fdel = getattr(found_descriptor, 'fdel', None)
redirect._del = getattr(found_descriptor, '__delete__', None)
redirect._attr_type = descriptor_type
redirect._cls_type = class_type
setattr(cls, name, redirect)
else:
setattr(cls, name, member)
# now add to _member_map_ (even aliases)
cls._member_map_[name] = member
#
cls._member_map_[name] = member
EnumMeta = EnumType # keep EnumMeta name for backwards compatibility
class Enum(metaclass=EnumType):
@ -1116,9 +1141,9 @@ class Enum(metaclass=EnumType):
pass
except TypeError:
# not there, now do long search -- O(n) behavior
for member in cls._member_map_.values():
if member._value_ == value:
return member
for name, values in cls._unhashable_values_map_.items():
if value in values:
return cls[name]
# still not found -- verify that members exist, in-case somebody got here mistakenly
# (such as via super when trying to override __new__)
if not cls._member_map_:
@ -1159,6 +1184,33 @@ class Enum(metaclass=EnumType):
def __init__(self, *args, **kwds):
pass
def _add_alias_(self, name):
self.__class__._add_member_(name, self)
def _add_value_alias_(self, value):
cls = self.__class__
try:
if value in cls._value2member_map_:
if cls._value2member_map_[value] is not self:
raise ValueError('%r is already bound: %r' % (value, cls._value2member_map_[value]))
return
except TypeError:
# unhashable value, do long search
for m in cls._member_map_.values():
if m._value_ == value:
if m is not self:
raise ValueError('%r is already bound: %r' % (value, cls._value2member_map_[value]))
return
try:
# This may fail if value is not hashable. We can't add the value
# to the map, and by-value lookups for this value will be
# linear.
cls._value2member_map_.setdefault(value, self)
except TypeError:
# keep track of the value in a list so containment checks are quick
cls._unhashable_values_.append(value)
cls._unhashable_values_map_.setdefault(self.name, []).append(value)
@staticmethod
def _generate_next_value_(name, start, count, last_values):
"""
@ -1671,7 +1723,8 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
body['_member_names_'] = member_names = []
body['_member_map_'] = member_map = {}
body['_value2member_map_'] = value2member_map = {}
body['_unhashable_values_'] = []
body['_unhashable_values_'] = unhashable_values = []
body['_unhashable_values_map_'] = {}
body['_member_type_'] = member_type = etype._member_type_
body['_value_repr_'] = etype._value_repr_
if issubclass(etype, Flag):
@ -1718,14 +1771,9 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
for name, value in attrs.items():
if isinstance(value, auto) and auto.value is _auto_null:
value = gnv(name, 1, len(member_names), gnv_last_values)
if value in value2member_map:
if value in value2member_map or value in unhashable_values:
# an alias to an existing member
member = value2member_map[value]
redirect = property()
redirect.member = member
redirect.__set_name__(enum_class, name)
setattr(enum_class, name, redirect)
member_map[name] = member
enum_class(value)._add_alias_(name)
else:
# create the member
if use_args:
@ -1740,12 +1788,12 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
member._name_ = name
member.__objclass__ = enum_class
member.__init__(value)
redirect = property()
redirect.member = member
redirect.__set_name__(enum_class, name)
setattr(enum_class, name, redirect)
member_map[name] = member
member._sort_order_ = len(member_names)
if name not in ('name', 'value'):
setattr(enum_class, name, member)
member_map[name] = member
else:
enum_class._add_member_(name, member)
value2member_map[value] = member
if _is_single_bit(value):
# not a multi-bit alias, record in _member_names_ and _flag_mask_
@ -1768,14 +1816,13 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
if value.value is _auto_null:
value.value = gnv(name, 1, len(member_names), gnv_last_values)
value = value.value
if value in value2member_map:
try:
contained = value in value2member_map
except TypeError:
contained = value in unhashable_values
if contained:
# an alias to an existing member
member = value2member_map[value]
redirect = property()
redirect.member = member
redirect.__set_name__(enum_class, name)
setattr(enum_class, name, redirect)
member_map[name] = member
enum_class(value)._add_alias_(name)
else:
# create the member
if use_args:
@ -1791,14 +1838,22 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
member.__objclass__ = enum_class
member.__init__(value)
member._sort_order_ = len(member_names)
redirect = property()
redirect.member = member
redirect.__set_name__(enum_class, name)
setattr(enum_class, name, redirect)
member_map[name] = member
value2member_map[value] = member
if name not in ('name', 'value'):
setattr(enum_class, name, member)
member_map[name] = member
else:
enum_class._add_member_(name, member)
member_names.append(name)
gnv_last_values.append(value)
try:
# This may fail if value is not hashable. We can't add the value
# to the map, and by-value lookups for this value will be
# linear.
enum_class._value2member_map_.setdefault(value, member)
except TypeError:
# keep track of the value in a list so containment checks are quick
enum_class._unhashable_values_.append(value)
enum_class._unhashable_values_map_.setdefault(name, []).append(value)
if '__new__' in body:
enum_class.__new_member__ = enum_class.__new__
enum_class.__new__ = Enum.__new__