Skip to content

Commit 90d4b73

Browse files
committed
fixes #55
1 parent 9e84547 commit 90d4b73

File tree

6 files changed

+179
-31
lines changed

6 files changed

+179
-31
lines changed

django_enum/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
'EnumFilter'
4848
]
4949

50-
VERSION = (1, 2, 3)
50+
VERSION = (1, 3, 0)
5151

5252
__title__ = 'Django Enum'
5353
__version__ = '.'.join(str(i) for i in VERSION)

django_enum/fields.py

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -83,20 +83,10 @@ class EnumMixin(
8383
enum: Optional[Type[Enum]] = None
8484
strict: bool = True
8585
coerce: bool = True
86-
primitive: Optional[Type[Any]] = None
86+
primitive: Type[Any]
8787

8888
descriptor_class = ToPythonDeferredAttribute
8989

90-
def _coerce_to_value_type(self, value: Any) -> Enum:
91-
"""Coerce the value to the enumerations value type"""
92-
# note if enum type is int and a floating point is passed we could get
93-
# situations like X.xxx == X - this is acceptable
94-
if self.enum and self.primitive:
95-
return self.primitive(value)
96-
# can't ever reach this - just here to make type checker happy
97-
return value # pragma: no cover
98-
99-
10090
def __init__(
10191
self,
10292
*args,
@@ -110,7 +100,6 @@ def __init__(
110100
self.coerce = coerce if enum else False
111101
if self.enum is not None:
112102
kwargs.setdefault('choices', choices(enum))
113-
self.primitive = type(values(self.enum)[0])
114103
super().__init__(*args, **kwargs)
115104

116105
def _try_coerce(
@@ -123,42 +112,41 @@ def _try_coerce(
123112
and non-strict, coercion to enum's primitive type will be done,
124113
otherwise a ValueError is raised.
125114
"""
126-
if (
127-
(self.coerce or force)
128-
and self.enum is not None
129-
and not isinstance(value, self.enum)
130-
):
115+
if self.enum is None:
116+
return value
117+
118+
if (self.coerce or force) and not isinstance(value, self.enum):
131119
try:
132120
value = self.enum(value)
133121
except (TypeError, ValueError):
134122
try:
135-
value = self._coerce_to_value_type(value)
136-
value = self.enum(value)
123+
value = self.enum(value:=self.primitive(value))
137124
except (TypeError, ValueError):
138125
try:
139126
value = self.enum[value]
140127
except KeyError as err:
141128
if self.strict or not isinstance(
142129
value,
143-
type(values(self.enum)[0])
130+
self.primitive
144131
):
145132
raise ValueError(
146133
f"'{value}' is not a valid "
147134
f"{self.enum.__name__} "
148135
f"required by field {self.name}."
149136
) from err
150137
elif (
151-
not self.coerce and self.primitive and
138+
not self.coerce and
152139
not isinstance(value, self.primitive) and
153-
self.enum and not isinstance(value, self.enum)
140+
not isinstance(value, self.enum)
154141
):
155142
try:
156-
value = self._coerce_to_value_type(value)
143+
return self.primitive(value)
157144
except (TypeError, ValueError) as err:
158145
raise ValueError(
159-
f"'{value}' is not a valid {self.primitive} "
146+
f"'{value}' is not coercible to {self.primitive.__name__} "
160147
f"required by field {self.name}."
161148
) from err
149+
162150
return value
163151

164152
def deconstruct(self) -> Tuple[str, str, List, dict]:
@@ -320,6 +308,8 @@ class EnumCharField(EnumMixin, CharField):
320308
A database field supporting enumerations with character values.
321309
"""
322310

311+
primitive = str
312+
323313
def __init__(self, *args, enum=None, **kwargs):
324314
kwargs.setdefault(
325315
'max_length',
@@ -334,48 +324,61 @@ def __init__(self, *args, enum=None, **kwargs):
334324
class EnumFloatField(EnumMixin, FloatField):
335325
"""A database field supporting enumerations with floating point values"""
336326

327+
primitive = float
328+
337329

338330
class EnumSmallIntegerField(EnumMixin, SmallIntegerField):
339331
"""
340332
A database field supporting enumerations with integer values that fit into
341333
2 bytes or fewer
342334
"""
343335

336+
primitive = int
337+
344338

345339
class EnumPositiveSmallIntegerField(EnumMixin, PositiveSmallIntegerField):
346340
"""
347341
A database field supporting enumerations with positive (but signed) integer
348342
values that fit into 2 bytes or fewer
349343
"""
350344

345+
primitive = int
351346

352347
class EnumIntegerField(EnumMixin, IntegerField):
353348
"""
354349
A database field supporting enumerations with integer values that fit into
355350
32 bytes or fewer
356351
"""
357352

353+
primitive = int
354+
358355

359356
class EnumPositiveIntegerField(EnumMixin, PositiveIntegerField):
360357
"""
361358
A database field supporting enumerations with positive (but signed) integer
362359
values that fit into 32 bytes or fewer
363360
"""
364361

362+
primitive = int
363+
365364

366365
class EnumBigIntegerField(EnumMixin, BigIntegerField):
367366
"""
368367
A database field supporting enumerations with integer values that fit into
369368
64 bytes or fewer
370369
"""
371370

371+
primitive = int
372+
372373

373374
class EnumPositiveBigIntegerField(EnumMixin, PositiveBigIntegerField):
374375
"""
375376
A database field supporting enumerations with positive (but signed) integer
376377
values that fit into 64 bytes or fewer
377378
"""
378379

380+
primitive = int
381+
379382

380383
class _EnumFieldMetaClass(type):
381384

django_enum/tests/tests.py

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from django_enum import TextChoices
1717
from django_enum.choices import choices, labels, names, values
1818
from django_enum.forms import EnumChoiceField # dont remove this
19+
from django.forms import Form, ModelForm
1920
# from django_enum.tests.djenum.enums import (
2021
# BigIntEnum,
2122
# BigPosIntEnum,
@@ -414,6 +415,28 @@ def test_basic_save(self):
414415
)
415416
self.MODEL_CLASS.objects.all().delete()
416417

418+
def test_coerce_to_primitive(self):
419+
420+
create_params = {
421+
**self.create_params,
422+
'no_coerce': '32767'
423+
}
424+
425+
tester = self.MODEL_CLASS.objects.create(**create_params)
426+
427+
self.assertIsInstance(tester.no_coerce, int)
428+
self.assertEqual(tester.no_coerce, 32767)
429+
430+
def test_coerce_to_primitive_error(self):
431+
432+
create_params = {
433+
**self.create_params,
434+
'no_coerce': 'Value 32767'
435+
}
436+
437+
with self.assertRaises(ValueError):
438+
self.MODEL_CLASS.objects.create(**create_params)
439+
417440
def test_to_python_deferred_attribute(self):
418441
obj = self.MODEL_CLASS.objects.create(**self.create_params)
419442
with self.assertNumQueries(1):
@@ -1813,6 +1836,109 @@ def test_bulk_update(self):
18131836
)
18141837

18151838

1839+
class FormTests(EnumTypeMixin, TestCase):
1840+
"""
1841+
Some more explicit form tests that allow easier access to other internal workflows.
1842+
"""
1843+
1844+
MODEL_CLASS = EnumTester
1845+
1846+
@property
1847+
def model_form_class(self):
1848+
1849+
class EnumTesterForm(ModelForm):
1850+
1851+
class Meta:
1852+
model = self.MODEL_CLASS
1853+
fields = '__all__'
1854+
1855+
return EnumTesterForm
1856+
1857+
@property
1858+
def basic_form_class(self):
1859+
from django.core.validators import MinValueValidator, MaxValueValidator
1860+
1861+
class BasicForm(Form):
1862+
1863+
small_pos_int = EnumChoiceField(self.SmallPosIntEnum)
1864+
small_int = EnumChoiceField(self.SmallIntEnum)
1865+
pos_int = EnumChoiceField(self.PosIntEnum)
1866+
int = EnumChoiceField(self.IntEnum)
1867+
big_pos_int = EnumChoiceField(self.BigPosIntEnum)
1868+
big_int = EnumChoiceField(self.BigIntEnum)
1869+
constant = EnumChoiceField(self.Constants)
1870+
text = EnumChoiceField(self.TextEnum)
1871+
extern = EnumChoiceField(self.ExternEnum)
1872+
dj_int_enum = EnumChoiceField(self.DJIntEnum)
1873+
dj_text_enum = EnumChoiceField(self.DJTextEnum)
1874+
non_strict_int = EnumChoiceField(self.SmallPosIntEnum, strict=False)
1875+
non_strict_text = EnumChoiceField(self.TextEnum, strict=False)
1876+
no_coerce = EnumChoiceField(
1877+
self.SmallPosIntEnum,
1878+
validators=[MinValueValidator(0), MaxValueValidator(32767)]
1879+
)
1880+
1881+
return BasicForm
1882+
1883+
@property
1884+
def test_params(self):
1885+
return {
1886+
'small_pos_int': self.SmallPosIntEnum.VAL2,
1887+
'small_int': self.SmallIntEnum.VALn1,
1888+
'pos_int': self.PosIntEnum.VAL3,
1889+
'int': self.IntEnum.VALn1,
1890+
'big_pos_int': self.BigPosIntEnum.VAL3,
1891+
'big_int': self.BigIntEnum.VAL2,
1892+
'constant': self.Constants.GOLDEN_RATIO,
1893+
'text': self.TextEnum.VALUE2,
1894+
'extern': self.ExternEnum.TWO,
1895+
'dj_int_enum': self.DJIntEnum.THREE,
1896+
'dj_text_enum': self.DJTextEnum.A,
1897+
'non_strict_int': '15',
1898+
'non_strict_text': 'arbitrary',
1899+
'no_coerce': self.SmallPosIntEnum.VAL3
1900+
}
1901+
1902+
@property
1903+
def test_data_strings(self):
1904+
return {key: str(value) for key, value in self.test_params.items()}
1905+
1906+
@property
1907+
def expected(self):
1908+
return {
1909+
**self.test_params,
1910+
'non_strict_int': int(self.test_params['non_strict_int']),
1911+
}
1912+
1913+
def test_modelform_binding(self):
1914+
form = self.model_form_class(data=self.test_data_strings)
1915+
1916+
form.full_clean()
1917+
self.assertTrue(form.is_valid())
1918+
1919+
for key, value in self.expected.items():
1920+
self.assertEqual(form.cleaned_data[key], value)
1921+
1922+
self.assertIsInstance(form.cleaned_data['no_coerce'], int)
1923+
self.assertIsInstance(form.cleaned_data['non_strict_int'], int)
1924+
1925+
obj = form.save()
1926+
1927+
for key, value in self.expected.items():
1928+
self.assertEqual(getattr(obj, key), value)
1929+
1930+
def test_basicform_binding(self):
1931+
form = self.basic_form_class(data=self.test_data_strings)
1932+
form.full_clean()
1933+
self.assertTrue(form.is_valid())
1934+
1935+
for key, value in self.expected.items():
1936+
self.assertEqual(form.cleaned_data[key], value)
1937+
1938+
self.assertIsInstance(form.cleaned_data['no_coerce'], int)
1939+
self.assertIsInstance(form.cleaned_data['non_strict_int'], int)
1940+
1941+
18161942
if ENUM_PROPERTIES_INSTALLED:
18171943

18181944
from django_enum.forms import EnumChoiceField
@@ -1843,6 +1969,9 @@ def test_bulk_update(self):
18431969
)
18441970
from enum_properties import EnumProperties, s
18451971

1972+
class EnumPropertiesFormTests(FormTests):
1973+
1974+
MODEL_CLASS = EnumTester
18461975

18471976
class TestEnumPropertiesIntegration(TestCase):
18481977

@@ -3362,6 +3491,22 @@ def test_validate(self):
33623491
self.assertTrue(tester._meta.get_field('dj_text_enum').validate('A', tester) is None)
33633492
self.assertTrue(tester._meta.get_field('non_strict_int').validate(20, tester) is None)
33643493

3494+
def test_coerce_to_primitive_error(self):
3495+
"""
3496+
Override this base class test because this should work with symmetrical enum.
3497+
"""
3498+
create_params = {
3499+
**self.create_params,
3500+
'no_coerce': 'Value 32767'
3501+
}
3502+
3503+
tester = self.MODEL_CLASS.objects.create(**create_params)
3504+
self.assertEqual(tester.no_coerce, self.SmallPosIntEnum.VAL3)
3505+
self.assertEqual(tester.no_coerce, 'Value 32767')
3506+
3507+
tester.refresh_from_db()
3508+
self.assertEqual(tester.no_coerce, 32767)
3509+
33653510
class PerformanceTest(TestCase):
33663511
"""
33673512
We intentionally test bulk operations performance because thats what
@@ -3660,7 +3805,6 @@ def test_color(self):
36603805
).first() == instance
36613806
)
36623807

3663-
from django.forms import ModelForm
36643808
from django_enum import EnumChoiceField
36653809

36663810
class TextChoicesExampleForm(ModelForm):
@@ -3728,4 +3872,3 @@ def test_no_coerce(self):
37283872

37293873
else: # pragma: no cover
37303874
pass
3731-

doc/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ sphinxcontrib-htmlhelp==2.0.1; python_version >= "3.5"
66
sphinxcontrib-jsmath==1.0.1; python_version >= "3.5"
77
sphinxcontrib-qthelp==1.0.3; python_version >= "3.5"
88
sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.5"
9-
django-enum==1.2.3
9+
django-enum==1.3.0

doc/source/changelog.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
Change Log
33
==========
44

5-
v1.2.3
5+
v1.3.0
66
======
77

8-
* Added `Support Django 5.0 <https://github.com/bckohan/django-enum/issues/54>`_
8+
* Implemented `Support db_default <https://github.com/bckohan/django-enum/issues/56>`_
9+
* Fixed `When coerce=False, enum form fields and model fields should still coerce to the enum's primitive type. <https://github.com/bckohan/django-enum/issues/55>`_
10+
* Implemented `Support Django 5.0 <https://github.com/bckohan/django-enum/issues/54>`_
911

1012
v1.2.2
1113
======

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-enum"
3-
version = "1.2.3"
3+
version = "1.3.0"
44
description = "Full and natural support for enumerations as Django model fields."
55
authors = ["Brian Kohan <[email protected]>"]
66
license = "MIT"

0 commit comments

Comments
 (0)