Skip to content

Commit 309da69

Browse files
committed
implement get_default coercion and handle deconstructing defaults appropriately, add test cases and fix misc type hinting issues closes #4
1 parent 5388ff9 commit 309da69

File tree

10 files changed

+318
-38
lines changed

10 files changed

+318
-38
lines changed

django_enum/fields.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,16 @@ def __init__(
108108
kwargs.setdefault('choices', enum.choices if enum else [])
109109
super().__init__(*args, **kwargs)
110110

111-
def _try_coerce(self, value: Any, force: bool = False) -> Union[Choices, Any]:
111+
def _try_coerce(
112+
self,
113+
value: Any,
114+
force: bool = False
115+
) -> Union[Choices, Any]:
116+
"""
117+
Attempt coercion of value to enumeration type instance, if unsuccessful
118+
and non-strict, coercion to enum's primitive type will be done,
119+
otherwise a ValueError is raised.
120+
"""
112121
if (
113122
(self.coerce or force)
114123
and self.enum is not None
@@ -133,15 +142,33 @@ def _try_coerce(self, value: Any, force: bool = False) -> Union[Choices, Any]:
133142

134143
def deconstruct(self) -> Tuple[str, str, List, dict]:
135144
"""
136-
Preserve enum class for migrations. Strict is omitted because
137-
reconstructed fields are *always* non-strict sense enum is null.
145+
Preserve field migrations. Strict and coerce are omitted because
146+
reconstructed fields are *always* non-strict and coerce is always
147+
False.
148+
149+
.. warning::
150+
151+
Do not add enum to kwargs! It is important that migration files not
152+
reference enum classes that might be removed from the code base in
153+
the future as this would break older migration files! We simply use
154+
the choices tuple, which is plain old data and entirely sufficient
155+
to de/reconstruct our field.
138156
139157
See deconstruct_
140158
"""
141159
name, path, args, kwargs = super().deconstruct()
142160
if self.enum is not None:
143161
kwargs['choices'] = self.enum.choices
144162

163+
if 'default' in kwargs:
164+
# ensure default in deconstructed fields is always the primitive
165+
# value type
166+
kwargs['default'] = getattr(
167+
self.get_default(),
168+
'value',
169+
self.get_default()
170+
)
171+
145172
return name, path, args, kwargs
146173

147174
def get_prep_value(self, value: Any) -> Any:
@@ -210,6 +237,15 @@ def to_python(self, value: Any) -> Union[Choices, Any]:
210237
f"{self.enum.__name__ if self.enum else ''}."
211238
) from err
212239

240+
def get_default(self) -> Any:
241+
"""Wrap get_default in an enum type coercion attempt"""
242+
if self.has_default():
243+
try:
244+
return self.to_python(super().get_default())
245+
except ValidationError:
246+
return super().get_default()
247+
return super().get_default()
248+
213249
def validate(self, value: Any, model_instance: Model):
214250
"""
215251
Validates the field as part of model clean routines. Runs super class
@@ -237,6 +273,9 @@ def validate(self, value: Any, model_instance: Model):
237273
params={'value': value}
238274
) from err
239275

276+
# def formfield(self, form_class=None, choices_form_class=None, **kwargs):
277+
# pass
278+
240279

241280
class EnumCharField(EnumMixin, CharField):
242281
"""
@@ -302,7 +341,10 @@ class _EnumFieldMetaClass(type):
302341

303342
SUPPORTED_PRIMITIVES = {int, str, float}
304343

305-
def __new__(mcs, enum: Choices) -> Field: # pylint: disable=R0911
344+
def __new__( # pylint: disable=R0911
345+
mcs,
346+
enum: Type[Choices]
347+
) -> Type[EnumMixin]:
306348
"""
307349
Construct a new Django Field class given the Enumeration class. The
308350
correct Django field class to inherit from is determined based on the
@@ -347,7 +389,7 @@ def EnumField( # pylint: disable=C0103
347389
enum: Type[Choices],
348390
*field_args,
349391
**field_kwargs
350-
) -> Field:
392+
) -> EnumMixin:
351393
"""
352394
*This is a function, not a type*. Some syntactic sugar that wraps the enum
353395
field metaclass so that we can cleanly create enums like so:

django_enum/filters.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from django.db.models import Field as ModelField
66
from django.forms.fields import Field as FormField
7-
from django_enum.fields import EnumMixin
87
from django_enum.forms import EnumChoiceField
98

109
try:
@@ -56,7 +55,7 @@ def filter_for_lookup(
5655
lookup_type: str
5756
) -> Tuple[Type[Filter], dict]:
5857
"""For EnumFields use the EnumFilter class by default"""
59-
if isinstance(field, EnumMixin) and getattr(field, 'enum', None):
58+
if hasattr(field, 'enum') and hasattr(field.enum, 'choices'):
6059
return EnumFilter, {'enum': field.enum}
6160
return super().filter_for_lookup(field, lookup_type)
6261

django_enum/forms.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ def _coerce(self, value: Any) -> Union[Choices, Any]:
120120
one of our empty_values, or the value itself if this is a
121121
non-strict field and the value is of a matching primitive type
122122
"""
123-
124123
if value in self.empty_values:
125124
return self.empty_value
126125
if (

django_enum/tests/djenum/migrations/0001_initial.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 3.2.14 on 2022-08-09 23:57
1+
# Generated by Django 3.2.15 on 2022-08-12 04:26
22

33
import django_enum.fields
44
from django.db import migrations, models
@@ -31,7 +31,7 @@ class Migration(migrations.Migration):
3131
('char_field', models.CharField(blank=True, default='A', max_length=1)),
3232
('dj_int_enum', django_enum.fields.EnumPositiveSmallIntegerField(choices=[(1, 'One'), (2, 'Two'), (3, 'Three')], default=1)),
3333
('dj_text_enum', django_enum.fields.EnumCharField(choices=[('A', 'Label A'), ('B', 'Label B'), ('C', 'Label C')], default='A', max_length=1)),
34-
('non_strict_int', django_enum.fields.EnumPositiveSmallIntegerField(blank=True, choices=[(0, 'Value 1'), (2, 'Value 2'), (32767, 'Value 32767')], default=None, null=True)),
34+
('non_strict_int', django_enum.fields.EnumPositiveSmallIntegerField(blank=True, choices=[(0, 'Value 1'), (2, 'Value 2'), (32767, 'Value 32767')], default=5, null=True)),
3535
('non_strict_text', django_enum.fields.EnumCharField(blank=True, choices=[('V1', 'Value1'), ('V22', 'Value2'), ('V333', 'Value3'), ('D', 'Default')], default='', max_length=12)),
3636
('no_coerce', django_enum.fields.EnumPositiveSmallIntegerField(blank=True, choices=[(0, 'Value 1'), (2, 'Value 2'), (32767, 'Value 32767')], default=None, null=True)),
3737
],

django_enum/tests/djenum/models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ class EnumTester(models.Model):
2020
small_pos_int = EnumField(SmallPosIntEnum, null=True, default=None, db_index=True, blank=True)
2121
small_int = EnumField(SmallIntEnum, null=False, default=SmallIntEnum.VAL3, db_index=True, blank=True)
2222

23-
pos_int = EnumField(PosIntEnum, default=PosIntEnum.VAL3, db_index=True, blank=True)
23+
pos_int = EnumField(PosIntEnum, default=2147483647, db_index=True, blank=True)
2424
int = EnumField(IntEnum, null=True, db_index=True, blank=True)
2525

2626
big_pos_int = EnumField(BigPosIntEnum, null=True, default=None, db_index=True, blank=True)
27-
big_int = EnumField(BigIntEnum, default=BigIntEnum.VAL0, db_index=True, blank=True)
27+
big_int = EnumField(BigIntEnum, default=-2147483649, db_index=True, blank=True)
2828

2929
constant = EnumField(Constants, null=True, default=None, db_index=True, blank=True)
3030

@@ -67,14 +67,14 @@ class EnumTester(models.Model):
6767
################################################
6868

6969
dj_int_enum = EnumField(DJIntEnum, default=DJIntEnum.ONE)
70-
dj_text_enum = EnumField(DJTextEnum, default=DJTextEnum.A)
70+
dj_text_enum = EnumField(DJTextEnum, default='A')
7171

7272
# Non-strict
7373
non_strict_int = EnumField(
7474
SmallPosIntEnum,
7575
strict=False,
7676
null=True,
77-
default=None,
77+
default=5,
7878
blank=True
7979
)
8080

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.db import models
2+
from django_enum import EnumField, TextChoices
3+
from enum_properties import s
4+
5+
6+
class MigrationTester(models.Model):
7+
8+
class IntEnum(models.TextChoices):
9+
A = 'A', 'One'
10+
B = 'B', 'Two',
11+
C = 'C', 'Three'
12+
13+
class Color(TextChoices, s('rgb'), s('hex', case_fold=True)):
14+
RED = 'R', 'Red', (1, 0, 0), 'ff0000'
15+
GREEN = 'G', 'Green', (0, 1, 0), '00ff00'
16+
BLUE = 'B', 'Blue', (0, 0, 1), '0000ff'
17+
BLACK = 'K', 'Black', (0, 0, 0), '000000'
18+
19+
int_enum = EnumField(IntEnum, null=True, default=None)
20+
21+
# set default
22+
color = EnumField(Color, default='000000')
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from django.db import models
2+
from django_enum import EnumField, TextChoices
3+
from enum_properties import s
4+
5+
6+
class MigrationTester(models.Model):
7+
8+
class IntEnum(models.TextChoices):
9+
A = 'A', 'One'
10+
B = 'B', 'Two',
11+
C = 'C', 'Three'
12+
13+
class Color(TextChoices, s('rgb'), s('hex', case_fold=True)):
14+
RED = 'R', 'Red', (1, 0, 0), 'ff0000'
15+
GREEN = 'G', 'Green', (0, 1, 0), '00ff00'
16+
17+
# change meaning of default indirectly
18+
BLUE = 'B', 'Blue', (0, 0, 1), '000000'
19+
BLACK = 'K', 'Black', (0, 0, 0), '0000ff'
20+
21+
int_enum = EnumField(IntEnum, null=True, default=None)
22+
23+
# default value unchanged
24+
color = EnumField(Color, default='000000')

django_enum/tests/enum_prop/migrations/0001_initial.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 3.2.14 on 2022-08-09 23:57
1+
# Generated by Django 3.2.15 on 2022-08-12 04:26
22

33
import django_enum.fields
44
from django.db import migrations, models
@@ -31,7 +31,7 @@ class Migration(migrations.Migration):
3131
('char_field', models.CharField(blank=True, default='A', max_length=1)),
3232
('dj_int_enum', django_enum.fields.EnumPositiveSmallIntegerField(choices=[(1, 'One'), (2, 'Two'), (3, 'Three')], default=1)),
3333
('dj_text_enum', django_enum.fields.EnumCharField(choices=[('A', 'Label A'), ('B', 'Label B'), ('C', 'Label C')], default='A', max_length=1)),
34-
('non_strict_int', django_enum.fields.EnumPositiveSmallIntegerField(blank=True, choices=[(0, 'Value 1'), (2, 'Value 2'), (32767, 'Value 32767')], default=None, null=True)),
34+
('non_strict_int', django_enum.fields.EnumPositiveSmallIntegerField(blank=True, choices=[(0, 'Value 1'), (2, 'Value 2'), (32767, 'Value 32767')], default=5, null=True)),
3535
('non_strict_text', django_enum.fields.EnumCharField(blank=True, choices=[('V1', 'Value1'), ('V22', 'Value2'), ('V333', 'Value3'), ('D', 'Default')], default='', max_length=12)),
3636
('no_coerce', django_enum.fields.EnumPositiveSmallIntegerField(blank=True, choices=[(0, 'Value 1'), (2, 'Value 2'), (32767, 'Value 32767')], default=None, null=True)),
3737
],

django_enum/tests/enum_prop/models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
class EnumTester(models.Model):
2121

2222
small_pos_int = EnumField(SmallPosIntEnum, null=True, default=None, db_index=True, blank=True)
23-
small_int = EnumField(SmallIntEnum, null=False, default=SmallIntEnum.VAL3, db_index=True, blank=True)
23+
small_int = EnumField(SmallIntEnum, null=False, default='Value 32767', db_index=True, blank=True)
2424

25-
pos_int = EnumField(PosIntEnum, default=PosIntEnum.VAL3, db_index=True, blank=True)
25+
pos_int = EnumField(PosIntEnum, default=2147483647, db_index=True, blank=True)
2626
int = EnumField(IntEnum, null=True, db_index=True, blank=True)
2727

2828
big_pos_int = EnumField(BigPosIntEnum, null=True, default=None, db_index=True, blank=True)
29-
big_int = EnumField(BigIntEnum, default=BigIntEnum.VAL0, db_index=True, blank=True)
29+
big_int = EnumField(BigIntEnum, default=BigPosIntEnum.VAL0, db_index=True, blank=True)
3030

3131
constant = EnumField(Constants, null=True, default=None, db_index=True, blank=True)
3232

@@ -76,7 +76,7 @@ class EnumTester(models.Model):
7676
SmallPosIntEnum,
7777
strict=False,
7878
null=True,
79-
default=None,
79+
default=5,
8080
blank=True
8181
)
8282
non_strict_text = EnumField(

0 commit comments

Comments
 (0)