Skip to content

Commit 64c9aea

Browse files
committed
add integration tests for all examples and implement coerce parameter
1 parent 8e0533f commit 64c9aea

File tree

6 files changed

+220
-40
lines changed

6 files changed

+220
-40
lines changed

README.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ possible very rich enumeration fields.
100100
from django_enum import TextChoices # use instead of Django's TextChoices
101101
from django.db import models
102102
103-
class MyModel(models.Model):
103+
class TextChoicesExample(models.Model):
104104
105105
class Color(TextChoices, s('rgb'), s('hex', case_fold=True)):
106106
@@ -115,8 +115,12 @@ possible very rich enumeration fields.
115115
116116
color = EnumField(Color)
117117
118-
instance = MyModel.objects.create(color=MyModel.Color('FF0000'))
119-
assert instance.color == MyModel.Color('Red') == MyModel.Color('R') == MyModel.Color((1, 0, 0))
118+
instance = TextChoicesExample.objects.create(
119+
color=TextChoicesExample.Color('FF0000')
120+
)
121+
assert instance.color == TextChoicesExample.Color('Red')
122+
assert instance.color == TextChoicesExample.Color('R')
123+
assert instance.color == TextChoicesExample.Color((1, 0, 0))
120124
121125
# direct comparison to any symmetric value also works
122126
assert instance.color == 'Red'

