Skip to content

Commit 2f4464e

Browse files
authored
Merge pull request #2404 from mas15/add-enum-field
Add EnumField
2 parents 89b9346 + 9e40f3a commit 2f4464e

File tree

5 files changed

+174
-2
lines changed

5 files changed

+174
-2
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,4 @@ that much better:
257257
* Matthew Simpson (https://github.com/mcsimps2)
258258
* Leonardo Domingues (https://github.com/leodmgs)
259259
* Agustin Barto (https://github.com/abarto)
260+
* Stankiewicz Mateusz (https://github.com/mas15)

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Development
1313
- Fix the behavior of Doc.objects.limit(0) which should return all documents (similar to mongodb) #2311
1414
- Bug fix in ListField when updating the first item, it was saving the whole list, instead of
1515
just replacing the first item (as it's usually done) #2392
16+
- Add EnumField: ``mongoengine.fields.EnumField``
1617

1718
Changes in 0.20.0
1819
=================

docs/guide/defining-documents.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ are as follows:
7676
* :class:`~mongoengine.fields.EmailField`
7777
* :class:`~mongoengine.fields.EmbeddedDocumentField`
7878
* :class:`~mongoengine.fields.EmbeddedDocumentListField`
79+
* :class:`~mongoengine.fields.EnumField`
7980
* :class:`~mongoengine.fields.FileField`
8081
* :class:`~mongoengine.fields.FloatField`
8182
* :class:`~mongoengine.fields.GenericEmbeddedDocumentField`

mongoengine/fields.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"PolygonField",
8888
"SequenceField",
8989
"UUIDField",
90+
"EnumField",
9091
"MultiPointField",
9192
"MultiLineStringField",
9293
"MultiPolygonField",
@@ -847,8 +848,7 @@ class DynamicField(BaseField):
847848
Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
848849

849850
def to_mongo(self, value, use_db_field=True, fields=None):
850-
"""Convert a Python type to a MongoDB compatible type.
851-
"""
851+
"""Convert a Python type to a MongoDB compatible type."""
852852

853853
if isinstance(value, str):
854854
return value
@@ -1622,6 +1622,68 @@ def prepare_query_value(self, op, value):
16221622
return super().prepare_query_value(op, self.to_mongo(value))
16231623

16241624

1625+
class EnumField(BaseField):
1626+
"""Enumeration Field. Values are stored underneath as strings.
1627+
Example usage:
1628+
.. code-block:: python
1629+
1630+
class Status(Enum):
1631+
NEW = 'new'
1632+
DONE = 'done'
1633+
1634+
class ModelWithEnum(Document):
1635+
status = EnumField(Status, default=Status.NEW)
1636+
1637+
ModelWithEnum(status='done')
1638+
ModelWithEnum(status=Status.DONE)
1639+
1640+
Enum fields can be searched using enum or its value:
1641+
.. code-block:: python
1642+
1643+
ModelWithEnum.objects(status='new').count()
1644+
ModelWithEnum.objects(status=Status.NEW).count()
1645+
1646+
Note that choices cannot be set explicitly, they are derived
1647+
from the provided enum class.
1648+
"""
1649+
1650+
def __init__(self, enum, **kwargs):
1651+
self._enum_cls = enum
1652+
if "choices" in kwargs:
1653+
raise ValueError(
1654+
"'choices' can't be set on EnumField, "
1655+
"it is implicitly set as the enum class"
1656+
)
1657+
kwargs["choices"] = list(self._enum_cls)
1658+
super().__init__(**kwargs)
1659+
1660+
def __set__(self, instance, value):
1661+
is_legal_value = value is None or isinstance(value, self._enum_cls)
1662+
if not is_legal_value:
1663+
try:
1664+
value = self._enum_cls(value)
1665+
except Exception:
1666+
pass
1667+
return super().__set__(instance, value)
1668+
1669+
def to_mongo(self, value):
1670+
if isinstance(value, self._enum_cls):
1671+
return value.value
1672+
return value
1673+
1674+
def validate(self, value):
1675+
if value and not isinstance(value, self._enum_cls):
1676+
try:
1677+
self._enum_cls(value)
1678+
except Exception as e:
1679+
self.error(str(e))
1680+
1681+
def prepare_query_value(self, op, value):
1682+
if value is None:
1683+
return value
1684+
return super().prepare_query_value(op, self.to_mongo(value))
1685+
1686+
16251687
class GridFSError(Exception):
16261688
pass
16271689

tests/fields/test_enum_field.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from enum import Enum
2+
3+
import pytest
4+
5+
from mongoengine import *
6+
from tests.utils import MongoDBTestCase, get_as_pymongo
7+
8+
9+
class Status(Enum):
10+
NEW = "new"
11+
DONE = "done"
12+
13+
14+
class ModelWithEnum(Document):
15+
status = EnumField(Status)
16+
17+
18+
class TestStringEnumField(MongoDBTestCase):
19+
def test_storage(self):
20+
model = ModelWithEnum(status=Status.NEW).save()
21+
assert get_as_pymongo(model) == {"_id": model.id, "status": "new"}
22+
23+
def test_set_enum(self):
24+
ModelWithEnum.drop_collection()
25+
ModelWithEnum(status=Status.NEW).save()
26+
assert ModelWithEnum.objects(status=Status.NEW).count() == 1
27+
assert ModelWithEnum.objects.first().status == Status.NEW
28+
29+
def test_set_by_value(self):
30+
ModelWithEnum.drop_collection()
31+
ModelWithEnum(status="new").save()
32+
assert ModelWithEnum.objects.first().status == Status.NEW
33+
34+
def test_filter(self):
35+
ModelWithEnum.drop_collection()
36+
ModelWithEnum(status="new").save()
37+
assert ModelWithEnum.objects(status="new").count() == 1
38+
assert ModelWithEnum.objects(status=Status.NEW).count() == 1
39+
assert ModelWithEnum.objects(status=Status.DONE).count() == 0
40+
41+
def test_change_value(self):
42+
m = ModelWithEnum(status="new")
43+
m.status = Status.DONE
44+
m.save()
45+
assert m.status == Status.DONE
46+
47+
def test_set_default(self):
48+
class ModelWithDefault(Document):
49+
status = EnumField(Status, default=Status.DONE)
50+
51+
m = ModelWithDefault().save()
52+
assert m.status == Status.DONE
53+
54+
def test_enum_field_can_be_empty(self):
55+
ModelWithEnum.drop_collection()
56+
m = ModelWithEnum().save()
57+
assert m.status is None
58+
assert ModelWithEnum.objects()[0].status is None
59+
assert ModelWithEnum.objects(status=None).count() == 1
60+
61+
def test_set_none_explicitly(self):
62+
ModelWithEnum.drop_collection()
63+
ModelWithEnum(status=None).save()
64+
assert ModelWithEnum.objects.first().status is None
65+
66+
def test_cannot_create_model_with_wrong_enum_value(self):
67+
m = ModelWithEnum(status="wrong_one")
68+
with pytest.raises(ValidationError):
69+
m.validate()
70+
71+
def test_user_is_informed_when_tries_to_set_choices(self):
72+
with pytest.raises(ValueError, match="'choices' can't be set on EnumField"):
73+
EnumField(Status, choices=["my", "custom", "options"])
74+
75+
76+
class Color(Enum):
77+
RED = 1
78+
BLUE = 2
79+
80+
81+
class ModelWithColor(Document):
82+
color = EnumField(Color, default=Color.RED)
83+
84+
85+
class TestIntEnumField(MongoDBTestCase):
86+
def test_enum_with_int(self):
87+
ModelWithColor.drop_collection()
88+
m = ModelWithColor().save()
89+
assert m.color == Color.RED
90+
assert ModelWithColor.objects(color=Color.RED).count() == 1
91+
assert ModelWithColor.objects(color=1).count() == 1
92+
assert ModelWithColor.objects(color=2).count() == 0
93+
94+
def test_create_int_enum_by_value(self):
95+
model = ModelWithColor(color=2).save()
96+
assert model.color == Color.BLUE
97+
98+
def test_storage_enum_with_int(self):
99+
model = ModelWithColor(color=Color.BLUE).save()
100+
assert get_as_pymongo(model) == {"_id": model.id, "color": 2}
101+
102+
def test_validate_model(self):
103+
with pytest.raises(ValidationError, match="Value must be one of"):
104+
ModelWithColor(color=3).validate()
105+
106+
with pytest.raises(ValidationError, match="Value must be one of"):
107+
ModelWithColor(color="wrong_type").validate()

0 commit comments

Comments
 (0)