Skip to content

Commit c42fa69

Browse files
committed
add docs on hash equivalency to fix #123
1 parent 242d989 commit c42fa69

File tree

10 files changed

+145
-3
lines changed

10 files changed

+145
-3
lines changed

doc/source/howto/admin.rst

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.. include:: ../refs.rst
2+
3+
.. _admin:
4+
5+
================
6+
Use Django Admin
7+
================
8+
9+
:class:`~django_enum.fields.EnumField` will mostly just work in the Django
10+
:mod:`~django.contrib.admin`. There is
11+
`one issue <https://github.com/django-commons/django-enum/issues/123>`_ where :ref:`enums that are
12+
not hash equivalent <hash_equivalency>` will not render value labels correctly in the
13+
:class:`~django.contrib.admin.ModelAdmin` :attr:`~django.contrib.admin.ModelAdmin.list_display`.

doc/source/howto/external.rst

+31
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,34 @@ The list of choice tuples for each field are:
2929
values assigned depend on the order of declaration. This means that if the order changes
3030
existing database values will no longer align with the enumeration values. When control over the
3131
values is not certain it is a good idea to add integration tests that look for value changes.
32+
33+
.. _hash_equivalency:
34+
35+
Hash Equivalency
36+
----------------
37+
38+
.. tip::
39+
40+
It is a good idea to make sure your enumeration instances are hash equivalent to their
41+
primitive values. You can do this simply by inheriting from their primitive value
42+
(e.g. ``class MyEnum(str, Enum):``) or by using :class:`~enum.StrEnum` and
43+
:class:`~enum.IntEnum` types. Any enumeration defined using :doc:`enum-properties:index`
44+
will be hash equivalent to its values by default.
45+
46+
:class:`~django_enum.fields.EnumField` automatically sets the choices tuple on the field. Django_
47+
has logic in a number of places that handles fields with choices in a special way
48+
(e.g. :ref:`in the admin <admin>`). For example, the choices may be converted to a dictionary
49+
mapping values to labels. The values will be the primitive values of the enumeration not
50+
enumeration instances and the current value of the field which may be an enumeration instance will
51+
be searched for in the dictionary. This will fail if the enumeration instance is not hash
52+
equivalent to its value.
53+
54+
To control the hashing behavior of an object, you must override its :meth:`~object.__hash__` and
55+
:meth:`~object.__eq__` methods.
56+
57+
For example:
58+
59+
.. literalinclude:: ../../../tests/examples/models/hash_equivalency.py
60+
61+
.. literalinclude:: ../../../tests/examples/hash_equivalency_howto.py
62+
:lines: 3-

doc/source/howto/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ are possible with :class:`~django_enum.fields.EnumField`. See :ref:`enum_props`.
4545
integrations
4646
migrations
4747
urls
48+
admin

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "django-enum"
3-
version = "2.2.2"
3+
version = "2.2.3"
44
description = "Full and natural support for enumerations as Django model fields."
55
requires-python = ">=3.9,<4.0"
66
authors = [

src/django_enum/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
__all__ = ["EnumField"]
1818

19-
VERSION = (2, 2, 2)
19+
VERSION = (2, 2, 3)
2020

2121
__title__ = "Django Enum"
2222
__version__ = ".".join(str(i) for i in VERSION)
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from .models.hash_equivalency import HashEquivalencyExample
2+
3+
4+
obj = HashEquivalencyExample.objects.create(
5+
not_hash_eq=HashEquivalencyExample.NotHashEq.VALUE1,
6+
hash_eq=HashEquivalencyExample.HashEq.VALUE1,
7+
hash_eq_str=HashEquivalencyExample.HashEqStr.VALUE1
8+
)
9+
10+
# direct comparisons to values do not work
11+
assert obj.not_hash_eq != "V1"
12+
13+
# unless you have provided __eq__ or inherited from the primitive
14+
assert obj.hash_eq == obj.hash_eq_str == "V1"
15+
16+
# here is the problem that can break some Django internals in rare instances:
17+
assert dict(HashEquivalencyExample._meta.get_field("not_hash_eq").flatchoices) == {
18+
"V1": "VALUE1",
19+
"V2": "VALUE2",
20+
"V3": "VALUE3"
21+
}
22+
23+
try:
24+
dict(HashEquivalencyExample._meta.get_field("not_hash_eq").flatchoices)[
25+
HashEquivalencyExample.NotHashEq.VALUE1
26+
]
27+
assert False
28+
except KeyError:
29+
assert True
30+
31+
# if we've made our enum hash equivalent though, this works:
32+
assert dict(HashEquivalencyExample._meta.get_field("hash_eq").flatchoices)[
33+
HashEquivalencyExample.HashEq.VALUE1
34+
] == "VALUE1"
35+
assert dict(HashEquivalencyExample._meta.get_field("hash_eq_str").flatchoices)[
36+
HashEquivalencyExample.HashEqStr.VALUE1
37+
] == "VALUE1"

tests/examples/models/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .gnss import GNSSReceiver, Constellation
1212
from .gnss_vanilla import GNSSReceiverBasic
1313
from .equivalency import EquivalencyExample
14+
from .hash_equivalency import HashEquivalencyExample
1415
from .extern import ExternalChoices
1516
from .flag_howto import Group
1617
from .text_choices import TextChoicesExample
@@ -37,6 +38,7 @@
3738
"Constellation",
3839
"GNSSReceiverBasic",
3940
"EquivalencyExample",
41+
"HashEquivalencyExample",
4042
"ExternalChoices",
4143
"Group",
4244
"TextChoicesExample",
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from enum import Enum
2+
from django.db.models import Model
3+
from django_enum import EnumField
4+
5+
6+
class HashEquivalencyExample(Model):
7+
"""
8+
This example model defines three enum fields. The first uses an enum that
9+
is not hash equivalent to its values. The second two are.
10+
"""
11+
12+
class NotHashEq(Enum):
13+
"""
14+
Enums that inherit only from :class:`~enum.Enum` are not hash equivalent
15+
to their values by default.
16+
"""
17+
18+
VALUE1 = "V1"
19+
VALUE2 = "V2"
20+
VALUE3 = "V3"
21+
22+
class HashEq(Enum):
23+
"""
24+
We can force our Enum to be hash equivalent by overriding the necessary
25+
dunder methods..
26+
"""
27+
28+
VALUE1 = "V1"
29+
VALUE2 = "V2"
30+
VALUE3 = "V3"
31+
32+
def __hash__(self):
33+
return hash(self.value)
34+
35+
def __eq__(self, value) -> bool:
36+
if isinstance(value, self.__class__):
37+
return self.value == value.value
38+
try:
39+
return self.value == self.__class__(value).value
40+
except (ValueError, TypeError):
41+
return False
42+
43+
class HashEqStr(str, Enum): # or StrEnum on py 3.11+
44+
"""
45+
Or we can inherit from the primitive value type.
46+
"""
47+
48+
VALUE1 = "V1"
49+
VALUE2 = "V2"
50+
VALUE3 = "V3"
51+
52+
53+
not_hash_eq = EnumField(NotHashEq)
54+
hash_eq = EnumField(HashEq)
55+
hash_eq_str = EnumField(HashEqStr)

tests/test_examples.py

+3
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,9 @@ def test_gnss_tutorial_vanilla(self):
304304
def test_equivalency_howto(self):
305305
from tests.examples import equivalency_howto
306306

307+
def test_hash_equivalency_howto(self):
308+
from tests.examples import hash_equivalency_howto
309+
307310
def test_extern_howto(self):
308311
from tests.examples import extern_howto
309312

uv.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)