A custom Django field able to use subclasses of Python's internal `Enum` class as choices
Project description
django-enum-choices
A custom Django choice field to use with Python enums.
Table of Contents
- Installation
- Basic Usage
- Customizing readable values
- Usage with forms
- Postgres ArrayField Usage
- Usage with Django Rest Framework
- Usage with
django-filter - Serializing PostgreSQL ArrayField
- Implementation details
- Using Python's
enum.auto - Development
Installation
pip install django-enum-choices
Basic Usage
from django.db import models
from django_enum_choices.fields import EnumChoiceField
from enum import Enum
class MyEnum(Enum):
A = 'a'
B = 'b'
class MyModel(models.Model):
enumerated_field = EnumChoiceField(MyEnum)
Model creation
instance = MyModel.objects.create(enumerated_field=MyEnum.A)
Changing enum values
instance.enumerated_field = MyEnum.B
instance.save()
Filtering
MyModel.objects.filter(enumerated_field=MyEnum.A)
Customizing readable values
If a choice_builder argument is passed to a model's EnumChoiceField, django_enum_choices will use it to generate the choices.
The choice_builder must be a callable that accepts an enumeration choice and returns a tuple,
containing the value to be saved and the readable value.
By default django_enum_choices uses one of the four choice builders defined in django_enum_choices.choice_builders, named value_value.
It returns a tuple containing the enumeration's value twice:
from django_enum_choices.choice_builders import value_value
class MyEnum(Enum):
A = 'a'
B = 'b'
print(value_value(MyEnum.A)) # ('a', 'a')
You can use one of the existing ones that fits your needs:
from django_enum_choices.choice_builders import attribute_value
class MyEnum(Enum):
A = 'a'
B = 'b'
class CustomReadableValueEnumModel(models.Model):
enumerated_field = EnumChoiceField(
MyEnum,
choice_builder=attribute_value
)
The resulting choices for enumerated_field will be (('A', 'a'), ('B', 'b'))
You can also define your own choice builder:
class MyEnum(Enum):
A = 'a'
B = 'b'
def choice_builder(choice):
return choice.value, choice.value.upper() + choice.value
class CustomReadableValueEnumModel(models.Model):
enumerated_field = EnumChoiceField(
MyEnum,
choice_builder=choice_builder
)
Which will result in the following choices (('a', 'Aa'), ('b', 'Bb'))
The values in the returned from choice_builder tuple will be cast to strings before being used.
Usage with forms
Usage with django.forms.Form
from django_enum_choices.forms import EnumChoiceField
from .enumerations import MyEnum
class StandardEnumForm(forms.Form):
enumerated_field = EnumChoiceField(MyEnum)
form = StandardEnumForm({
'enumerated_field': 'a'
})
form.is_valid()
print(form.cleaned_data) # {'enumerated_field': <MyEnum.A: 'a'>}
Usage with django.forms.ModelForm
from .models import MyModel
class ModelEnumForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['enumerated_field']
form = ModelEnumForm({
'enumerated_field': 'a'
})
form.is_valid()
print(form.save(commit=True)) # <MyModel: MyModel object (12)>
A choice_builder argument can be passed to django_model_choices.forms.EnumChoiceField
for usage with model fields with custom choice builders:
from .enumerations import MyEnum
def custom_choice_builder(choice):
return 'Custom_' + choice.value, choice.value
class CustomChoiceBuilderEnumForm(forms.Form):
enumerated_field = EnumChoiceField(
MyEnum,
choice_builder=custom_choice_builder
)
form = CustomChoiceBuilderEnumForm({
'enumerated_field': 'Custom_a'
})
form.is_valid()
print(form.cleaned_data) # {'enumerated_field': <MyEnum.A: 'a'>}
Postgres ArrayField Usage
from django.db import models
from django.contrib.postgres.fields import ArrayField
from django_enum_choices.fields import EnumChoiceField
from enum import Enum
class MyEnum(Enum):
A = 'a'
B = 'b'
class MyModelMultiple(models.Model):
enumerated_field = ArrayField(
base_field=EnumChoiceField(MyEnum)
)
Model Creation
instance = MyModelMultiple.objects.create(enumerated_field=[MyEnum.A, MyEnum.B])
Changing enum values
instance.enumerated_field = [MyEnum.B]
instance.save()
Usage with Django Rest Framework
Using a subclass of serializers.Serializer
from rest_framework import serializers
from django_enum_choices.serializers import EnumChoiceField
class MySerializer(serializers.Serializer):
enumerated_field = EnumChoiceField(MyEnum)
# Serialization:
serializer = MySerializer({
'enumerated_field': MyEnum.A
})
data = serializer.data # {'enumerated_field': 'a'}
# Deserialization:
serializer = MySerializer(data={
'enumerated_field': 'a'
})
serializer.is_valid()
data = serializer.validated_data # OrderedDict([('enumerated_field', <MyEnum.A: 'a'>)])
Using a subclass of serializers.ModelSerializer
from rest_framework import serializers
from django_enum_choices.serializers import EnumChoiceField
class MyModelSerializer(serializers.ModelSerializer):
enumerated_field = EnumChoiceField(MyEnum)
class Meta:
model = MyModel
fields = ('enumerated_field', )
# Serialization:
instance = MyModel.objects.create(enumerated_field=MyEnum.A)
serializer = MyModelSerializer(instance)
data = serializer.data # {'enumerated_field': 'a'}
# Saving:
serializer = MyModelSerializer(data={
'enumerated_field': 'a'
})
serializer.is_valid()
serializer.save()
Additionally, a choice_builder argument can be passed to the serializer field for custom choice generation:
def custom_choice_builder(choice):
return 'Custom_' + choice.value, choice.value
class CustomChoiceBuilderSerializer(serializers.Serializer):
enumerted_field = EnumChoiceField(
MyEnum,
choice_builder=custom_choice_builder
)
serializer = CustomChoiceBuilderSerializer({
'enumerated_field': MyEnum.A
})
data = serializer.data # {'enumerated_field': 'Custom_a'}
When using the EnumChoiceModelSerializerMixin with DRF's serializers.ModelSerializer, the choice_builder is automatically passed from the model field to the serializer field.
Caveat
If you don't explicitly specify the enumerated_field = EnumChoiceField(MyEnum), then you need to include the EnumChoiceModelSerializerMixin:
from rest_framework import serializers
from django_enum_choices.serializers import EnumChoiceModelSerializerMixin
class ImplicitMyModelSerializer(
EnumChoiceModelSerializerMixin,
serializers.ModelSerializer
):
class Meta:
model = MyModel
fields = ('enumerated_field', )
By default ModelSerializer.build_standard_field coerces any field that has a model field with choices to ChoiceField which returns the value directly.
Since enum values resemble EnumClass.ENUM_INSTANCE they won't be able to be encoded by the JSONEncoder when being passed to a Response.
That's why we need the mixin.
Serializing PostgreSQL ArrayField
django-enum-choices exposes a MultipleEnumChoiceField that can be used for serializing arrays of enumerations.
Using a subclass of serializers.Serializer
from rest_framework import serializers
from django_enum_choices.serializers import MultipleEnumChoiceField
class MultipleMySerializer(serializers.Serializer):
enumerated_field = MultipleEnumChoiceField(MyEnum)
# Serialization:
serializer = MultipleMySerializer({
'enumerated_field': [MyEnum.A, MyEnum.B]
})
data = serializer.data # {'enumerated_field': ['a', 'b']}
# Deserialization:
serializer = MultipleMySerializer(data={
'enumerated_field': ['a', 'b']
})
serializer.is_valid()
data = serializer.validated_data # OrderedDict([('enumerated_field', [<MyEnum.A: 'a'>, <MyEnum.B: 'b'>])])
Using a subclass of serializers.ModelSerializer
class ImplicitMultipleMyModelSerializer(
EnumChoiceModelSerializerMixin,
serializers.ModelSerializer
):
class Meta:
model = MyModelMultiple
fields = ('enumerated_field', )
# Serialization:
instance = MyModelMultiple.objects.create(enumerated_field=[MyEnum.A, MyEnum.B])
serializer = ImplicitMultipleMyModelSerializer(instance)
data = serializer.data # {'enumerated_field': ['a', 'b']}
# Saving:
serializer = ImplicitMultipleMyModelSerializer(data={
'enumerated_field': ['a', 'b']
})
serializer.is_valid()
serializer.save()
The EnumChoiceModelSerializerMixin does not need to be used if enumerated_field is defined on the serializer class explicitly.
Usage with django-filter
By declaring the field explicitly on the FilterSet
import django_filters as filters
from django_enum_choices.filters import EnumChoiceFilter
class ExplicitFilterSet(filters.FilterSet):
enumerated_field = EnumChoiceFilter(MyEnum)
filters = {
'enumerated_field': 'a'
}
filterset = ExplicitFilterSet(filters, MyModel.objects.all())
print(filterset.qs.values_list('enumerated_field', flat=True)) # <QuerySet [<MyEnum.A: 'a'>, <MyEnum.A: 'a'>, <MyEnum.A: 'a'>]>
By using a Meta inner class and inheriting from EnumChoiceFilterMixin
import django_filters as filters
from django_enum_choices.filters import EnumChoiceFilterMixin
class ImplicitFilterSet(EnumChoiceFilterSetMixin, filters.FilterSet):
class Meta:
model = MyModel
fields = ['enumerated_field']
filters = {
'enumerated_field': 'a'
}
filterset = ImplicitFilterSet(filters)
print(filterset.qs.values_list('enumerated_field', flat=True)) # <QuerySet [<MyEnum.A: 'a'>, <MyEnum.A: 'a'>, <MyEnum.A: 'a'>]>
The choice_builder argument can be passed to django_enum_choices.filters.EnumChoiceFilter as well when using the field explicitly. When using EnumChoiceFilterSetMixin, the choice_builder is determined from the model field, for the fields defined inside the Meta inner class.
import django_filters as filters
from django_enum_choices.filters import EnumChoiceFilter
def custom_choice_builder(choice):
return 'Custom_' + choice.value, choice.value
class ExplicitCustomChoiceBuilderFilterSet(filters.FilterSet):
enumerated_field = EnumChoiceFilter(
MyEnum,
choice_builder=custom_choice_builder
)
filters = {
'enumerated_field': 'Custom_a'
}
filterset = ExplicitCustomChoiceBuilderFilterSet(filters, MyModel.objects.all())
print(filterset.qs.values_list('enumerated_field', flat=True)) # <QuerySet [<MyEnum.A: 'a'>, <MyEnum.A: 'a'>, <MyEnum.A: 'a'>]>
Implementation details
EnumChoiceFieldis a subclass ofCharField.- Only subclasses of
Enumare valid arguments forEnumChoiceField. max_length, if passed, is ignored.max_lengthis automatically calculated from the longest choice.choicesare generated using a specialchoice_builderfunction, which accepts an enumeration and returns a tuple of 2 items.- Four choice builder functions are defined inside
django_enum_choices.choice_builders - By default the
value_valuechoice builder is used. It produces the choices from the values in the enumeration class, like(enumeration.value, enumeration.value) choice_buildercan be overriden by passing a callable to thechoice_builderkeyword argument ofEnumChoiceField.- All values returned from the choice builder will be cast to strings when generating choices.
- Four choice builder functions are defined inside
For example, lets have the following case:
class Value:
def __init__(self, value):
self.value = value
def __str__(self):
return self.value
class CustomObjectEnum(Enum):
A = Value(1)
B = Value('B')
# The default choice builder `value_value` is being used
class SomeModel(models.Model):
enumerated_field = EnumChoiceField(CustomObjectEnum)
We'll have the following:
SomeModel.enumerated_field.choices == (('1', '1'), ('B', 'B'))SomeModel.enumerated_field.max_length == 3
Using Python's enum.auto
enum.auto can be used for shorthand enumeration definitions:
from enum import Enum, auto
class AutoEnum(Enum):
A = auto() # 1
B = auto() # 2
class SomeModel(models.Model):
enumerated_field = EnumChoiceField(Enum)
This will result in the following:
SomeModel.enumerated_field.choices == (('1', '1'), ('2', '2'))
Overridinng auto behaviour
Custom values for enumerations, created by auto, can be defined by
subclassing an Enum that defines _generate_next_value_:
class CustomAutoEnumValueGenerator(Enum):
def _generate_next_value_(name, start, count, last_values):
return {
'A': 'foo',
'B': 'bar'
}[name]
class CustomAutoEnum(CustomAutoEnumValueGenerator):
A = auto()
B = auto()
The above will assign the values mapped in the dictionary as values to attributes in CustomAutoEnum.
Development
Prerequisites
- SQLite3
- PostgreSQL server
- Python >= 3.5 virtual environment
git clone https://github.com/HackSoftware/django-enum-choices.git
cd django_enum_choices
pip install -e .[dev]
Linting and running the tests:
tox
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_enum_choices-2.0.0.tar.gz.
File metadata
- Download URL: django_enum_choices-2.0.0.tar.gz
- Upload date:
- Size: 22.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/41.0.1 requests-toolbelt/0.9.1 tqdm/4.32.2 CPython/3.6.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bdd2abf4d9ba09372155e5f5c039e426921d749b1be3c73062749a8d5d5cee2
|
|
| MD5 |
1d38f0ec54b6cafcd5b40c9393c0787f
|
|
| BLAKE2b-256 |
48d7b65ce4221d52c41ad770cbf5bdf08fcdfa6d64454646af4ac4a10d6a4ff5
|
File details
Details for the file django_enum_choices-2.0.0-py2.py3-none-any.whl.
File metadata
- Download URL: django_enum_choices-2.0.0-py2.py3-none-any.whl
- Upload date:
- Size: 27.1 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/41.0.1 requests-toolbelt/0.9.1 tqdm/4.32.2 CPython/3.6.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79ce1290cd4866699313b1e6cd6457cdc8cebc2907f18ec56048a60320b7ba80
|
|
| MD5 |
0b116127d0b2353d4b0d230720518156
|
|
| BLAKE2b-256 |
3f7b63e7b3d6ce336138b27b039bd4c9e1f01441947f37422c5bb91f67b4bb94
|