Eccentric Enums¶
Python’s enum.Enum
type is extremely lenient. Enumeration values may be any hashable type
and values of the same enumeration may be of different types.
Tip
We define an eccentric enumeration to be any enumeration where the value type is not a simple string or integer or where the enumeration values are not all of the same type.
For use in databases it is recommended to use more strict enumeration types that only allow a single value type of either string or integer. If additional properties need to be associated with enumeration values, a library like Enum Properties should be used to store them on the enumeration value classes.
However, the goal of django-enum is to provide as complete of a bridge as possible between Python and the database so eccentric enumerations are supported with caveats. The following enumeration value types are supported out of the box, and map to the obvious model field type.
You should avoid eccentric enums if possible, but there may be some compelling reasons to use them. For example, for unusual data types it may make sense in situations where the database will be used in a non-Python context and the enumeration values need to retain their native meaning. Or you may not have direct control over the enumeration you want to store.
Mixed Value Enumerations¶
Mixed value enumerations are supported. For example:
from decimal import Decimal
from enum import Enum
from django.db import models
from django_enum import EnumField
class MixedValueEnum(Enum):
NONE = None
VAL1 = 1
VAL2 = '2.0'
VAL3 = 3.0
VAL4 = Decimal('4.5')
class MixedValueExample(models.Model):
# Since None is an enumeration value, EnumField will automatically set
# null=True on these model fields.
# column will be a CharField
eccentric_str = EnumField(MixedValueEnum)
# column will be a FloatField
eccentric_float = EnumField(MixedValueEnum, primitive=float)
EnumField
will determine the most appropriate database column type to
store the enumeration by trying each of the supported primitive types in order and selecting the
first one that is symmetrically coercible to and from each enumeration value. None
values are
allowed and do not take part in the primitive type selection. In the above example, the database
column type would default to a string.
Note
If none of the supported primitive types are symmetrically coercible
EnumField
will not be able to determine an appropriate column
type and a ValueError
will be raised.
In these cases, or to override the primitive type selection made by
EnumField
, pass the primitive
parameter. It may be necessary to
extend one of the supported primitives to make it coercible. It may also be necessary
to override the enum.Enum
class’s _missing_()
method:
obj = MixedValueExample.objects.create(
eccentric_str=MixedValueEnum.NONE,
eccentric_float=MixedValueEnum.NONE
)
assert isinstance(obj._meta.get_field("eccentric_str"), models.CharField)
assert isinstance(obj._meta.get_field("eccentric_float"), models.FloatField)
for en in list(MixedValueEnum):
obj.eccentric_str = en
obj.eccentric_float = en
obj.save()
obj.refresh_from_db()
assert obj.eccentric_str is en
assert obj.eccentric_float is en
print(f"{obj.eccentric_str=}")
print(f"{obj.eccentric_float=}")
In the above case since None
is an enumeration value, EnumField
will automatically set null=True on the model field.
The above yields:
obj.eccentric_str=<MixedValueEnum.NONE: None>
obj.eccentric_float=<MixedValueEnum.NONE: None>
obj.eccentric_str=<MixedValueEnum.VAL1: 1>
obj.eccentric_float=<MixedValueEnum.VAL1: 1>
obj.eccentric_str=<MixedValueEnum.VAL2: '2.0'>
obj.eccentric_float=<MixedValueEnum.VAL2: '2.0'>
obj.eccentric_str=<MixedValueEnum.VAL3: 3.0>
obj.eccentric_float=<MixedValueEnum.VAL3: 3.0>
obj.eccentric_str=<MixedValueEnum.VAL4: Decimal('4.5')>
obj.eccentric_float=<MixedValueEnum.VAL4: Decimal('4.5')>
Custom Enum Value Types¶
Warning
There is almost certainly a better way to do what you might be trying to do by writing a custom
enumeration value - for example consider using Enum Properties to make your
enumeration types more robust by pushing more of this functionality on the enum.Enum
class itself.
If you must use a custom value type, you can by specifying a symmetrically coercible primitive type. For example Path is already symmetrically coercible to str so this works:
from django.db import models
from enum import Enum
from django_enum import EnumField
from pathlib import Path
class PathValueExample(models.Model):
class PathEnum(Enum):
USR = Path('/usr')
USR_LOCAL = Path('/usr/local')
USR_LOCAL_BIN = Path('/usr/local/bin')
path = EnumField(PathEnum, primitive=str)
A fully custom value might look like the following contrived example:
from enum import Enum
from django.db import models
from django_enum import EnumField
class StrProps:
"""
Wrap a string with some properties.
"""
_str = ''
def __init__(self, string):
self._str = string
def __str__(self):
"""
coercion to str - str(StrProps('str1')) == 'str1'
"""
return self._str
@property
def upper(self):
return self._str.upper()
@property
def lower(self):
return self._str.lower()
def __eq__(self, other):
"""
Make sure StrProps('str1') == 'str1'
"""
if isinstance(other, str):
return self._str == other
if other is not None:
return self._str == other._str
return False
def deconstruct(self):
"""
Necessary to construct choices and default in migration files
"""
return (
f'{self.__class__.__module__}.{self.__class__.__qualname__}',
(self._str,),
{}
)
class CustomValueExample(models.Model):
class StrPropsEnum(Enum):
STR1 = StrProps('str1')
STR2 = StrProps('str2')
STR3 = StrProps('str3')
str_props = EnumField(StrPropsEnum, primitive=str)