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)