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'
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
TextChoices and IntegerChoices types are provided that extend Django’s native choice types with support for enum-properties. The dependency on enum-properties is optional, so to utilize these classes you must separately install enum-properties:
pip install enum-properties
These choice extensions make possible very rich enumerations that have other values that can be symmetrically mapped back to enumeration values:
from enum_properties import s
from django_enum import TextChoices # use instead of Django's TextChoices
from django.db import models
class TextChoicesExample(models.Model):
class Color(TextChoices, s('rgb'), s('hex', 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 == TextChoicesExample.Color('Red')
assert instance.color == TextChoicesExample.Color('R')
assert instance.color == 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.
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 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 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 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.
Performance
The cost to resolve a raw database value into an Enum type object is
non-zero. EnumFields
may not be appropriate for use cases at the very edge
of critical performance targets, but for most scenarios the cost of using
EnumFields
is negligible.
An effort is made to characterize and monitor the performance penalty of
using EnumFields
over a Django native field with choices and integration
tests ensure performance of future releases will not worsen.
Note
The read performance penalty can be eliminated by setting coerce
to
False
.