Skip to content

Commit fb9bef7

Browse files
authored
Merge pull request #57 from bckohan/54-support-django-50
54 support django 50
2 parents fa07a0b + f2b2e22 commit fb9bef7

File tree

17 files changed

+668
-33
lines changed

17 files changed

+668
-33
lines changed

.github/workflows/test.yml

+6-4
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,21 @@ jobs:
1010
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
1111
django-version:
1212
- 'Django~=3.2.0' # LTS April 2024
13-
- 'Django~=4.1.0' # December 2023
1413
- 'Django~=4.2.0' # LTS April 2026
14+
- 'Django~=5.0.0' # April 2025
1515
exclude:
1616
- python-version: '3.7'
17-
django-version: 'Django~=4.1.0'
17+
django-version: 'Django~=5.0.0'
1818
- python-version: '3.7'
1919
django-version: 'Django~=4.2.0'
20+
- python-version: '3.8'
21+
django-version: 'Django~=5.0.0'
22+
- python-version: '3.9'
23+
django-version: 'Django~=5.0.0'
2024
- python-version: '3.11'
2125
django-version: 'Django~=3.2.0'
2226
- python-version: '3.12'
2327
django-version: 'Django~=3.2.0'
24-
- python-version: '3.12'
25-
django-version: 'Django~=4.1.0'
2628

2729
steps:
2830
- uses: actions/checkout@v2

django_enum/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
'EnumFilter'
4848
]
4949

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

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

django_enum/choices.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
from django.db.models import Choices
1010
from django.db.models import IntegerChoices as DjangoIntegerChoices
1111
from django.db.models import TextChoices as DjangoTextChoices
12-
from django.db.models.enums import ChoicesMeta
12+
13+
try:
14+
from django.db.models.enums import ChoicesType
15+
except ImportError: # pragma: no cover
16+
from django.db.models.enums import ChoicesMeta as ChoicesType
1317

1418

1519
def choices(enum: Optional[Type[Enum]]) -> List[Tuple[Any, str]]:
@@ -87,7 +91,7 @@ def values(enum: Optional[Type[Enum]]) -> List[Any]:
8791
from enum_properties import EnumPropertiesMeta, SymmetricMixin
8892

8993

90-
class DjangoEnumPropertiesMeta(EnumPropertiesMeta, ChoicesMeta):
94+
class DjangoEnumPropertiesMeta(EnumPropertiesMeta, ChoicesType):
9195
"""
9296
A composite meta class that combines Django's Choices metaclass with
9397
enum-properties metaclass. This metaclass will add Django's expected
@@ -104,7 +108,7 @@ class DjangoSymmetricMixin(SymmetricMixin):
104108
_symmetric_builtins_ = ['name', 'label']
105109

106110

107-
class TextChoices(
111+
class TextChoices( # pylint: disable=too-many-ancestors
108112
DjangoSymmetricMixin,
109113
DjangoTextChoices,
110114
metaclass=DjangoEnumPropertiesMeta
@@ -118,7 +122,7 @@ def __hash__(self):
118122
return DjangoTextChoices.__hash__(self)
119123

120124

121-
class IntegerChoices(
125+
class IntegerChoices( # pylint: disable=too-many-ancestors
122126
DjangoSymmetricMixin,
123127
DjangoIntegerChoices,
124128
metaclass=DjangoEnumPropertiesMeta
@@ -146,6 +150,9 @@ class FloatChoices(
146150
def __hash__(self):
147151
return float.__hash__(self)
148152

153+
def __str__(self):
154+
return str(self.value)
155+
149156

150157
except (ImportError, ModuleNotFoundError):
151158

@@ -162,7 +169,7 @@ def __init__(self, *args, **kwargs): # pylint: disable=W0231
162169
DjangoSymmetricMixin = MissingEnumProperties # type: ignore
163170

164171

165-
class DjangoEnumPropertiesMeta(ChoicesMeta): # type: ignore
172+
class DjangoEnumPropertiesMeta(ChoicesType): # type: ignore
166173
"""
167174
Throw error if metaclass is used without enum-properties
168175

django_enum/fields.py

+47-18
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727
SmallIntegerField,
2828
)
2929
from django.db.models.query_utils import DeferredAttribute
30+
31+
try:
32+
from django.db.models.expressions import DatabaseDefault
33+
except ImportError: # pragma: no cover
34+
class DatabaseDefault: # type: ignore
35+
"""Spoof DatabaseDefault for Django < 5.0"""
36+
3037
from django_enum.choices import choices, values
3138
from django_enum.forms import EnumChoiceField, NonStrictSelect
3239

@@ -56,7 +63,11 @@ class ToPythonDeferredAttribute(DeferredAttribute):
5663

