Usage¶
EnumField
inherits from the appropriate native Django field and sets
the correct choice tuple set based on the enumeration type. This means EnumFields
are
compatible with all modules, utilities and libraries that fields defined with a choice tuple are.
For example:
from django.db import models
from django_enum import EnumField
class MyModel(models.Model):
class TextEnum(models.TextChoices):
VALUE0 = 'V0', 'Value 0'
VALUE1 = 'V1', 'Value 1'
VALUE2 = 'V2', 'Value 2'
txt_enum = EnumField(TextEnum, null=True, blank=True, default=None)
txt_choices = models.CharField(
max_length=2,
choices=MyModel.TextEnum.choices,
null=True,
blank=True,
default=None
)
txt_enum
and txt_choices
fields are equivalent in all ways with the
following exceptions:
# txt_enum fields will always be an instance of the TextEnum type, unless
# set to a value that is not part of the enumeration
assert isinstance(MyModel.objects.first().txt_enum, MyModel.TextEnum)
assert not isinstance(MyModel.objects.first().txt_choices, MyModel.TextEnum)
# by default EnumFields are more strict, this is possible:
MyModel.objects.create(txt_choices='AA')
# but this will throw a ValueError (unless strict=False)
MyModel.objects.create(txt_enum='AA')
# and this will throw a ValidationError
MyModel(txt_enum='AA').full_clean()
Any ModelForms
, DRF serializers and filters will behave the same way with
txt_enum
and txt_choices
. A few types are provided for deeper
integration with forms and django-filter but their usage is optional.
See Forms and Filtering.
Very rich enumeration fields that encapsulate much more functionality in a
simple declarative syntax are possible with EnumField
. See
enum-properties.
External Enum Types¶
Enum classes defined externally to your code base or enum classes that otherwise do not inherit
from Django’s Choices type, are supported. When no choices are present on an 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.
In short, EnumField
should work with any subclass of Enum.
from enum import Enum
from django.db import models
from django_enum import EnumField
class MyModel(models.Model):
class TextEnum(str, Enum)
VALUE0 = 'V0'
VALUE1 = 'V1'
VALUE2 = 'V2'
txt_enum = EnumField(TextEnum)
The above code will produce a choices set like [('V0', 'VALUE0'), ...]
.
Warning
One nice feature of Django’s Choices type is that it disables
auto()
on Enum fields. 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 using Enums
where control over the values is
not certain it is a good idea to add integration tests that look for value
changes.
Parameters¶
All parameters available to the equivalent model field with choices may be set directly in the
EnumField
instantiation. If not provided
EnumField
will set choices
and max_length
automatically.
The following EnumField
specific parameters are available:
strict
¶
By default all EnumFields
are strict
. This means a ValidationError
will be thrown anytime full_clean is run on a model and a value is set for the
field that can not be coerced to its native Enum type. To allow the field
to store values that are not present in the fields Enum type we can pass
strict=False.
Non-strict fields that have values outside of the enumeration will be instances of the enumeration where a valid Enum value is present and the plain old data where no Enum type coercion is possible.
class StrictExample(models.Model):
class EnumType(TextChoices):
ONE = '1', 'One'
TWO = '2', 'Two'
non_strict = EnumField(
EnumType,
strict=False,
# it might be necessary to override max_length also, otherwise
# max_length will be 1
max_length=10
)
obj = StrictExample()
# set to a valid EnumType value
obj.non_strict = '1'
# when accessed will be an EnumType instance
assert obj.non_strict is StrictExample.EnumType.ONE
# we can also store any string less than or equal to length 10
obj.non_strict = 'arbitrary'
obj.full_clean() # no errors
# when accessed will be a str instance
assert obj.non_strict == 'arbitrary'
constrained
¶
By default all strict EnumFields
are constrained
. This means that
CheckConstraints will be
generated at the database level to ensure that the column will reject any value that is not
present in the enumeration. This is a good idea for most use cases, but it can be turned off
by setting constrained
to False
.
Note
This is new in version 2.0. If you are upgrading from a previous version, you may set
this parameter to False
to maintain the previous behavior.
primitive
¶
EnumFields
dynamically determine the database column type by determining the most appropriate
primitive type for the enumeration based on the enumeration values. You may override the primitive
determined by EnumField
by passing a type to the primitive
parameter. You will likely not need to do this unless your enumeration is
eccentric in some way.
coerce
¶
Setting this parameter to False
will turn off the automatic conversion to
the field’s Enum type while leaving all validation checks in place. It will
still be possible to set the field directly as an Enum instance and to
filter by Enum instance or any symmetric value:
non_strict = EnumField(
EnumType,
strict=False,
coerce=False,
# it might be necessary to override max_length also, otherwise
# max_length will be 1
max_length=10
)
# set to a valid EnumType value
obj.non_strict = '1'
# when accessed will be the primitive value
assert obj.non_strict == '1'
assert isinstance(obj.non_strict, str)
assert not isinstance(obj.non_strict, StrictExample.EnumType)
enum-properties¶
Almost any Enum type is supported, so you may make use of Enum extension libraries like enum-properties to define very rich enumeration fields:
pip install enum-properties
enum-properties is an extension to Enum that allows properties to be added to enumeration instances using a simple declarative syntax. This is a less awkward and more compatible alternative than dataclass enumerations.
If you find yourself considering a dataclass enumeration, consider using enum-properties instead.
dataclass enumerations do not work with EnumField
because their value
type is a dataclass. Futher, most libraries that expect to be able to work with enumerations expect
the value
attribute to be a primitive serializable type.
import typing as t
from enum_properties import StrEnumProperties, Symmetric
from django_enum.choices import TextChoices # use instead of Django's TextChoices
from django.db import models
class TextChoicesExample(models.Model):
class Color(StrEnumProperties):
label: t.Annotated[str, Symmetric()]
rgb: t.Annotated[t.Tuple[int, int, int], Symmetric()]
hex: t.Annotated[str, Symmetric(case_fold=True)]
# name value label rgb hex
RED = 'R', 'Red', (1, 0, 0), 'ff0000'
GREEN = 'G', 'Green', (0, 1, 0), '00ff00'
BLUE = 'B', 'Blue', (0, 0, 1), '0000ff'
# any named s() values in the Enum's inheritance become properties on
# each value, and the enumeration value may be instantiated from the
# property's value
color = EnumField(Color)
instance = TextChoicesExample.objects.create(
color=TextChoicesExample.Color('FF0000')
)
assert instance.color is TextChoicesExample.Color('Red')
assert instance.color is TextChoicesExample.Color('R')
assert instance.color is TextChoicesExample.Color((1, 0, 0))
# direct comparison to any symmetric value also works
assert instance.color == 'Red'
assert instance.color == 'R'
assert instance.color == (1, 0, 0)
# save by any symmetric value
instance.color = 'FF0000'
# access any enum property right from the model field
assert instance.color.hex == 'ff0000'
# this also works!
assert instance.color == 'ff0000'
# and so does this!
assert instance.color == 'FF0000'
instance.save()
# filtering works by any symmetric value or enum type instance
assert TextChoicesExample.objects.filter(
color=TextChoicesExample.Color.RED
).first() == instance
assert TextChoicesExample.objects.filter(color=(1, 0, 0)).first() == instance
assert TextChoicesExample.objects.filter(color='FF0000').first() == instance
For a real-world example see Examples.
It should be unnecessary, but if you need to integrate with code that expects an interface fully
compatible with Django’s
enumeration types
(TextChoices
and IntegerChoices
django-enum provides
TextChoices
, IntegerChoices
,
FlagChoices
and FloatChoices
types that
derive from enum-properties and Django’s Choices
. So the above enumeration could also be
written:
from django_enum.choices import TextChoices
class Color(TextChoices):
# label is added as a symmetric property by the base class
rgb: Annotated[t.Tuple[int, int, int], Symmetric()]
hex: Annotated[str, Symmetric(case_fold=True)]
# name value label rgb hex
RED = "R", "Red", (1, 0, 0), "ff0000"
GREEN = "G", "Green", (0, 1, 0), "00ff00"
BLUE = "B", "Blue", (0, 0, 1), "0000ff"
Note
To use these Choices
extensions you will need to install enum-properties which is an
optional dependency.
Forms¶
An EnumChoiceField
type is provided that enables symmetric value resolution
and will automatically coerce any set value to the underlying enumeration type.
Django’s ModelForms
will use this form field type to represent
EnumFields
by default. For most scenarios this is sufficient. The
EnumChoiceField
can also be explicitly used. For example, using our
TextChoicesExample
from above - if color
was declared with
strict=False, we could add additional choices to our form field like so:
from django_enum.forms import EnumChoiceField
class TextChoicesExampleForm(ModelForm):
color = EnumChoiceField(
TextChoicesExample.Color,
strict=False,
choices=[
('P', 'Purple'),
('O', 'Orange'),
] + TextChoicesExample.Color.choices
)
class Meta:
model = TextChoicesExample
fields = '__all__'
# when this form is rendered in a template it will include a selected
# option for the value 'Y' that is not part of our Color enumeration.
# since our field is not strict, we can set it to a value not in our
# enum or choice tuple.
form = TextChoicesExampleForm(
instance=TextChoicesExample.objects.create(color='Y')
)
<!-- The above will render the following options: -->
<select>
<!-- our extended choices -->
<option value='P'>Purple</option>
<option value='O'>Orange</option>
<!-- choices from our enum -->
<option value='R'>Red</option>
<option value='G'>Green</option>
<option value='B'>Blue</option>
<!--
non-strict fields that have data that is not a valid enum value and is
not present in the form field's choices tuple will have that value
rendered as the selected option.
-->
<option value='Y' selected>Y</option>
</select>
Django Rest Framework¶
By default DRF ModelSerializer
will use a ChoiceField
to represent an
EnumField
. This works great, but it will not accept symmetric enumeration
values. A serializer field EnumField
is provided that will. The dependency
on DRF is optional so to use the provided serializer field you must install
DRF:
pip install djangorestframework
from django_enum.drf import EnumField
from rest_framework import serializers
class ExampleSerializer(serializers.Serializer):
color = EnumField(TextChoicesExample.Color)
ser = ExampleSerializer(data={'color': (1, 0, 0)})
assert ser.is_valid()
The serializer EnumField
accepts any arguments that ChoiceField
does. It also accepts the strict
parameter which behaves the same way as it does
on the model field.
Filtering¶
As shown above, filtering by any value, enumeration type instance or symmetric
value works with Django’s ORM. This is not natively true for automatically
generated FilterSets
from django-filter. Those filter sets will only be
filterable by direct enumeration value by default. An EnumFilter
type is
provided to enable filtering by symmetric property values, but since the
dependency on django-filter is optional, you must first install it:
pip install django-filter
from django_enum.filters import EnumFilter
from django_filters.views import FilterView
from django_filters import FilterSet
class TextChoicesExampleFilterViewSet(FilterView):
class TextChoicesExampleFilter(FilterSet):
color = EnumFilter(TextChoicesExample.Color)
class Meta:
model = TextChoicesExample
fields = '__all__'
filterset_class = TextChoicesExampleFilter
model = TextChoicesExample
# now filtering by symmetric value in url parameters works:
# e.g.: /?color=FF0000
An EnumFilterSet
type is also provided that uses EnumFilter
for EnumFields
by default. So the above is also equivalent to:
from django_enum.filters import FilterSet as EnumFilterSet
from django_filters.views import FilterView
class TextChoicesExampleFilterViewSet(FilterView):
class TextChoicesExampleFilter(EnumFilterSet):
class Meta:
model = TextChoicesExample
fields = '__all__'
filterset_class = TextChoicesExampleFilter
model = TextChoicesExample
Migrations¶
Important
There is one rule for writing custom migration files for EnumFields: Never reference or import your enumeration classes in a migration file, work with the primitive values instead.
The deconstructed EnumFields
only include the choices tuple in the
migration files. This is because Enum classes may come and go or be
altered but the earlier migration files must still work. Simply treat any
custom migration routines as if they were operating on a normal model field
with choices.
EnumFields
in migration files will not resolve the field values to
enumeration types. The fields will be the primitive enumeration values as they
are with any field with choices.
Flag Enumerations¶
Python supports bit masks through the Flag extension to Enum.
These enumerations are fully supported and will render as multi select form fields by default. For example:
from enum import IntFlag
from django_enum import EnumField
from django.db import models
class MyModel(models.Model):
class GNSSConstellation(IntFlag):
GPS = 2**1
GLONASS = 2**2
GALILEO = 2**3
BEIDOU = 2**4
QZSS = 2**5
constellation = EnumField(GNSSConstellation)
obj1 = MyModel.objects.create(
constellation=(
GNSSConstellation.GPS |
GNSSConstellation.GLONASS |
GNSSConstellation.GALILEO
)
)
obj2 = MyModel.objects.create(constellation=GNSSConstellation.GPS)
assert GNSSConstellation.GPS in obj1.constellation
assert GNSSConstellation.GLONASS in obj1.constellation
Two new field lookups are provided for flag enumerations: has_any
and
has_all
.
has_any¶
The has_any
lookup will return any object that has at least one of the flags in the referenced
enumeration. For example:
# this will return both obj1 and obj2
MyModel.objects.filter(
constellation__has_any=GNSSConstellation.GPS | GNSSConstellation.QZSS
)
has_all¶
The has_all
lookup will return any object that has at least all of the flags in the referenced
enumeration. For example:
# this will return only obj1
MyModel.objects.filter(
constellation__has_all=GNSSConstellation.GPS | GNSSConstellation.GLONASS
)
There are performance considerations when using a bit mask like a Flag enumeration instead of multiple boolean columns. See flag performance for discussion and benchmarks.
Flags with more than 64 bits¶
Flag enumerations of arbitrary size are supported, however if the enum has more than 64 flags it will be stored as a BinaryField. It is therefore strongly recommended to keep your Flag enumerations at 64 bits or less.
Warning
Support for extra large flag fields is experimental. has_any
and has_all
do not work.
Most RDBMS systems do not support bitwise operations on binary fields. Future work may
involve exploring support for this as a Postgres extension.
URLs¶
django-enum provides a converter that can be used to register enum url parameters with the Django path resolver.
from enum import IntEnum
from django.http import HttpResponse
from django.urls import path
from django_enum.urls import register_enum_converter
class Enum1(IntEnum):
A = 1
B = 2
register_enum_converter(Enum1)
def enum_converter_view(request, enum):
assert isinstance(enum, Enum1)
return HttpResponse(status=200)
# this will match paths /1/ and /2/
urlpatterns = [
path("<Enum1:enum>", register_enum_converter, name="enum1_view"),
]
By default the converter will use the value property of the enumeration to resolve the enumeration, but this can be overridden by passing the prop parameter, so we could for example use the label instead.