Skip to content

Commit

Permalink
Moved ChoiceEnums & co. to enums.py
Browse files Browse the repository at this point in the history
  • Loading branch information
felixxm committed Jul 4, 2019
1 parent f8a5e52 commit 7b79203
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 92 deletions.
2 changes: 2 additions & 0 deletions django/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.db.models.deletion import (
CASCADE, DO_NOTHING, PROTECT, SET, SET_DEFAULT, SET_NULL, ProtectedError,
)
from django.db.models.enums import ChoiceEnum, ChoiceIntEnum, ChoiceStrEnum
from django.db.models.expressions import (
Case, Exists, Expression, ExpressionList, ExpressionWrapper, F, Func,
OuterRef, RowRange, Subquery, Value, ValueRange, When, Window, WindowFrame,
Expand Down Expand Up @@ -36,6 +37,7 @@
__all__ += [
'ObjectDoesNotExist', 'signals',
'CASCADE', 'DO_NOTHING', 'PROTECT', 'SET', 'SET_DEFAULT', 'SET_NULL',
'ChoiceEnum', 'ChoiceIntEnum', 'ChoiceStrEnum',
'ProtectedError',
'Case', 'Exists', 'Expression', 'ExpressionList', 'ExpressionWrapper', 'F',
'Func', 'OuterRef', 'RowRange', 'Subquery', 'Value', 'ValueRange', 'When',
Expand Down
87 changes: 87 additions & 0 deletions django/db/models/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from enum import Enum, EnumMeta

from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

__all__ = ['ChoiceEnum', 'ChoiceIntEnum', 'ChoiceStrEnum']


class ChoiceEnumMeta(EnumMeta):
def __new__(metacls, classname, bases, classdict):
choices = {}
for key in classdict._member_names:
value = classdict[key]
if isinstance(value, (list, tuple)):
try:
value, display = value
except ValueError as e:
raise ValueError('Invalid ChoiceEnum member %r' % key) from e
else:
display = key.replace('_', ' ').title()
choices[value] = display
# Use dict.__setitem__() to suppress defenses against double
# assignment in enum's classdict.
dict.__setitem__(classdict, key, value)
cls = super().__new__(metacls, classname, bases, classdict)
cls._choices = choices
cls.choices = tuple(choices.items())
return cls


class ChoiceEnum(Enum, metaclass=ChoiceEnumMeta):
"""
A class suitable for using as an enum with translatable choices.
The typical use is similar to the stdlib's enums, with three
modifications:
* Instead of values in the enum, we use "(value, display)" tuples.
The "display" can be a lazy translatable string.
* We add a class method "choices()" which returns a value suitable
for use as "choices" in a Django field definition.
* We add a property "display" on enum values, to return the display
specified.
Thus, the definition of the Enum class can look like:
class YearInSchool(ChoiceStrEnum):
FRESHMAN = 'FR', _('Freshman')
SOPHOMORE = 'SO', _('Sophomore')
JUNIOR = 'JR', _('Junior')
SENIOR = 'SR', _('Senior')
or even
class Suit(ChoiceIntEnum):
DIAMOND = 1, _('Diamond')
SPADE = 2, _('Spade')
HEART = 3, _('Heart')
CLUB = 4, _('Club')
A field could be defined as
class Card(models.Model):
suit = models.IntegerField(choices=Suit.choices)
Suit.HEART, Suit['HEART'] and Suit(3) work as expected, while
Suit.HEART.display is a pretty, translatable string.
"""
@property
def display(self):
return self._choices[self]

@classmethod
def validate(cls, value, message=_('Invalid choice')):
try:
cls(value)
except ValueError:
raise ValidationError(message)


class ChoiceIntEnum(int, ChoiceEnum):
"""Class for creating an enum int choices."""
pass


class ChoiceStrEnum(str, ChoiceEnum):
"""Class for creating an enum string choices."""
pass
98 changes: 7 additions & 91 deletions django/db/models/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import uuid
import warnings
from base64 import b64decode, b64encode
from collections import OrderedDict
from enum import Enum, EnumMeta
from functools import partialmethod, total_ordering

from django import forms
Expand Down Expand Up @@ -35,14 +33,13 @@

__all__ = [
'AutoField', 'BLANK_CHOICE_DASH', 'BigAutoField', 'BigIntegerField',
'BinaryField', 'BooleanField', 'CharField', 'ChoiceEnum', 'ChoiceIntEnum',
'ChoiceStrEnum', 'CommaSeparatedIntegerField', 'DateField',
'DateTimeField', 'DecimalField', 'DurationField', 'EmailField', 'Empty',
'Field', 'FieldDoesNotExist', 'FilePathField', 'FloatField',
'GenericIPAddressField', 'IPAddressField', 'IntegerField', 'NOT_PROVIDED',
'NullBooleanField', 'PositiveIntegerField', 'PositiveSmallIntegerField',
'SlugField', 'SmallIntegerField', 'TextField', 'TimeField', 'URLField',
'UUIDField',
'BinaryField', 'BooleanField', 'CharField', 'CommaSeparatedIntegerField',
'DateField', 'DateTimeField', 'DecimalField', 'DurationField',
'EmailField', 'Empty', 'Field', 'FieldDoesNotExist', 'FilePathField',
'FloatField', 'GenericIPAddressField', 'IPAddressField', 'IntegerField',
'NOT_PROVIDED', 'NullBooleanField', 'PositiveIntegerField',
'PositiveSmallIntegerField', 'SlugField', 'SmallIntegerField', 'TextField',
'TimeField', 'URLField', 'UUIDField',
]


Expand All @@ -59,87 +56,6 @@ class NOT_PROVIDED:
BLANK_CHOICE_DASH = [("", "---------")]


class ChoiceEnumMeta(EnumMeta):
def __new__(metacls, classname, bases, classdict):
choices = {}
for key in classdict._member_names:
value = classdict[key]
if isinstance(value, (list, tuple)):
try:
value, display = value
except ValueError as e:
raise ValueError('Invalid ChoiceEnum member %r' % key) from e
else:
display = key.replace('_', ' ').title()
choices[value] = display
# Use dict.__setitem__() to suppress defenses against double
# assignment in enum's classdict.
dict.__setitem__(classdict, key, value)
cls = super().__new__(metacls, classname, bases, classdict)
cls._choices = choices
cls.choices = tuple(choices.items())
return cls


class ChoiceEnum(Enum, metaclass=ChoiceEnumMeta):
"""
A class suitable for using as an enum with translatable choices.
The typical use is similar to the stdlib's enums, with three
modifications:
* Instead of values in the enum, we use "(value, display)" tuples.
The "display" can be a lazy translatable string.
* We add a class method "choices()" which returns a value suitable
for use as "choices" in a Django field definition.
* We add a property "display" on enum values, to return the display
specified.
Thus, the definition of the Enum class can look like:
class YearInSchool(ChoiceStrEnum):
FRESHMAN = 'FR', _('Freshman')
SOPHOMORE = 'SO', _('Sophomore')
JUNIOR = 'JR', _('Junior')
SENIOR = 'SR', _('Senior')
or even
class Suit(ChoiceIntEnum):
DIAMOND = 1, _('Diamond')
SPADE = 2, _('Spade')
HEART = 3, _('Heart')
CLUB = 4, _('Club')
A field could be defined as
class Card(models.Model):
suit = models.IntegerField(choices=Suit.choices)
Suit.HEART, Suit['HEART'] and Suit(3) work as expected, while
Suit.HEART.display is a pretty, translatable string.
"""
@property
def display(self):
return self._choices[self]

@classmethod
def validate(cls, value, message=_('Invalid choice')):
try:
cls(value)
except ValueError:
raise exceptions.ValidationError(message)


class ChoiceIntEnum(int, ChoiceEnum):
"""Class for creating an enum int choices."""
pass


class ChoiceStrEnum(str, ChoiceEnum):
"""Class for creating an enum string choices."""
pass


def _load_field(app_label, model_name, field_name):
return apps.get_model(app_label, model_name)._meta.get_field(field_name)

Expand Down
2 changes: 1 addition & 1 deletion tests/model_fields/test_choiceenum.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def test_value_saved(self):
self.assertEquals(foo.a, '1')

def test_value_query(self):
foo = Foo.objects.create(a=self.StrEnum.ONE_PLUS_ONE, d=2.0)
Foo.objects.create(a=self.StrEnum.ONE_PLUS_ONE, d=2.0)
bar = Foo.objects.get(a=self.StrEnum.ONE_PLUS_ONE)
self.assertEquals(bar.a, self.StrEnum.ONE_PLUS_ONE)
self.assertEquals(bar.a, '2')
Expand Down

0 comments on commit 7b79203

Please sign in to comment.