Moving to Django 3.0's Field.choices Enumeration Types

New blog post:

https://adamj.eu/tech/2020/01/27/moving-to-django-3-field-choices-enumeration-types/

4 Likes

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.

1 Like

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:

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.)

1 Like

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

1 Like

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]]
        ]
2 Likes

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 :+1:

I’ve created two tickets here: https://code.djangoproject.com/ticket/31261#ticket

https://code.djangoproject.com/ticket/31262#ticket

1 Like