New blog post:
https://adamj.eu/tech/2020/01/27/moving-to-django-3-field-choices-enumeration-types/
Thanks, I had been wondering if moving to enumeration types was worth the hassle, but after reading your post I think I’ll give it a go.
Hey @adamchainz. I really liked this post.
One follow-up thought, I’ve been considering but not down to, is how you might get around the no named groups limitation? From the docs:
- Enumeration types do not support named groups.
Overriding ChoiceWidget.optgroups()
would let you do it for a form, but I don’t know how elegant it would end up being. (It seems the subclass would have to know the choices in play, which is OK, but…)
I don’t know if you’ve thought about it at all? (Against APIs and such, groups fade away, but I often like them for forms.)
I didn’t think about it, no.
It might be possible to add a special grouping attribute to choices classes?
class Status(models.TextChoices):
UNPUBLISHED = 'UN', 'Unpublished'
PUBLISHED = 'PB', 'Published'
grouping = [
['Visible', [PUBLISHED]],
['Hidden', [UNPUBLISHED]],
]
But that’s just me bikeshedding
There’s an older implementation that includes grouping, https://github.com/orf/django-choice-object (it was written for Python 2, so not enum-based, but it was actually the inspiration for the way Choices were done). We could adopt more ideas from there.
@orf any feels on this?
That library is a bit of a blast from the past! IMO enum ‘groups’ and ‘named groups’ are related but distinct.
‘groups’ are an important feature - basically named fields that are not included as part of the choice display. This has been pretty common in all projects I’ve worked on, and it’s just a way to group common sets of attributes together that can be checked with a simple contains. i.e field.value in MoonLandings.FAILED_MOON_LANDINGS
. FAILED_MOON_LANDINGS
would be a list of MoonLandings
values, and it feels natural for it to be included on the enum itself. Without it you end up with a bunch of lists scattered around and not kept in one place.
Named groups are similar but are used to influence the display of the widget itself.
I think we could add a Meta
to the Choices object to accomodate these?
class MyChoices(models.TextChoices):
WHATEVER = "1"
SOMETHING = "2"
class Meta:
SOME_GROUP = {WHATEVER, SOMETHING}
GROUPING = [
['Something': [WHATEVER, SOMETHING]]
]
Unfortunately the Meta class can’t refer to the names in the outer class being defined:
In [11]: class MyChoices(Enum):
...: WHATEVER = "1"
...: SOMETHING = "2"
...:
...: class Meta:
...: SOME_GROUP = {WHATEVER, SOMETHING}
...: GROUPING = [
...: ['Something', [WHATEVER, SOMETHING]]
...: ]
...:
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-11-3fe4f79f24e8> in <module>
...
<ipython-input-11-3fe4f79f24e8> in Meta()
4
5 class Meta:
----> 6 SOME_GROUP = {WHATEVER, SOMETHING}
7 GROUPING = [
8 ['Something', [WHATEVER, SOMETHING]]
NameError: name 'WHATEVER' is not defined
We could use strings to refer to them (SOME_GROUP = {'WHATEVER', 'SOMETHING'}
) or special names in the main class. Given that enums already use some special names, and it’s cleaner syntax, I am in favour of the latter.
Damn, that’s quite annoying. I’m not a fan of using strings here as you lose some nice properties (auto-completion, refactoring etc).
What about this: We special case enum values that are either sets, lists or dictionaries?
class MoonMissions(Enum):
APOLLO_10 = 1
APOLLO_11 = 2
EXPLORER_33 = 3
FAILED_MISSIONS = {EXPLORER_33}
SUCCESSFUL_MISSIONS = [APOLLO_10, APOLLO_11]
SUCCESS_GROUPING = {
"Failed": FAILED_MISSIONS,
"Successful": SUCCESSFUL_MISSIONS
}
COUNTRY_GROUPING = [
["USA", [APOLLO_10, APOLLO_11, EXPLORER_33]]
]
Side note: Now that dictionaries are insertion ordered in Python 3.7+ (and on cPython 3.6+), the general grouping syntax could now use dictionaries instead of the list-of-lists we’ve had before (which I assume is due to unordered dictionaries?).
The above syntax would keep the nice ability to just do field.value in MoonMissions.SUCCESSFUL_MISSIONS
without too much metaclass magic. The obvious downside would be that defining an enum with list/dictionary values would not be possible, but is that a common use case at all?
Seems like a nice syntax. Want to make a ticket?
We can work for list/dict/set Enums by inspecting if the current enum inherits from one and disabling the functionality.
Sure thing, I’ll create one today
I’ve created two tickets here: https://code.djangoproject.com/ticket/31261#ticket