Skip to content

Commit 7bd6d8c

Browse files
committed
migration issues fixed, migration test suite, strict parameter
1 parent bb36adb commit 7bd6d8c

File tree

26 files changed

+1027
-53
lines changed

26 files changed

+1027
-53
lines changed

.github/workflows/test.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ jobs:
4242
- name: Run Static Analysis
4343
run: |
4444
poetry run pylint django_enum
45-
poetry run mypy django_enum
4645
poetry run doc8 -q doc
4746
poetry check
4847
poetry run pip check

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,8 @@ dmypy.json
130130

131131
/poetry.lock
132132
test.db
133+
django_enum/tests/edit_tests/migrations/0001_initial.py
134+
django_enum/tests/edit_tests/migrations/0002_alter_values.py
135+
django_enum/tests/edit_tests/migrations/0003_remove_black.py
136+
django_enum/tests/edit_tests/migrations/0004_remove_int_enum.py
137+
django_enum/tests/edit_tests/migrations/0005_add_int_enum.py

django_enum/fields.py

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,54 @@ class EnumMixin:
2222
and convert any values to the Enumeration type in question.
2323
2424
:param enum: The enum class
25+
:param strict: If True (default) the field will throw ValueErrors if the
26+
value is not coercible to a valid enumeration type.
2527
:param args: Any standard unnamed field arguments for the underlying
2628
field type.
2729
:param field_kwargs: Any standard named field arguments for the underlying
2830
field type.
2931
"""
3032

3133
enum = None
34+
strict = True
3235

33-
def __init__(self, *args, enum, **kwargs):
36+
def _coerce_to_value_type(self, value):
37+
"""Coerce the value to the enumerations value type"""
38+
return type(self.enum.values[0])(value)
39+
40+
def __init__(self, *args, enum=None, strict=strict, **kwargs):
3441
self.enum = enum
35-
kwargs.setdefault('choices', self.enum.choices)
42+
self.strict = strict if enum else False
43+
if self.enum is not None:
44+
kwargs.setdefault('choices', enum.choices if enum else [])
3645
super().__init__(*args, **kwargs)
3746

47+
def _try_coerce(self, value):
48+
if self.enum is not None:
49+
try:
50+
value = self.enum(value)
51+
except (TypeError, ValueError):
52+
try:
53+
value = self.enum(self._coerce_to_value_type(value))
54+
except (TypeError, ValueError) as err:
55+
if self.strict:
56+
raise ValueError(
57+
f"'{value}' is not a valid {self.enum.__name__} "
58+
f"required by field {self.name}."
59+
) from err
60+
return value
61+
3862
def deconstruct(self):
3963
"""
40-
Preserve enum class for migrations
64+
Preserve enum class for migrations. Strict is omitted because
65+
reconstructed fields are *always* non-strict sense enum is null.
4166
4267
See deconstruct_
4368
"""
4469
name, path, args, kwargs = super().deconstruct()
45-
kwargs['enum'] = self.enum
70+
if self.enum is not None:
71+
kwargs['choices'] = self.enum.choices
72+
4673
return name, path, args, kwargs
4774

4875
def get_prep_value(self, value):
@@ -51,39 +78,22 @@ def get_prep_value(self, value):
5178
5279
See get_prep_value_
5380
"""
54-
if value is not None:
55-
if not isinstance(value, self.enum):
56-
try:
57-
value = self.enum(value).value
58-
except (TypeError, ValueError) as err:
59-
raise ValueError(
60-
f"Field '{self.name}' expected a "
61-
f"'{self.enum.__name__}' but got '{value}'.",
62-
) from err
63-
else:
81+
if value is not None and self.enum is not None:
82+
value = self._try_coerce(value)
83+
if isinstance(value, self.enum):
6484
value = value.value
6585
return super().get_prep_value(value)
6686

