Subclassing Python Enum with `auto()`, string values, and an extra property - python

I have an Enum subclass that I would like to do two things:
Use the lowercased name of the enum as the value for each item and a property called function. I don't want to have to type this out each time, so I'm using auto().
Optionally, allow the user to pass a custom function name that overrides the lowercased name.
Here is the code I have come up with to do that:
from enum import Enum, auto
class LowerNameEnum(Enum):
def _generate_next_value_(name, start, count, last_values):
print(f"{name=}")
return name.lower()
class Task(str, LowerNameEnum):
def __new__(cls, value, function=None):
obj = str.__new__(cls, value)
obj._value_ = value
obj.function = function or "generic_function"
return obj
DO_SOMETHING = auto()
DO_SOMETHING_ELSE = auto()
DO_ANOTHER_THING = auto(), "do_it_do_it"
THING_4 = auto()
for t in Task:
print(t)
print(" ", t.value)
print(" ", t.function)
But when I run this, I get the following:
$ python myenum.py
name='DO_SOMETHING'
name='DO_SOMETHING_ELSE'
name='THING_4'
Task.DO_SOMETHING
do_something
generic_function
Task.DO_SOMETHING_ELSE
do_something_else
generic_function
Task.DO_ANOTHER_THING
<enum.auto object at 0x103000160>
do_it_do_it
Task.THING_4
thing_4
generic_function
When I try to use the auto() value in combination with the extra argument, I get what looks like a __repr__ of an enum.auto object instead of the lowercased item name. This is because _generate_next_value_ isn't even called for this one, despite my having passed auto().
What am I doing wrong?
Update: Well, it turns out that value in the __new__ method for the third item is an enum.auto object. So that's how it's getting set as the value. I can use isinstance to capture this, which is good. But then my question becomes: how do I find out the name of the item at this point so I can pass it to _generate_next_value_? Or is there some better way to do this?

Until this is fixed (in 3.12), you'll need to use aenum:
from aenum import Enum
...
class Task(str, LowerNameEnum):
...
DO_ANOTHER_THING = auto(function="do_it_do_it")
Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

Related

Python Enums: substitute `element` with `element.value`

I have a simple Enum:
class E(Enum):
A = 'a'
B = 'b'
To access 'a' I must type E.A.value. However, the value is the only thing I need from the Enum object.
How to write an Enum, where 'a' could be accessed just by E.A?
Using an int as value was just an example. It should actually be a user-defined class.
If you mixin a class/type with the Enum, then simply accessing the member itself will give you a subtype of that type:
from enum import Enum
class MyClass:
def __init__(self, color):
self.color = color
class MyEnum(MyClass, Enum):
first = 'red'
second = 'green'
third = 'blue'
and in use:
>>> MyEnum.first
<MyEnum.first: 'red'>
>>> MyEnum.first.color
'red'
>>> type(MyEnum.first)
<enum 'MyEnum'>
>>> isinstance(MyEnum.first, MyClass)
True
Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.
I looked around a lot, and I couldn't find a good solution to this problem using the Enum class you're trying to use. However, if you're willing to eschew the idea of using the Enum as a superclass, you can kludge this together:
class Demo:
# something with a 'value' method in it
def __init__(self, val):
self.value = val
def custom_enum(cls):
# class decorator to get __getattribute__() to work properly
# this is necessary because __getattribute__() only exists as an instance method,
# and there seems to be no direct equivalent for class methods
return cls()
#custom_enum
class E:
# first, define our enumerated variables in a dict
_enums = {
'A': Demo('a'),
'B': Demo('b'),
'chicken': Demo('cluck')
}
# then, override __getattribute__() to first get the key from the dict,
# and return the .value property of it
def __getattribute__(self, key):
# because of the decorator, we can't call self._enums or else we get a RecursionError
# therefore, we need to implicitly subclass `object`, and then
# deliberately invoke object.__getattribute__ on self, to access _enums
my_enums = object.__getattribute__(self, '_enums')
return my_enums[key].value
Actually defining the values of your enumerable is as simple as editing the _enums dict. And once you've done so, it should work roughly as you want it to:
>>> E.A
'a'
>>> E.B
'b'
>>> E.chicken
'cluck'
From here you could modify the implementation however necessary (such as returning an AttributeError instead of a KeyError, for example, or overriding __setattr__() to make enum values non-settable, or whatever).

