Ability to customize how Enum interprets values with access to names
Here's a working example of what I'm talking about. In this example, `EnumX` is a tweak of `Enum` to that has the capability that I'm talking about, and I have also pasted the code for `EnumX` later in this email. Please note that the important thing in this example is NOT what this `ChoiceEnum` class does but the ability to create custom enums that can do custom processing of assigned value/key such as what `ChoiceEnum` does, using a documented feature of `Enum`, without having to either write and debug a complicated hacks (that might not work with future updates to `Enum`) or replace hunks of `Enum` (as my `EnumX` shown here does) in ways that might not work or might not be feature-complete with future `Enum` updates. # Example usage of enhanced Enum with _preprocess_value_ support. def _label_from_key(key): return key.replace('_', ' ').title() class _ChoiceValue(str): """Easy way to have an object with immutable string content that is identified for equality by its content with a label attribute that is not significant for equality. """ def __new__(cls, content, label): obj = str.__new__(cls, content) obj.label = label return obj def __repr__(self): content_repr = str.__repr__(self) return '%s(%r, %r)' % ( self.__class__.__name__, content_repr, self.label) def get_value_label_pair(self): return (f'{self}', self.label) class ChoiceEnum(EnumX): def _preprocess_value_(key, src): value = None label = None if src == (): pass elif isinstance(src, tuple) and src != (): value, label = src elif isinstance(str, str) and src.startswith(','): label = src[1:] else: value = src if value is None: value = key label = label or _label_from_key(key) return _ChoiceValue(value, label) def __getitem__(self, key): return (f'{self._value_}', self._value_.label).__getitem__(key) def __len__(self): return 2 @property def value(self): return self._value_.get_value_label_pair() @property def label(self): return self._value_.label class Food(ChoiceEnum): APPLE = () CHEESE = () HAMBURGER = 'BURGER' SOUFFLE = ',Soufflé' CHICKEN_MCNUGGETS = ('CHX_MCNUG', 'Chicken McNuggets') DEFAULT = 'APPLE' for food in Food: print(repr(food)) # Prints... # <Food.APPLE: _ChoiceValue("'APPLE'", 'Apple')> # <Food.CHEESE: _ChoiceValue("'CHEESE'", 'Cheese')> # <Food.HAMBURGER: _ChoiceValue("'BURGER'", 'Hamburger')> # <Food.SOUFFLE: _ChoiceValue("',Soufflé'", 'Souffle')> # <Food.CHICKEN_MCNUGGETS: _ChoiceValue("'CHX_MCNUG'", 'Chicken McNuggets')> print(f'Food.DEFAULT is Food.APPLE: {Food.DEFAULT is Food.APPLE}') # Prints... # Food.DEFAULT is Food.APPLE: True Here's the implementation of `EnumX` that is being used for the above. There are just a handful of those lines that represent changes to the implementation at https://github.com/python/cpython/blob/3.8/Lib/enum.py and I there are inline comments to draw attention to those. from enum import ( Enum, EnumMeta, auto, _is_sunder, _is_dunder, _is_descriptor, _auto_null) # Copy of EnumDict with tweaks to support _preprocess_value_ class _EnumDictX(dict): def __init__(self): super().__init__() self._member_names = [] self._last_values = [] self._ignore = [] def __setitem__(self, key, value): """Duplicate all of _EnumDict.__setitem__ in order to insert a hook """ if _is_sunder(key): if key not in ( '_order_', '_create_pseudo_member_', '_generate_next_value_', '_missing_', '_ignore_', '_preprocess_value_', # <-- ): raise ValueError('_names_ are reserved for future Enum use') if key == '_generate_next_value_': setattr(self, '_generate_next_value', value) # ==================== if key == '_preprocess_value_': setattr(self, '_preprocess_value', value) # ==================== elif key == '_ignore_': if isinstance(value, str): value = value.replace(',', ' ').split() else: value = list(value) self._ignore = value already = set(value) & set(self._member_names) 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: # descriptor overwriting an enum? raise TypeError('Attempted to reuse key: %r' % key) 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])) # ==================== value = self._preprocess_value(key, value) # ==================== if isinstance(value, auto): if value.value == _auto_null: value.value = self._generate_next_value( key, 1, len(self._member_names), self._last_values[:]) value = value.value self._member_names.append(key) self._last_values.append(value) dict.__setitem__(self, key, value) # Subclass of EnumMeta with tweak to support _preprocess_value_ class EnumMetaX(EnumMeta): # Copy of EnumMeta.__prepare__ with tweak to support _preprocess_value_ @classmethod def __prepare__(metacls, cls, bases): # create the namespace dict enum_dict = _EnumDictX() # inherit previous flags and _generate_next_value_ function member_type, first_enum = metacls._get_mixins_(bases) if first_enum is not None: # ==================== enum_dict['_preprocess_value_'] = getattr( first_enum, '_preprocess_value_', None) # ==================== enum_dict['_generate_next_value_'] = getattr(first_enum, '_generate_next_value_', None) return enum_dict # Subclass of Enum using EnumMetaX as metaclass and with default # implementation of _preprocess_value_. class EnumX(Enum, metaclass=EnumMetaX): def _preprocess_value_(key, value): return value
I do agree that this is a worthwhile feature-ish, but it's marred by the tuples-for-values assumption. We've build very much the same thing in tri.token (https://github.com/TriOptima/tri.token) but tri.token is much more scalable to many arguments because it uses keyword arguments and not positional arguments. We have "enums" that declare rich static data with more than ten arguments. This just doesn't work for positional arguments.
On 28 Oct 2019, at 03:13, Steve Jorgensen <stevej@stevej.name> wrote:
Here's a working example of what I'm talking about. In this example, `EnumX` is a tweak of `Enum` to that has the capability that I'm talking about, and I have also pasted the code for `EnumX` later in this email.
Please note that the important thing in this example is NOT what this `ChoiceEnum` class does but the ability to create custom enums that can do custom processing of assigned value/key such as what `ChoiceEnum` does, using a documented feature of `Enum`, without having to either write and debug a complicated hacks (that might not work with future updates to `Enum`) or replace hunks of `Enum` (as my `EnumX` shown here does) in ways that might not work or might not be feature-complete with future `Enum` updates.
# Example usage of enhanced Enum with _preprocess_value_ support.
def _label_from_key(key): return key.replace('_', ' ').title()
class _ChoiceValue(str): """Easy way to have an object with immutable string content that is identified for equality by its content with a label attribute that is not significant for equality. """ def __new__(cls, content, label): obj = str.__new__(cls, content) obj.label = label return obj
def __repr__(self): content_repr = str.__repr__(self) return '%s(%r, %r)' % ( self.__class__.__name__, content_repr, self.label)
def get_value_label_pair(self): return (f'{self}', self.label)
class ChoiceEnum(EnumX): def _preprocess_value_(key, src): value = None label = None if src == (): pass elif isinstance(src, tuple) and src != (): value, label = src elif isinstance(str, str) and src.startswith(','): label = src[1:] else: value = src if value is None: value = key label = label or _label_from_key(key) return _ChoiceValue(value, label)
def __getitem__(self, key): return (f'{self._value_}', self._value_.label).__getitem__(key)
def __len__(self): return 2
@property def value(self): return self._value_.get_value_label_pair()
@property def label(self): return self._value_.label
class Food(ChoiceEnum): APPLE = () CHEESE = () HAMBURGER = 'BURGER' SOUFFLE = ',Soufflé' CHICKEN_MCNUGGETS = ('CHX_MCNUG', 'Chicken McNuggets') DEFAULT = 'APPLE'
for food in Food: print(repr(food)) # Prints... # <Food.APPLE: _ChoiceValue("'APPLE'", 'Apple')> # <Food.CHEESE: _ChoiceValue("'CHEESE'", 'Cheese')> # <Food.HAMBURGER: _ChoiceValue("'BURGER'", 'Hamburger')> # <Food.SOUFFLE: _ChoiceValue("',Soufflé'", 'Souffle')> # <Food.CHICKEN_MCNUGGETS: _ChoiceValue("'CHX_MCNUG'", 'Chicken McNuggets')>
print(f'Food.DEFAULT is Food.APPLE: {Food.DEFAULT is Food.APPLE}') # Prints... # Food.DEFAULT is Food.APPLE: True
Here's the implementation of `EnumX` that is being used for the above. There are just a handful of those lines that represent changes to the implementation at https://github.com/python/cpython/blob/3.8/Lib/enum.py and I there are inline comments to draw attention to those.
from enum import ( Enum, EnumMeta, auto, _is_sunder, _is_dunder, _is_descriptor, _auto_null)
# Copy of EnumDict with tweaks to support _preprocess_value_ class _EnumDictX(dict): def __init__(self): super().__init__() self._member_names = [] self._last_values = [] self._ignore = []
def __setitem__(self, key, value): """Duplicate all of _EnumDict.__setitem__ in order to insert a hook """ if _is_sunder(key): if key not in ( '_order_', '_create_pseudo_member_', '_generate_next_value_', '_missing_', '_ignore_', '_preprocess_value_', # <-- ): raise ValueError('_names_ are reserved for future Enum use') if key == '_generate_next_value_': setattr(self, '_generate_next_value', value) # ==================== if key == '_preprocess_value_': setattr(self, '_preprocess_value', value) # ==================== elif key == '_ignore_': if isinstance(value, str): value = value.replace(',', ' ').split() else: value = list(value) self._ignore = value already = set(value) & set(self._member_names) 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: # descriptor overwriting an enum? raise TypeError('Attempted to reuse key: %r' % key) 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])) # ==================== value = self._preprocess_value(key, value) # ==================== if isinstance(value, auto): if value.value == _auto_null: value.value = self._generate_next_value( key, 1, len(self._member_names), self._last_values[:]) value = value.value self._member_names.append(key) self._last_values.append(value) dict.__setitem__(self, key, value)
# Subclass of EnumMeta with tweak to support _preprocess_value_ class EnumMetaX(EnumMeta): # Copy of EnumMeta.__prepare__ with tweak to support _preprocess_value_ @classmethod def __prepare__(metacls, cls, bases): # create the namespace dict enum_dict = _EnumDictX() # inherit previous flags and _generate_next_value_ function member_type, first_enum = metacls._get_mixins_(bases) if first_enum is not None: # ==================== enum_dict['_preprocess_value_'] = getattr( first_enum, '_preprocess_value_', None) # ==================== enum_dict['_generate_next_value_'] = getattr(first_enum, '_generate_next_value_', None) return enum_dict
# Subclass of Enum using EnumMetaX as metaclass and with default # implementation of _preprocess_value_. class EnumX(Enum, metaclass=EnumMetaX): def _preprocess_value_(key, value): return value _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/INFWIW... Code of Conduct: http://python.org/psf/codeofconduct/
Anders Hovmöller wrote:
I do agree that this is a worthwhile feature-ish, but it's marred by the tuples-for-values assumption. We've build very much the same thing in tri.token (https://github.com/TriOptima/tri.token) but tri.token is much more scalable to many arguments because it uses keyword arguments and not positional arguments. We have "enums" that declare rich static data with more than ten arguments. This just doesn't work for positional arguments.
It is only my example of a possible usage of the concept uses uses tuples for values though. The fundamental concept is simply to have a hook that can accept key & value as params and may return a different value. The choice of what kind of values to supply and how to transform them would be the decision of the developer of any particular `Enum`–derived class.
participants (2)
-
Anders Hovmöller
-
Steve Jorgensen