67-
# def get_db_prep_save(self, value, connection):
68-
# return self.get_db_prep_value(value, connection=connection)
69-
7087
def get_db_prep_value(self, value, connection, prepared=False):
7188
"""
7289
Convert the field value into the Enum type and then pull its value
7390
out.
7491
7592
See get_db_prep_value_
7693
"""
77-
if value is not None:
78-
if not isinstance(value, self.enum):
79-
try:
80-
value = self.enum(value).value
81-
except (TypeError, ValueError) as err:
82-
raise ValueError(
83-
f"Field '{self.name}' expected a "
84-
f"'{self.enum.__name__}' but got '{value}'.",
85-
) from err
86-
else:
94+
if value is not None and self.enum is not None:
95+
value = self._try_coerce(value)
96+
if isinstance(value, self.enum):
8797
value = value.value
8898
return super().get_db_prep_value(
8999
value,
@@ -104,7 +114,7 @@ def from_db_value(
104114
"""
105115
if value is None: # pragma: no cover
106116
return value
107-
return self.enum(value)
117+
return self._try_coerce(value)
108118

109119
def to_python(self, value):
110120
"""
@@ -117,12 +127,12 @@ def to_python(self, value):
117127
:raises ValidationError: If the value is not mappable to a valid
118128
enumeration
119129
"""
120-
if isinstance(value, self.enum) or value is None:
130+
if self.enum is None or isinstance(value, self.enum) or value is None:
121131
return value
122132

123133
try:
124-
return self.enum(value)
125-
except (TypeError, ValueError) as err:
134+
return self._try_coerce(value)
135+
except ValueError as err:
126136
raise ValidationError(
127137
f"'{value}' is not a valid {self.enum.__name__}."
128138
) from err
@@ -160,10 +170,11 @@ class EnumCharField(EnumMixin, CharField):
160170
A database field supporting enumerations with character values.
161171
"""
162172

163-
def __init__(self, enum, *args, **kwargs):
173+
def __init__(self, *args, enum=None, **kwargs):
174+
choices = kwargs.get('choices', enum.choices if enum else [])
164175
kwargs.setdefault(
165176
'max_length',
166-
max([len(define.value) for define in enum])
177+
max([len(choice[0]) for choice in choices])
167178
)
168179
super().__init__(*args, enum=enum, **kwargs)
169180

django_enum/forms.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,15 @@ def _coerce(self, value):
3636

3737
def clean(self, value):
3838
return super().clean(self._coerce(value))
39+
40+
# def prepare_value(self, value):
41+
# return value
42+
#
43+
# def to_python(self, value):
44+
# return value
45+
#
46+
# def validate(self, value):
47+
# if value in self.empty_values and self.required:
48+
# raise ValidationError(
49+
# self.error_messages['required'], code='required'
50+
# )

django_enum/tests/app1/enums.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
from django.utils.translation import gettext as _
22
from django_enum import FloatChoices, IntegerChoices, TextChoices
33
from enum_properties import p, s
4+
from django.db.models import (
5+
IntegerChoices as DjangoIntegerChoices,
6+
TextChoices as DjangoTextChoices
7+
)
8+
9+
10+
class DJIntEnum(DjangoIntegerChoices):
11+
12+
ONE = 1, 'One'
13+
TWO = 2, 'Two'
14+
THREE = 3, 'Three'
15+
16+
17+
class DJTextEnum(DjangoTextChoices):
18+
19+
A = 'A', 'Label A'
20+
B = 'B', 'Label B'
21+
C = 'C', 'Label C'
422

523

624
class TextEnum(TextChoices, p('version'), p('help'), s('aliases', case_fold=True)):
Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
# Generated by Django 3.2.14 on 2022-07-19 20:29
1+
# Generated by Django 3.2.14 on 2022-07-22 14:29
22

3-
import django_enum.fields
4-
import django_enum.tests.app1.enums
5-
import django_enum.tests.app1.models
63
from django.db import migrations, models
4+
import django_enum.fields
75

86

97
class Migration(migrations.Migration):
@@ -18,19 +16,21 @@ class Migration(migrations.Migration):
1816
name='EnumTester',
1917
fields=[
2018
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21-
('small_pos_int', django_enum.fields.EnumPositiveSmallIntegerField(blank=True, choices=[(0, 'Value 1'), (2, 'Value 2'), (32767, 'Value 32767')], db_index=True, default=None, enum=django_enum.tests.app1.enums.SmallPosIntEnum, null=True)),
22-
('small_int', django_enum.fields.EnumSmallIntegerField(blank=True, choices=[(-32768, 'Value -32768'), (0, 'Value 0'), (1, 'Value 1'), (2, 'Value 2'), (32767, 'Value 32767')], db_index=True, default=32767, enum=django_enum.tests.app1.enums.SmallIntEnum)),
23-
('pos_int', django_enum.fields.EnumPositiveIntegerField(blank=True, choices=[(0, 'Value 0'), (1, 'Value 1'), (2, 'Value 2'), (2147483647, 'Value 2147483647')], db_index=True, default=2147483647, enum=django_enum.tests.app1.enums.PosIntEnum)),
24-
('int', django_enum.fields.EnumIntegerField(blank=True, choices=[(-2147483648, 'Value -2147483648'), (0, 'Value 0'), (1, 'Value 1'), (2, 'Value 2'), (2147483647, 'Value 2147483647')], db_index=True, enum=django_enum.tests.app1.enums.IntEnum, null=True)),
25-
('big_pos_int', django_enum.fields.EnumPositiveBigIntegerField(blank=True, choices=[(0, 'Value 0'), (1, 'Value 1'), (2, 'Value 2'), (2147483648, 'Value 2147483647')], db_index=True, default=None, enum=django_enum.tests.app1.enums.BigPosIntEnum, null=True)),
26-
('big_int', django_enum.fields.EnumBigIntegerField(blank=True, choices=[(-2147483649, 'Value -2147483649'), (1, 'Value 1'), (2, 'Value 2'), (2147483648, 'Value 2147483647')], db_index=True, default=-2147483649, enum=django_enum.tests.app1.enums.BigIntEnum)),
27-
('constant', django_enum.fields.EnumFloatField(blank=True, choices=[(3.141592653589793, 'Pi'), (2.71828, "Euler's Number"), (1.618033988749895, 'Golden Ratio')], db_index=True, default=None, enum=django_enum.tests.app1.enums.Constants, null=True)),
28-
('text', django_enum.fields.EnumCharField(blank=True, choices=[('V1', 'Value1'), ('V22', 'Value2'), ('V333', 'Value3'), ('D', 'Default')], db_index=True, default=None, enum=django_enum.tests.app1.enums.TextEnum, max_length=4, null=True)),
19+
('small_pos_int', django_enum.fields.EnumPositiveSmallIntegerField(blank=True, choices=[(0, 'Value 1'), (2, 'Value 2'), (32767, 'Value 32767')], db_index=True, default=None, null=True)),
20+
('small_int', django_enum.fields.EnumSmallIntegerField(blank=True, choices=[(-32768, 'Value -32768'), (0, 'Value 0'), (1, 'Value 1'), (2, 'Value 2'), (32767, 'Value 32767')], db_index=True, default=32767)),
21+
('pos_int', django_enum.fields.EnumPositiveIntegerField(blank=True, choices=[(0, 'Value 0'), (1, 'Value 1'), (2, 'Value 2'), (2147483647, 'Value 2147483647')], db_index=True, default=2147483647)),
22+
('int', django_enum.fields.EnumIntegerField(blank=True, choices=[(-2147483648, 'Value -2147483648'), (0, 'Value 0'), (1, 'Value 1'), (2, 'Value 2'), (2147483647, 'Value 2147483647')], db_index=True, null=True)),
23+
('big_pos_int', django_enum.fields.EnumPositiveBigIntegerField(blank=True, choices=[(0, 'Value 0'), (1, 'Value 1'), (2, 'Value 2'), (2147483648, 'Value 2147483647')], db_index=True, default=None, null=True)),
24+
('big_int', django_enum.fields.EnumBigIntegerField(blank=True, choices=[(-2147483649, 'Value -2147483649'), (1, 'Value 1'), (2, 'Value 2'), (2147483648, 'Value 2147483647')], db_index=True, default=-2147483649)),
25+
('constant', django_enum.fields.EnumFloatField(blank=True, choices=[(3.141592653589793, 'Pi'), (2.71828, "Euler's Number"), (1.618033988749895, 'Golden Ratio')], db_index=True, default=None, null=True)),
26+
('text', django_enum.fields.EnumCharField(blank=True, choices=[('V1', 'Value1'), ('V22', 'Value2'), ('V333', 'Value3'), ('D', 'Default')], db_index=True, default=None, max_length=4, null=True)),
2927
('int_choice', models.IntegerField(blank=True, choices=[(1, 'One'), (2, 'Two'), (3, 'Three')], default=1)),
3028
('char_choice', models.CharField(blank=True, choices=[('A', 'First'), ('B', 'Second'), ('C', 'Third')], default='A', max_length=1)),
3129
('int_field', models.IntegerField(blank=True, default=1)),
3230
('float_field', models.FloatField(blank=True, default=1.5)),
3331
('char_field', models.CharField(blank=True, default='A', max_length=1)),
32+
('dj_int_enum', django_enum.fields.EnumPositiveSmallIntegerField(choices=[(1, 'One'), (2, 'Two'), (3, 'Three')], default=1)),
33+
('dj_text_enum', django_enum.fields.EnumCharField(choices=[('A', 'Label A'), ('B', 'Label B'), ('C', 'Label C')], default='A', max_length=1)),
3434
],
3535
options={
3636
'ordering': ('id',),
@@ -40,9 +40,9 @@ class Migration(migrations.Migration):
4040
name='MyModel',
4141
fields=[
4242
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
43-
('txt_enum', django_enum.fields.EnumCharField(blank=True, choices=[('V0', 'Value 0'), ('V1', 'Value 1'), ('V2', 'Value 2')], enum=django_enum.tests.app1.models.MyModel.TextEnum, max_length=2, null=True)),
44-
('int_enum', django_enum.fields.EnumPositiveSmallIntegerField(choices=[(1, 'One'), (2, 'Two'), (3, 'Three')], enum=django_enum.tests.app1.models.MyModel.IntEnum)),
45-
('color', django_enum.fields.EnumCharField(choices=[('R', 'Red'), ('G', 'Green'), ('B', 'Blue')], enum=django_enum.tests.app1.models.MyModel.Color, max_length=1)),
43+
('txt_enum', django_enum.fields.EnumCharField(blank=True, choices=[('V0', 'Value 0'), ('V1', 'Value 1'), ('V2', 'Value 2')], max_length=2, null=True)),
44+
('int_enum', django_enum.fields.EnumPositiveSmallIntegerField(choices=[(1, 'One'), (2, 'Two'), (3, 'Three')])),
45+
('color', django_enum.fields.EnumCharField(choices=[('R', 'Red'), ('G', 'Green'), ('B', 'Blue')], max_length=1)),
4646
],
4747
),
4848
]

django_enum/tests/app1/models.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from django.db import models
22
from django.urls import reverse
3+
from enum import auto
34
from django_enum import EnumField, IntegerChoices, TextChoices
45
from django_enum.tests.app1.enums import (
6+
DJIntEnum,
7+
DJTextEnum,
58
BigIntEnum,
69
BigPosIntEnum,
710
Constants,
@@ -65,6 +68,9 @@ class EnumTester(models.Model):
6568
)
6669
################################################
6770

71+
dj_int_enum = EnumField(DJIntEnum, default=DJIntEnum.ONE.value)
72+
dj_text_enum = EnumField(DJTextEnum, default=DJTextEnum.A.value)
73+
6874
def get_absolute_url(self):
6975
return reverse('django_enum_tests_app1:enum-detail', kwargs={'pk': self.pk})
7076

@@ -96,5 +102,3 @@ class Color(TextChoices, s('rgb'), s('hex', case_fold=True)):
96102
int_enum = EnumField(IntEnum)
97103
color = EnumField(Color)
98104

99-
def save(self, *args, **kwargs):
100-
super().save(*args, **kwargs)

django_enum/tests/app1/templates/django_enum_tests_app1/enumtester_detail.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ <h1>{{ object.pk }}</h1>
1111
<p class="big_int">big_int: <b><span class="value">{{ object.big_int.value }}</span></b> <b><span class="label">{{ object.big_int.label }}</span></b></p>
1212
<p class="constant">constant: <b><span class="value">{{ object.constant.value }}</span></b> <b><span class="label">{{ object.constant.label }}</span></b></p>
1313
<p class="text">text: <b><span class="value">{{ object.text.value }}</span></b> <b><span class="label">{{ object.text.label }}</span></b></p>
14+
<p class="dj_int_enum">dj_int_enum: <b><span class="value">{{ object.dj_int_enum.value }}</span></b> <b><span class="label">{{ object.dj_int_enum.label }}</span></b></p>
15+
<p class="dj_text_enum">dj_text_enum: <b><span class="value">{{ object.dj_text_enum.value }}</span></b> <b><span class="label">{{ object.dj_text_enum.label }}</span></b></p>
1416

1517
<p>
1618
<a href="{% url 'django_enum_tests_app1:enum-update' pk=object.pk %}">Edit</a>

django_enum/tests/app1/templates/django_enum_tests_app1/enumtester_list.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ <h2>{{ object.pk }}</h2>
1313
<p class="big_int">big_int: <b><span class="value">{{ object.big_int.value }}</span></b> <b><span class="label">{{ object.big_int.label }}</span></b></p>
1414
<p class="constant">constant: <b><span class="value">{{ object.constant.value }}</span></b> <b><span class="label">{{ object.constant.label }}</span></b></p>
1515
<p class="text">text: <b><span class="value">{{ object.text.value }}</span></b> <b><span class="label">{{ object.text.label }}</span></b></p>
16+
<p class="dj_int_enum">dj_int_enum: <b><span class="value">{{ object.dj_int_enum.value }}</span></b> <b><span class="label">{{ object.dj_int_enum.label }}</span></b></p>
17+
<p class="dj_text_enum">dj_text_enum: <b><span class="value">{{ object.dj_text_enum.value }}</span></b> <b><span class="label">{{ object.dj_text_enum.label }}</span></b></p>
1618

1719
<p>
1820
<a href="{% url 'django_enum_tests_app1:enum-update' pk=object.pk %}">Edit</a>

django_enum/tests/app1/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
SmallIntEnum,
1414
SmallPosIntEnum,
1515
TextEnum,
16+
DJIntEnum,
17+
DJTextEnum
1618
)
1719
from django_enum.tests.app1.models import EnumTester
1820
from django_filters.views import FilterView
@@ -47,6 +49,8 @@ class EnumTesterForm(ModelForm):
4749
big_int = EnumChoiceField(BigIntEnum)
4850
constant = EnumChoiceField(Constants)
4951
text = EnumChoiceField(TextEnum)
52+
dj_int_enum = EnumChoiceField(DJIntEnum)
53+
dj_text_enum = EnumChoiceField(DJTextEnum)
5054

5155
class Meta:
5256
model = EnumTester

0 commit comments

Comments
 (0)