Custom enum creation in Python [duplicate]

This question already has an answer here:
Overriding Enum __call__ method
(1 answer)
Closed 4 years ago.
I can easily create a class like
class MyEnum(enum.Enum):
BOB = "bob"
RALPH = "ralph"
ETC = "etc"
Then I can assign variables by enum value:
a = MyEnum('bob')
However -- I want to assign variables by things that could be the correct value. I.e., I'd like to do
a = MyEnum('bob')
b = MyEnum('Bob')
c = MyEnum('BOB')
and have them all work, and all map to the same enum value.
Is there a way of doing this without making a factory method? I've currently defined a create method, so a = MyEnum.create('Bob') works, but I'd like things to be seamless.
The thing you are looking for is called _missing_ and is available in the stdlib as of Python3.6, and in aenum1 as of 2.0.
class MyEnum(Enum):
BOB = "bob"
RALPH = "ralph"
ETC = "etc"
#classmethod
def _missing_(cls, value):
for member in cls:
if member.value == value.lower():
return member
If _missing_ fails to return a MyEnum member then EnumMeta will raise an exception (so _missing_ doesn't have to worry about that part)2.
1 Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.
2 Thanks, Aran-Fey, for bringing that up.
This can be achieved by subclassing EnumMeta, which is the metaclass responsible for defining the __call__ method that's invoked by MyEnum('Bob').
import enum
class CaseInsensitiveEnum(enum.EnumMeta):
def __call__(self, string):
string = string.lower() # convert the string to lowercase
return super().__call__(string)
class MyEnum(enum.Enum, metaclass=CaseInsensitiveEnum):
BOB = "bob"
RALPH = "ralph"
ETC = "etc"
(Keep in mind that all the enum values have to be lowercase; i.e. BOB = 'Bob' would not work.)
Demonstration:
>>> MyEnum('Bob')
<MyEnum.BOB: 'bob'>
>>> MyEnum('Bob') is MyEnum.BOB
True

how to define constructor for Python's new NamedTuple type?

As you may know, this is most recent type of defining named tuples in python:
from typing import NamedTuple
class MyType(NamedTuple):
id: int = 0
name: str = 0
After defining the type, Python interpreter defines a default constructor getting id and name and you can instantiate a new object using your fields.
Now I want to initialise a new object using a string and within the function I parse it. How can I define another constructor without spoiling the good default ones?
How can I define another constructor without spoiling the good default ones?
You can't. Python classes can't have multiple __new__ methods (or, if you meant "initializer", __init__ methods), just one.
But there's an easy way to work around this: the alternate constructor idiom: you write a #classmethod that provides an alternate way to construct instances. There are plenty of examples in the standard library, like datetime.now and datetime.utcfromtimestamp. There are even a few examples in the basic builtin types, like int.from_bytes.
Here's how that works:
class MyType(NamedTuple):
id: int = 0
name: str = 0
#classmethod
def from_string(cls, string_to_parse):
id, name = … your parsing code here …
return cls(id, name)
This is, of course, the same thing you'd do with a collections.namedtuple subclass, a #dataclass, or a plain-old class that had too many different ways to construct it.
If you really want to, the other way to do it is to provide an ugly constructor with either keyword-only parameters, or parameters that have different meanings depending on what you pass. With NamedTuple, you'll have to either insert an extra class in the way, or monkeypatch the class after creation, because otherwise there's no documented way of getting at the default constructor implementation.
So:
class _MyType(NamedTuple):
id: int = 0
name: str = 0
class MyType(_MyType):
def __new__(cls, id: int=None, name: str=None, *, parseything: str=None):
if parseything:
if id is not None or str is not None:
raise TypeError("don't provide both")
id, name = … your parsing code here …
return super().__new__(cls, id, name)
… or, if you prefer monkeypatching:
class MyType(NamedTuple):
id: int = 0
name: str = 0
_new = MyType.__new__
def __new__(cls, id=None, name=None, *, parseything=None):
if parseything:
if id is not None or str is not None:
raise TypeError("don't provide both")
id, name = … your parsing code here …
return _new(cls, id, name)
MyType.__new__ = __new__
del _new
del __new__
… or, if you want more of a range-style ugly API you can do either of the above with:
def __new__(cls, id_or_parsey_thing: Union[int,str]=None,
name: str=None):
if isinstance(id_or_parsey_thing, str):
if name is not None:
raise TypeError("don't provide both")
id, name = … your parsing code here …
else:
id = id_or_parsey_thing
# super().__new__ or _new here
Yes since Python 3.6 there is a new alternative for namedtuple - NamedTuple. Thanks to variable annotations now it is possible. So, if you previously wrote something like:
MyType = namedtuple('MyType', ('a', 'b', 'c'))
Now you can define it as follows.
To add a new constructor just define a classmethod:
from typing import NamedTuple
class MyType(NamedTuple):
a: str
b: int
c: float
#classmethod
def from_string(cls, s):
a, b, c = s.split()
return cls(a, int(b), float(c))
print(MyType.from_string('1 2 3'))

When should I subclass EnumMeta instead of Enum?

In this article Nick Coghlan talks about some of the design decisions that went in to the PEP 435 Enum type, and how EnumMeta can be subclassed to provide a different Enum experience.
However, the advice I give (and I am the primary stdlib Enum author) about using a metaclass is it should not be done without a really good reason -- such as not being able to accomplish what you need with a class decorator, or a dedicated function to hide any ugliness; and in my own work I've been able to do whatever I needed simply by using __new__, __init__, and/or normal class/instance methods when creating the Enum class:
Enum with attributes
Handling missing members
class constants that are not Enum members
And then there is this cautionary tale of being careful when delving into Enum, with and without metaclass subclassing:
Is it possible to override __new__ in an enum to parse strings to an instance?
Given all that, when would I need to fiddle with EnumMeta itself?
The best cases I have seen so far for subclassing EnumMeta comes from these four questions:
A more pythonic way to define an enum with dynamic members
Prevent invalid enum attribute assignment
Create an abstract Enum class
Invoke a function when an enum member is accessed
We'll examine the dynamic member case further here.
First, a look at the code needed when not subclassing EnumMeta:
The stdlib way
from enum import Enum
import json
class BaseCountry(Enum):
def __new__(cls, record):
member = object.__new__(cls)
member.country_name = record['name']
member.code = int(record['country-code'])
member.abbr = record['alpha-2']
member._value_ = member.abbr, member.code, member.country_name
if not hasattr(cls, '_choices'):
cls._choices = {}
cls._choices[member.code] = member.country_name
cls._choices[member.abbr] = member.country_name
return member
def __str__(self):
return self.country_name
Country = BaseCountry(
'Country',
[(rec['alpha-2'], rec) for rec in json.load(open('slim-2.json'))],
)
The aenum way 1 2
from aenum import Enum, MultiValue
import json
class Country(Enum, init='abbr code country_name', settings=MultiValue):
_ignore_ = 'country this' # do not add these names as members
# create members
this = vars()
for country in json.load(open('slim-2.json')):
this[country['alpha-2']] = (
country['alpha-2'],
int(country['country-code']),
country['name'],
)
# have str() print just the country name
def __str__(self):
return self.country_name
The above code is fine for a one-off enumeration -- but what if creating Enums from JSON files was common for you? Imagine if you could do this instead:
class Country(JSONEnum):
_init_ = 'abbr code country_name' # remove if not using aenum
_file = 'some_file.json'
_name = 'alpha-2'
_value = {
1: ('alpha-2', None),
2: ('country-code', lambda c: int(c)),
3: ('name', None),
}
As you can see:
_file is the name of the json file to use
_name is the path to whatever should be used for the name
_value is a dictionary mapping paths to values3
_init_ specifies the attribute names for the different value components (if using aenum)
The JSON data is taken from https://github.com/lukes/ISO-3166-Countries-with-Regional-Codes -- here is a short excerpt:
[{"name":"Afghanistan","alpha-2":"AF","country-code":"004"},
{"name":"Åland Islands","alpha-2":"AX","country-code":"248"},
{"name":"Albania","alpha-2":"AL","country-code":"008"},
{"name":"Algeria","alpha-2":"DZ","country-code":"012"}]
Here is the JSONEnumMeta class:
class JSONEnumMeta(EnumMeta):
#classmethod
def __prepare__(metacls, cls, bases, **kwds):
# return a standard dictionary for the initial processing
return {}
def __init__(cls, *args , **kwds):
super(JSONEnumMeta, cls).__init__(*args)
def __new__(metacls, cls, bases, clsdict, **kwds):
import json
members = []
missing = [
name
for name in ('_file', '_name', '_value')
if name not in clsdict
]
if len(missing) in (1, 2):
# all three must be present or absent
raise TypeError('missing required settings: %r' % (missing, ))
if not missing:
# process
name_spec = clsdict.pop('_name')
if not isinstance(name_spec, (tuple, list)):
name_spec = (name_spec, )
value_spec = clsdict.pop('_value')
file = clsdict.pop('_file')
with open(file) as f:
json_data = json.load(f)
for data in json_data:
values = []
name = data[name_spec[0]]
for piece in name_spec[1:]:
name = name[piece]
for order, (value_path, func) in sorted(value_spec.items()):
if not isinstance(value_path, (list, tuple)):
value_path = (value_path, )
value = data[value_path[0]]
for piece in value_path[1:]:
value = value[piece]
if func is not None:
value = func(value)
values.append(value)
values = tuple(values)
members.append(
(name, values)
)
# get the real EnumDict
enum_dict = super(JSONEnumMeta, metacls).__prepare__(cls, bases, **kwds)
# transfer the original dict content, _items first
items = list(clsdict.items())
items.sort(key=lambda p: (0 if p[0][0] == '_' else 1, p))
for name, value in items:
enum_dict[name] = value
# add the members
for name, value in members:
enum_dict[name] = value
return super(JSONEnumMeta, metacls).__new__(metacls, cls, bases, enum_dict, **kwds)
# for use with both Python 2/3
JSONEnum = JSONEnumMeta('JsonEnum', (Enum, ), {})
A few notes:
JSONEnumMeta.__prepare__ returns a normal dict
EnumMeta.__prepare__ is used to get an instance of _EnumDict -- this is the proper way to get one
keys with a leading underscore are passed to the real _EnumDict first as they may be needed when processing the enum members
Enum members are in the same order as they were in the file
1 Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.
2 This requires aenum 2.0.5+.
3 The keys are numeric to keep multiple values in order should your Enum need more than one.

Python 3 Enums with Function Values

I noticed an oddity in the Python 3 Enums (link).
If you set the value of an Enum to a function, it prevents the attribute from being wrapped as an Enum object, which prevents you from being able to use the cool features like EnumCls['AttrName'] to dynamically load the attribute.
Is this a bug? Done on purpose?
I searched for a while but found no mention of restricted values that you can use in an Enum.
Here is sample code that displays the issue:
class Color(Enum):
Red = lambda: print('In Red')
Blue = lambda: print('In Blue')
print(Color.Red) # <function> - should be Color.Red via Docs
print(Color.Blue) # <function> - should be Color.Bluevia Docs
print(Color['Red']) # throws KeyError - should be Color.Red via Docs
Also, this is my first time asking, so let me know if there's anything I should be doing differently! And thanks for the help!
You can override the __call__ method:
from enum import Enum, auto
class Color(Enum):
red = auto()
blue = auto()
def __call__(self, *args, **kwargs):
return f'<font color={self.name}>{args[0]}</font>'
Can then be used:
>>> Color.red('flowers')
<font color=red>flowers</font>
The documentation says:
The rules for what is allowed are as follows: _sunder_ names (starting and ending with a single underscore) are reserved by enum and cannot be used; all other attributes defined within an enumeration will become members of this enumeration, with the exception of __dunder__ names and descriptors (methods are also descriptors).
A "method" is just a function defined inside a class body. It doesn't matter whether you define it with lambda or def. So your example is the same as:
class Color(Enum):
def Red():
print('In Red')
def Blue():
print('In Blue')
In other words, your purported enum values are actually methods, and so won't become members of the Enum.
If someone need/want to use Enum with functions as values, its possible to do so by using a callable object as a proxy, something like this:
class FunctionProxy:
"""Allow to mask a function as an Object."""
def __init__(self, function):
self.function = function
def __call__(self, *args, **kwargs):
return self.function(*args, **kwargs)
A simple test:
from enum import Enum
class Functions(Enum):
Print_Function = FunctionProxy(lambda *a: print(*a))
Split_Function = FunctionProxy(lambda s, d='.': s.split(d))
Functions.Print_Function.value('Hello World!')
# Hello World!
Functions.Split_Function.value('Hello.World.!')
# ['Hello', 'World', '!']
You can also use functools.partial to trick the enum into not considering your function a method of Color:
from functools import partial
from enum import Enum
class Color(Enum):
Red = partial(lambda: print('In Red'))
Blue = partial(lambda: print('In Blue'))
With this you can access name and value as expected.
Color.Red
Out[17]: <Color.Red: functools.partial(<function Color.<lambda> at 0x7f84ad6303a0>)>
Color.Red.name
Out[18]: 'Red'
Color.Red.value()
In Red
I ran into this issue recently, found this post, and first was tempted to use the wrapper pattern suggested in the other related post. However eventually I found out that this was a bit overkill for what I had to do. In the past years this happened to me several times with Enum, so I would like to share this simple experience feedback:
if you need an enumeration, ask yourself whether you actually need an enum or just a namespace.
The difference is simple: Enum members are instances of their host enum class, while namespace members are completely independent from the class, they are just located inside.
Here is an example of namespace containing callables, with a get method to return any of them by name.
class Foo(object):
""" A simple namespace class with a `get` method to access members """
#classmethod
def get(cls, member_name: str):
"""Get a member by name"""
if not member_name.startswith('__') and member_name != 'get':
try:
return getattr(cls, member_name)
except AttributeError:
pass
raise ValueError("Unknown %r member: %r" % (cls.__name__, member_name))
# -- the "members" --
a = 1
#staticmethod
def welcome(name):
return "greetings, %s!" % name
#staticmethod
def wave(name):
return "(silently waving, %s)" % name
w = Foo.get('welcome')
a = Foo.get('a')
Foo.get('unknown') # ValueError: Unknown 'Foo' member: 'unknown'
See also this post on namespaces.
Initially, I thought your issue was just missing commas because I got the output you were expecting.:
from enum import Enum
class Color(Enum):
Red = lambda: print('In Red'),
Blue = lambda: print('In Blue'),
print(Color.Red)
print(Color.Blue)
print(Color['Red'])
output (python3.7)
$ /usr/local/opt/python/bin/python3.7 ~/test_enum.py
Color.Red
Color.Blue
Color.Red
#BernBarn was kind enough to explain that in my solution that a tuple is being created, and to invoke the function would require dereferencing value[0]. There is already another answer using value[0] in this way. I miss rb for this.

Categories