How to construct a case insensitive enum?
Question:
I have a simple Python 2.7 enum:
from enum import Enum
class Label(enum):
RedApple = 1
GreenApple = 2
I would like to be able create enum objects using case-insensitive keys:
fruitname = "redapple"
a = Label[fruitname]
I’ve tried to create an __init__
method:
def __init__(self, key):
super(Label, self).__init__()
pass # do comparison here
but I keep running into errors:
super(Label, self).__init__()
NameError: global name 'Label' is not defined
I’d like to do a comparison on key.lower().strip()
. Is this even possible?
Answers:
In Python 3.6 and aenum 2.0
12 (which is compatible with 2.7 and 3.0+) a new method has been added: _missing_
2.
This method is called just before a ValueError
is raised to give custom code a chance to try and find the enum member by value. Unfortunately, this makes it unsuited for your use-case — looking up by name.
Fortunately, aenum
has the extra option of defining a _missing_name_
method3 which will be called when name lookup fails.
Your code above should look like this:
from aenum import Enum
class Label(Enum):
RedApple = 1
GreenApple = 2
@classmethod
def _missing_name_(cls, name):
for member in cls:
if member.name.lower() == name.lower():
return member
And in use:
>>> Label['redapple']
<Label.RedApple: 1>
If stuck using the 3.6 stdlib (or want to stay compatible with it) you can (ab)use _missing_
but:
- you will have to do
Label('redapple')
instead (round parens, not square brackets), and
- you will be working against the design of enum (‘redapple’ is the name, not the value)
1 Disclosure: I am the author of the Python stdlib Enum
, the enum34
backport, and the Advanced Enumeration (aenum
) library.
2 enum34
does not have these improvements as it is only being maintained for bug fixes.
3 _missing_value_
is preferred in aenum
as it is more explicit about what it is checking, but it falls back to _missing_
for compatibility with the stdlib.
4 aenum v2.0.2 has a bug where _missing_
is called for both values and names if _missing_name_
has not been overridden — this is fixed in v2.0.3+.
Not 100% sure this will work in Python 2.7, but I came up with a simple way to make it work exactly as requested for Python 3.6+.
The idea is that lookup for names is done by the class using square brackets, which means that it uses __getitem__
from the metaclass. So you can make a simple metaclass that implements a case insensitive search. Since it will extend the existing EnumMeta
, it will be totally compatible with existing enums:
class CaseInsensitiveEnumMeta(EnumMeta):
def __getitem__(self, item):
if isinstance(item, str):
item = item.upper()
return super().__getitem__(item)
This presupposes that your enums are all uppercase. Using it is pretty straightforward:
class Label(Enum, metaclass=CaseInsensitiveEnumMeta):
REDAPPLE = 1
GREENAPPLE = 2
I’m not sure you even need Enum
in this case. The catch here is that your enums have to be all uppercase for this to work. If you want to have a truly insensitive search, you would have to make sure that all of the keys in __members__
are properly casefolded.
In the meantime, you can do
>>> Label['GreenApple']
Label.GREENAPPLE
enum have missing function which can be overridden to make enum case insensitive.
As per documentation
https://docs.python.org/3.11/howto/enum.html
missing – a lookup function used when a value is not found; may be overridden
example
class Status(enum.Enum):
@classmethod
def _missing_(cls, value):
for member in cls:
if member.value == value.upper():
return member
SUCCESS = 'SUCCESS'
FAILURE = 'FAILURE'
print(Status('success'))
Output
Status.SUCCESS
I don’t wanted to define this validation in the enum class, this is a general functionality I want to reuse. So I decided to do this in an MetaEnum:
from enum import Enum, auto, EnumMeta
class CaseInsensitveKeys(EnumMeta):
def __getitem__(cls, item):
try:
return super().__getitem__(item)
except:
for key in cls._member_map_.keys():
if key.casefold() == item.casefold():
return super().__getitem__(key)
class DatabaseType(Enum, metaclass=CaseInsensitveKeys):
PostgreSQL = auto()
Oracle = auto()
I have a simple Python 2.7 enum:
from enum import Enum
class Label(enum):
RedApple = 1
GreenApple = 2
I would like to be able create enum objects using case-insensitive keys:
fruitname = "redapple"
a = Label[fruitname]
I’ve tried to create an __init__
method:
def __init__(self, key):
super(Label, self).__init__()
pass # do comparison here
but I keep running into errors:
super(Label, self).__init__()
NameError: global name 'Label' is not defined
I’d like to do a comparison on key.lower().strip()
. Is this even possible?
In Python 3.6 and aenum 2.0
12 (which is compatible with 2.7 and 3.0+) a new method has been added: _missing_
2.
This method is called just before a ValueError
is raised to give custom code a chance to try and find the enum member by value. Unfortunately, this makes it unsuited for your use-case — looking up by name.
Fortunately, aenum
has the extra option of defining a _missing_name_
method3 which will be called when name lookup fails.
Your code above should look like this:
from aenum import Enum
class Label(Enum):
RedApple = 1
GreenApple = 2
@classmethod
def _missing_name_(cls, name):
for member in cls:
if member.name.lower() == name.lower():
return member
And in use:
>>> Label['redapple']
<Label.RedApple: 1>
If stuck using the 3.6 stdlib (or want to stay compatible with it) you can (ab)use _missing_
but:
- you will have to do
Label('redapple')
instead (round parens, not square brackets), and - you will be working against the design of enum (‘redapple’ is the name, not the value)
1 Disclosure: I am the author of the Python stdlib Enum
, the enum34
backport, and the Advanced Enumeration (aenum
) library.
2 enum34
does not have these improvements as it is only being maintained for bug fixes.
3 _missing_value_
is preferred in aenum
as it is more explicit about what it is checking, but it falls back to _missing_
for compatibility with the stdlib.
4 aenum v2.0.2 has a bug where _missing_
is called for both values and names if _missing_name_
has not been overridden — this is fixed in v2.0.3+.
Not 100% sure this will work in Python 2.7, but I came up with a simple way to make it work exactly as requested for Python 3.6+.
The idea is that lookup for names is done by the class using square brackets, which means that it uses __getitem__
from the metaclass. So you can make a simple metaclass that implements a case insensitive search. Since it will extend the existing EnumMeta
, it will be totally compatible with existing enums:
class CaseInsensitiveEnumMeta(EnumMeta):
def __getitem__(self, item):
if isinstance(item, str):
item = item.upper()
return super().__getitem__(item)
This presupposes that your enums are all uppercase. Using it is pretty straightforward:
class Label(Enum, metaclass=CaseInsensitiveEnumMeta):
REDAPPLE = 1
GREENAPPLE = 2
I’m not sure you even need Enum
in this case. The catch here is that your enums have to be all uppercase for this to work. If you want to have a truly insensitive search, you would have to make sure that all of the keys in __members__
are properly casefolded.
In the meantime, you can do
>>> Label['GreenApple']
Label.GREENAPPLE
enum have missing function which can be overridden to make enum case insensitive.
As per documentation
https://docs.python.org/3.11/howto/enum.html
missing – a lookup function used when a value is not found; may be overridden
example
class Status(enum.Enum):
@classmethod
def _missing_(cls, value):
for member in cls:
if member.value == value.upper():
return member
SUCCESS = 'SUCCESS'
FAILURE = 'FAILURE'
print(Status('success'))
Output
Status.SUCCESS
I don’t wanted to define this validation in the enum class, this is a general functionality I want to reuse. So I decided to do this in an MetaEnum:
from enum import Enum, auto, EnumMeta
class CaseInsensitveKeys(EnumMeta):
def __getitem__(cls, item):
try:
return super().__getitem__(item)
except:
for key in cls._member_map_.keys():
if key.casefold() == item.casefold():
return super().__getitem__(key)
class DatabaseType(Enum, metaclass=CaseInsensitveKeys):
PostgreSQL = auto()
Oracle = auto()