django_enum/fields.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class EnumMixin(
6161

6262
enum: Optional[Type[Choices]] = None
6363
strict: bool = True
64+
coerce: bool = True
6465

6566
def _coerce_to_value_type(self, value: Any) -> Choices:
6667
"""Coerce the value to the enumerations value type"""
@@ -76,16 +77,22 @@ def __init__(
7677
*args,
7778
enum: Optional[Type[Choices]] = None,
7879
strict: bool = strict,
80+
coerce: bool = coerce,
7981
**kwargs
8082
):
8183
self.enum = enum
8284
self.strict = strict if enum else False
85+
self.coerce = coerce if enum else False
8386
if self.enum is not None:
8487
kwargs.setdefault('choices', enum.choices if enum else [])
8588
super().__init__(*args, **kwargs)
8689

87-
def _try_coerce(self, value: Any) -> Union[Choices, Any]:
88-
if self.enum is not None and not isinstance(value, self.enum):
90+
def _try_coerce(self, value: Any, force: bool = False) -> Union[Choices, Any]:
91+
if (
92+
(self.coerce or force)
93+
and self.enum is not None
94+
and not isinstance(value, self.enum)
95+
):
8996
try:
9097
value = self.enum(value)
9198
except (TypeError, ValueError):
@@ -167,7 +174,7 @@ def to_python(self, value: Any) -> Union[Choices, Any]:
167174
:raises ValidationError: If the value is not mappable to a valid
168175
enumeration
169176
"""
170-
if self.enum is None or isinstance(value, self.enum) or value is None:
177+
if value is None:
171178
return value
172179

173180
try:
@@ -196,10 +203,10 @@ def validate(self, value: Any, model_instance: Model):
196203
if err.code != 'invalid_choice':
197204
raise err
198205
try:
199-
self.to_python(value)
200-
except ValidationError as err:
206+
self._try_coerce(value, force=True)
207+
except ValueError as err:
201208
raise ValidationError(
202-
err.message,
209+
str(err),
203210
code='invalid_choice',
204211
params={'value': value}
205212
) from err

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# Generated by Django 3.2.14 on 2022-08-09 02:32
1+
# Generated by Django 3.2.14 on 2022-08-09 03:04
22

3-
import django_enum.fields
43
from django.db import migrations, models
4+
import django_enum.fields
55

66

77
class Migration(migrations.Migration):
@@ -19,4 +19,33 @@ class Migration(migrations.Migration):
1919
('style', django_enum.fields.EnumPositiveSmallIntegerField(choices=[(1, 'Streets'), (2, 'Outdoors'), (3, 'Light'), (4, 'Dark'), (5, 'Satellite'), (6, 'Satellite Streets'), (7, 'Navigation Day'), (8, 'Navigation Night')], default=1)),
2020
],
2121
),
22+
migrations.CreateModel(
23+
name='MyModel',
24+
fields=[
25+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
26+
('txt_enum', django_enum.fields.EnumCharField(blank=True, choices=[('V0', 'Value 0'), ('V1', 'Value 1'), ('V2', 'Value 2')], max_length=2, null=True)),
27+
('int_enum', django_enum.fields.EnumPositiveSmallIntegerField(choices=[(1, 'One'), (2, 'Two'), (3, 'Three')])),
28+
],
29+
),
30+
migrations.CreateModel(
31+
name='NoCoerceExample',
32+
fields=[
33+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34+
('non_strict', django_enum.fields.EnumCharField(choices=[('1', 'One'), ('2', 'Two')], max_length=10)),
35+
],
36+
),
37+
migrations.CreateModel(
38+
name='StrictExample',
39+
fields=[
40+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
41+
('non_strict', django_enum.fields.EnumCharField(choices=[('1', 'One'), ('2', 'Two')], max_length=10)),
42+
],
43+
),
44+
migrations.CreateModel(
45+
name='TextChoicesExample',
46+
fields=[
47+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
48+
('color', django_enum.fields.EnumCharField(choices=[('R', 'Red'), ('G', 'Green'), ('B', 'Blue')], max_length=1)),
49+
],
50+
),
2251
]

django_enum/tests/examples/models.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.db import models
2-
from django_enum import EnumField, IntegerChoices
2+
from django_enum import EnumField, IntegerChoices, TextChoices
33
from enum_properties import p, s
44

55

@@ -34,3 +34,75 @@ def __str__(self):
3434
return self.uri
3535

3636
style = EnumField(MapBoxStyle, default=MapBoxStyle.STREETS)
37+
38+
39+
class StrictExample(models.Model):
40+
41+
class EnumType(TextChoices):
42+
43+
ONE = '1', 'One'
44+
TWO = '2', 'Two'
45+
46+
non_strict = EnumField(
47+
EnumType,
48+
strict=False,
49+
# it might be necessary to override max_length also, otherwise
50+
# max_length will be 1
51+
max_length=10
52+
)
53+
54+
55+
class NoCoerceExample(models.Model):
56+
57+
class EnumType(TextChoices):
58+
59+
ONE = '1', 'One'
60+
TWO = '2', 'Two'
61+
62+
non_strict = EnumField(
63+
EnumType,
64+
strict=False,
65+
coerce=False,
66+
# it might be necessary to override max_length also, otherwise
67+
# max_length will be 1
68+
max_length=10
69+
)
70+
71+
72+
class TextChoicesExample(models.Model):
73+
74+
class Color(TextChoices, s('rgb'), s('hex', case_fold=True)):
75+
76+
# name value label rgb hex
77+
RED = 'R', 'Red', (1, 0, 0), 'ff0000'
78+
GREEN = 'G', 'Green', (0, 1, 0), '00ff00'
79+
BLUE = 'B', 'Blue', (0, 0, 1), '0000ff'
80+
81+
# any named s() values in the Enum's inheritance become properties on
82+
# each value, and the enumeration value may be instantiated from the
83+
# property's value
84+
85+
color = EnumField(Color)
86+
87+
88+
class MyModel(models.Model):
89+
90+
class TextEnum(models.TextChoices):
91+
92+
VALUE0 = 'V0', 'Value 0'
93+
VALUE1 = 'V1', 'Value 1'
94+
VALUE2 = 'V2', 'Value 2'
95+
96+
class IntEnum(models.IntegerChoices):
97+
98+
ONE = 1, 'One'
99+
TWO = 2, 'Two',
100+
THREE = 3, 'Three'
101+
102+
# this is equivalent to:
103+
# CharField(max_length=2, choices=TextEnum.choices, null=True, blank=True)
104+
txt_enum = EnumField(TextEnum, null=True, blank=True)
105+
106+
# this is equivalent to
107+
# PositiveSmallIntegerField(choices=IntEnum.choices)
108+
int_enum = EnumField(IntEnum)

django_enum/tests/tests.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2729,5 +2729,70 @@ def test_mapboxstyle(self):
27292729
'mapbox://styles/mapbox/satellite-streets-v11'
27302730
)
27312731

2732+
def test_color(self):
2733+
from django_enum.tests.examples.models import TextChoicesExample
2734+
2735+
instance = TextChoicesExample.objects.create(
2736+
color=TextChoicesExample.Color('FF0000')
2737+
)
2738+
self.assertTrue(
2739+
instance.color == TextChoicesExample.Color('Red') ==
2740+
TextChoicesExample.Color('R') == TextChoicesExample.Color((1, 0, 0))
2741+
)
2742+
2743+
# direct comparison to any symmetric value also works
2744+
self.assertTrue(instance.color == 'Red')
2745+
self.assertTrue(instance.color == 'R')
2746+
self.assertTrue(instance.color == (1, 0, 0))
2747+
2748+
# save by any symmetric value
2749+
instance.color = 'FF0000'
2750+
instance.full_clean()
2751+
self.assertTrue(instance.color.hex == 'ff0000')
2752+
instance.save()
2753+
2754+
def test_strict(self):
2755+
from django_enum.tests.examples.models import StrictExample
2756+
obj = StrictExample()
2757+
2758+
# set to a valid EnumType value
2759+
obj.non_strict = '1'
2760+
obj.full_clean()
2761+
# when accessed from the db or after clean, will be an EnumType instance
2762+
self.assertTrue(obj.non_strict is StrictExample.EnumType.ONE)
2763+
2764+
# we can also store any string less than or equal to length 10
2765+
obj.non_strict = 'arbitrary'
2766+
obj.full_clean() # no errors
2767+
# when accessed will be a str instance
2768+
self.assertTrue(obj.non_strict == 'arbitrary')
2769+
2770+
def test_basic(self):
2771+
from django_enum.tests.examples.models import MyModel
2772+
instance = MyModel.objects.create(
2773+
txt_enum=MyModel.TextEnum.VALUE1,
2774+
int_enum=3 # by-value assignment also works
2775+
)
2776+
instance.refresh_from_db()
2777+
2778+
self.assertTrue(instance.txt_enum == MyModel.TextEnum('V1'))
2779+
self.assertTrue(instance.txt_enum.label == 'Value 1')
2780+
2781+
self.assertTrue(instance.int_enum == MyModel.IntEnum['THREE'])
2782+
self.assertTrue(instance.int_enum.value == 3)
2783+
2784+
def test_no_coerce(self):
2785+
from django_enum.tests.examples.models import NoCoerceExample
2786+
2787+
obj = NoCoerceExample()
2788+
# set to a valid EnumType value
2789+
obj.non_strict = '1'
2790+
obj.full_clean()
2791+
2792+
# when accessed from the db or after clean, will be the primitive value
2793+
self.assertTrue(obj.non_strict == '1')
2794+
self.assertTrue(isinstance(obj.non_strict, str))
2795+
self.assertFalse(isinstance(obj.non_strict, NoCoerceExample.EnumType))
2796+
27322797
else: # pragma: no cover
27332798
pass

doc/source/usage.rst

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ data where no ``Enum`` type coercion is possible.
2929

3030
.. code-block:: python
3131
32-
class MyModel(models.Model):
32+
class StrictExample(models.Model):
3333
3434
class EnumType(TextChoices):
3535
@@ -44,13 +44,13 @@ data where no ``Enum`` type coercion is possible.
4444
max_length=10
4545
)
4646
47-
obj = MyModel()
47+
obj = StrictExample()
4848
4949
# set to a valid EnumType value
5050
obj.non_strict = '1'
5151
obj.full_clean()
5252
# when accessed from the db or after clean, will be an EnumType instance
53-
assert obj.non_strict is MyModel.EnumType.ONE
53+
assert obj.non_strict is StrictExample.EnumType.ONE
5454
5555
# we can also store any string less than or equal to length 10
5656
obj.non_strict = 'arbitrary'
@@ -59,33 +59,32 @@ data where no ``Enum`` type coercion is possible.
5959
assert obj.non_strict == 'arbitrary'
6060
6161
62-
..
63-
``coerce``
64-
----------
65-
66-
Setting this parameter to ``False`` will turn off the automatic conversion to
67-
the field's ``Enum`` type while leaving all validation checks in place. It will
68-
still be possible to set the field directly as an ``Enum`` instance:
62+
``coerce``
63+
----------
6964

70-
.. code-block:: python
65+
Setting this parameter to ``False`` will turn off the automatic conversion to
66+
the field's ``Enum`` type while leaving all validation checks in place. It will
67+
still be possible to set the field directly as an ``Enum`` instance:
7168

72-
non_strict = EnumField(
73-
EnumType,
74-
strict=False,
75-
coerce=False,
76-
# it might be necessary to override max_length also, otherwise
77-
# max_length will be 1
78-
max_length=10
79-
)
69+
.. code-block:: python
8070
81-
# set to a valid EnumType value
82-
obj.non_strict = '1'
83-
obj.full_clean()
71+
non_strict = EnumField(
72+
EnumType,
73+
strict=False,
74+
coerce=False,
75+
# it might be necessary to override max_length also, otherwise
76+
# max_length will be 1
77+
max_length=10
78+
)
8479
85-
# when accessed from the db or after clean, will be the primitive value
86-
assert obj.non_strict == '1'
87-
assert isinstance(obj.non_strict, str)
80+
# set to a valid EnumType value
81+
obj.non_strict = '1'
82+
obj.full_clean()
8883
84+
# when accessed from the db or after clean, will be the primitive value
85+
assert obj.non_strict == '1'
86+
assert isinstance(obj.non_strict, str)
87+
assert not isinstance(obj.non_strict, StrictExample.EnumType)
8988
9089
enum-properties
9190
###############
@@ -108,7 +107,7 @@ values that can be symmetrically mapped back to enumeration values:
108107
from django_enum import TextChoices # use instead of Django's TextChoices
109108
from django.db import models
110109
111-
class MyModel(models.Model):
110+
class TextChoicesExample(models.Model):
112111
113112
class Color(TextChoices, s('rgb'), s('hex', case_fold=True)):
114113
@@ -123,8 +122,12 @@ values that can be symmetrically mapped back to enumeration values:
123122
124123
color = EnumField(Color)
125124
126-
instance = MyModel.objects.create(color=MyModel.Color('FF0000'))
127-
assert instance.color == MyModel.Color('Red') == MyModel.Color('R') == MyModel.Color((1, 0, 0))
125+
instance = TextChoicesExample.objects.create(
126+
color=TextChoicesExample.Color('FF0000')
127+
)
128+
assert instance.color == TextChoicesExample.Color('Red')
129+
assert instance.color == TextChoicesExample.Color('R')
130+
assert instance.color == TextChoicesExample.Color((1, 0, 0))
128131
129132
# direct comparison to any symmetric value also works
130133
assert instance.color == 'Red'

0 commit comments

Comments
 (0)