5764
def __set__(self, instance: Model, value: Any):
5865
try:
59-
instance.__dict__[self.field.name] = self.field.to_python(value)
66+
instance.__dict__[self.field.name] = (
67+
value
68+
if isinstance(value, DatabaseDefault) else
69+
self.field.to_python(value)
70+
)
6071
except (ValidationError, ValueError):
6172
# Django core fields allow assignment of any value, we do the same
6273
instance.__dict__[self.field.name] = value
@@ -83,19 +94,10 @@ class EnumMixin(
8394
enum: Optional[Type[Enum]] = None
8495
strict: bool = True
8596
coerce: bool = True
97+
primitive: Type[Any]
8698

8799
descriptor_class = ToPythonDeferredAttribute
88100

89-
def _coerce_to_value_type(self, value: Any) -> Enum:
90-
"""Coerce the value to the enumerations value type"""
91-
# note if enum type is int and a floating point is passed we could get
92-
# situations like X.xxx == X - this is acceptable
93-
if self.enum:
94-
return type(values(self.enum)[0])(value)
95-
# can't ever reach this - just here to make type checker happy
96-
return value # pragma: no cover
97-
98-
99101
def __init__(
100102
self,
101103
*args,
@@ -121,30 +123,42 @@ def _try_coerce(
121123
and non-strict, coercion to enum's primitive type will be done,
122124
otherwise a ValueError is raised.
123125
"""
124-
if (
125-
(self.coerce or force)
126-
and self.enum is not None
127-
and not isinstance(value, self.enum)
128-
):
126+
if self.enum is None:
127+
return value
128+
129+
if (self.coerce or force) and not isinstance(value, self.enum):
129130
try:
130131
value = self.enum(value)
131132
except (TypeError, ValueError):
132133
try:
133-
value = self._coerce_to_value_type(value)
134+
value = self.primitive(value)
134135
value = self.enum(value)
135136
except (TypeError, ValueError):
136137
try:
137138
value = self.enum[value]
138139
except KeyError as err:
139140
if self.strict or not isinstance(
140141
value,
141-
type(values(self.enum)[0])
142+
self.primitive
142143
):
143144
raise ValueError(
144145
f"'{value}' is not a valid "
145146
f"{self.enum.__name__} "
146147
f"required by field {self.name}."
147148
) from err
149+
elif (
150+
not self.coerce and
151+
not isinstance(value, self.primitive) and
152+
not isinstance(value, self.enum)
153+
):
154+
try:
155+
return self.primitive(value)
156+
except (TypeError, ValueError) as err:
157+
raise ValueError(
158+
f"'{value}' is not coercible to {self.primitive.__name__} "
159+
f"required by field {self.name}."
160+
) from err
161+
148162
return value
149163

150164
def deconstruct(self) -> Tuple[str, str, List, dict]:
@@ -306,6 +320,8 @@ class EnumCharField(EnumMixin, CharField):
306320
A database field supporting enumerations with character values.
307321
"""
308322

323+
primitive = str
324+
309325
def __init__(self, *args, enum=None, **kwargs):
310326
kwargs.setdefault(
311327
'max_length',
@@ -320,48 +336,61 @@ def __init__(self, *args, enum=None, **kwargs):
320336
class EnumFloatField(EnumMixin, FloatField):
321337
"""A database field supporting enumerations with floating point values"""
322338

339+
primitive = float
340+
323341

324342
class EnumSmallIntegerField(EnumMixin, SmallIntegerField):
325343
"""
326344
A database field supporting enumerations with integer values that fit into
327345
2 bytes or fewer
328346
"""
329347

348+
primitive = int
349+
330350

331351
class EnumPositiveSmallIntegerField(EnumMixin, PositiveSmallIntegerField):
332352
"""
333353
A database field supporting enumerations with positive (but signed) integer
334354
values that fit into 2 bytes or fewer
335355
"""
336356

357+
primitive = int
337358

338359
class EnumIntegerField(EnumMixin, IntegerField):
339360
"""
340361
A database field supporting enumerations with integer values that fit into
341362
32 bytes or fewer
342363
"""
343364

365+
primitive = int
366+
344367

345368
class EnumPositiveIntegerField(EnumMixin, PositiveIntegerField):
346369
"""
347370
A database field supporting enumerations with positive (but signed) integer
348371
values that fit into 32 bytes or fewer
349372
"""
350373

374+
primitive = int
375+
351376

352377
class EnumBigIntegerField(EnumMixin, BigIntegerField):
353378
"""
354379
A database field supporting enumerations with integer values that fit into
355380
64 bytes or fewer
356381
"""
357382

383+
primitive = int
384+
358385

359386
class EnumPositiveBigIntegerField(EnumMixin, PositiveBigIntegerField):
360387
"""
361388
A database field supporting enumerations with positive (but signed) integer
362389
values that fit into 64 bytes or fewer
363390
"""
364391

392+
primitive = int
393+
365394

366395
class _EnumFieldMetaClass(type):
367396

django_enum/tests/db_default/__init__.py

Whitespace-only changes.

django_enum/tests/db_default/apps.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class DBDefaultConfig(AppConfig):
5+
name = 'django_enum.tests.db_default'
6+
label = name.replace('.', '_')

0 commit comments

Comments
 (0)