Use External Enums¶
enum.Enum
classes defined externally to your code base or enum classes that otherwise do
not inherit from Django’s Enumeration types, are supported. When no choices are
present on an enum.Enum
type, EnumField
will attempt to use
the label
member on each enumeration value if it is present, otherwise the labels will be based
off the enumeration name. Choices can also be overridden at the
EnumField
declaration.
EnumField
should work with any subclass of enum.Enum
.
from enum import Enum
from django.db import models
from django_enum import EnumField
class ExternalChoices(models.Model):
class TextEnum(str, Enum):
VALUE0 = 'V0'
VALUE1 = 'V1'
VALUE2 = 'V2'
# choices will default to (value, name) pairs
txt_enum1 = EnumField(TextEnum)
# you can also override choices
txt_enum2 = EnumField(
TextEnum,
choices=[(en.value, en.name.title()) for en in TextEnum]
)
The list of choice tuples for each field are:
assert ExternalChoices._meta.get_field('txt_enum1').choices == [
('V0', 'VALUE0'),
('V1', 'VALUE1'),
('V2', 'VALUE2')
]
assert ExternalChoices._meta.get_field('txt_enum2').choices == [
('V0', 'Value0'),
('V1', 'Value1'),
('V2', 'Value2')
]
Warning
One nice feature of Django’s Enumeration types are that they disable
enum.auto
on enum.Enum
fields. enum.auto
can be dangerous because the
values assigned depend on the order of declaration. This means that if the order changes
existing database values will no longer align with the enumeration values. When control over the
values is not certain it is a good idea to add integration tests that look for value changes.
Hash Equivalency¶
Tip
It is a good idea to make sure your enumeration instances are hash equivalent to their
primitive values. You can do this simply by inheriting from their primitive value
(e.g. class MyEnum(str, Enum):
) or by using StrEnum
and
IntEnum
types. Any enumeration defined using Enum Properties
will be hash equivalent to its values by default.
EnumField
automatically sets the choices tuple on the field. Django
has logic in a number of places that handles fields with choices in a special way
(e.g. in the admin). For example, the choices may be converted to a dictionary
mapping values to labels. The values will be the primitive values of the enumeration not
enumeration instances and the current value of the field which may be an enumeration instance will
be searched for in the dictionary. This will fail if the enumeration instance is not hash
equivalent to its value.
To control the hashing behavior of an object, you must override its __hash__()
and
__eq__()
methods.
For example:
from enum import Enum
from django.db.models import Model
from django_enum import EnumField
class HashEquivalencyExample(Model):
"""
This example model defines three enum fields. The first uses an enum that
is not hash equivalent to its values. The second two are.
"""
class NotHashEq(Enum):
"""
Enums that inherit only from :class:`~enum.Enum` are not hash equivalent
to their values by default.
"""
VALUE1 = "V1"
VALUE2 = "V2"
VALUE3 = "V3"
class HashEq(Enum):
"""
We can force our Enum to be hash equivalent by overriding the necessary
dunder methods..
"""
VALUE1 = "V1"
VALUE2 = "V2"
VALUE3 = "V3"
def __hash__(self):
return hash(self.value)
def __eq__(self, value) -> bool:
if isinstance(value, self.__class__):
return self.value == value.value
try:
return self.value == self.__class__(value).value
except (ValueError, TypeError):
return False
class HashEqStr(str, Enum): # or StrEnum on py 3.11+
"""
Or we can inherit from the primitive value type.
"""
VALUE1 = "V1"
VALUE2 = "V2"
VALUE3 = "V3"
not_hash_eq = EnumField(NotHashEq)
hash_eq = EnumField(HashEq)
hash_eq_str = EnumField(HashEqStr)
obj = HashEquivalencyExample.objects.create(
not_hash_eq=HashEquivalencyExample.NotHashEq.VALUE1,
hash_eq=HashEquivalencyExample.HashEq.VALUE1,
hash_eq_str=HashEquivalencyExample.HashEqStr.VALUE1
)
# direct comparisons to values do not work
assert obj.not_hash_eq != "V1"
# unless you have provided __eq__ or inherited from the primitive
assert obj.hash_eq == obj.hash_eq_str == "V1"
# here is the problem that can break some Django internals in rare instances:
assert dict(HashEquivalencyExample._meta.get_field("not_hash_eq").flatchoices) == {
"V1": "VALUE1",
"V2": "VALUE2",
"V3": "VALUE3"
}
try:
dict(HashEquivalencyExample._meta.get_field("not_hash_eq").flatchoices)[
HashEquivalencyExample.NotHashEq.VALUE1
]
assert False
except KeyError:
assert True
# if we've made our enum hash equivalent though, this works:
assert dict(HashEquivalencyExample._meta.get_field("hash_eq").flatchoices)[
HashEquivalencyExample.HashEq.VALUE1
] == "VALUE1"
assert dict(HashEquivalencyExample._meta.get_field("hash_eq_str").flatchoices)[
HashEquivalencyExample.HashEqStr.VALUE1
] == "VALUE1"