Opened 4 years ago

Closed 4 years ago

Last modified 4 years ago

#31261 closed New feature (needsinfo)

Add support for defining models.Choices fields with dict, tuple, set and list values.

Reported by: Tom Forbes Owned by: nobody
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords:
Cc: Shai Berger, pope1ni Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Mariusz Felisiak)

As per the discussion on this forum post: https://forum.djangoproject.com/t/moving-to-django-3-0s-field-choices-enumeration-types/970/8

It would be good to support two fairly common use cases with Enums: defining groups of related values (groups), and defining grouping for use with specific widgets.

One way to enable both of these would be to simply special-case enum values with dict, tuple, set and list values:

class MoonMissions(models.IntegerChoices):
    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]]
    ]

Now a user can do:

SomeModel.objects.filter(mission__in=MoonMissions.FAILED_MISSIONS)
enum_form_field.value in MoonMissions.SUCCESSFUL_MISSIONS
models.IntegerField(choices=MoonMissions.COUNTRY_GROUPING)

Change History (9)

comment:1 by Mariusz Felisiak, 4 years ago

Description: modified (diff)
Resolution: needsinfo
Status: newclosed
Summary: Add support for defining Enum fields with dict, tuple, set and list valuesAdd support for defining models.Choices fields with dict, tuple, set and list values.

Thanks for this ticket, however I'm not sure if that's feasible because we support subclasses of Choices for a data type other that int or str (see examples). This can be break backward compatibility. I would be very happy to evaluate a patch, but without breaking a current behavior.

comment:2 by Adam Johnson, 4 years ago

As I wrote on the forum post, we can detect if dict/list/set/tuple is in the MRO and disable this functionality in those (probably very rare) cases.

comment:3 by Tom Forbes, 4 years ago

Yep, or we could optionally include it as a mixin. An alternative approach might be to use a nested class (like Meta), but there are some tradeoffs there.

Perhaps this ticket should be more general: "Support for defining model choice groups in Enums"? The exact implementation might vary, but I think the overall goal is something we should work towards.

in reply to:  2 ; comment:4 by Mariusz Felisiak, 4 years ago

Cc: Shai Berger pope1ni added

Replying to Adam (Chainz) Johnson:

As I wrote on the forum post, we can detect if dict/list/set/tuple is in the MRO and disable this functionality in those (probably very rare) cases.

Can we? e.g. MoonLandings each option is defined by tuple but none of them is a group.

comment:5 by Carlton Gibson, 4 years ago

Perhaps this ticket should be more general: "Support for defining model choice groups in Enums"? The exact implementation might vary, but I think the overall goal is something we should work towards.

Yes. I agree with that sentiment. Grouping (both kinds) is useful. I guess the needsinfo here just means Proof-of-concept to push is forwards.

in reply to:  5 comment:6 by Mariusz Felisiak, 4 years ago

Replying to Carlton Gibson:

Yes. I agree with that sentiment. Grouping (both kinds) is useful.

Yes, totally agree, I just think it's quite tricky. Maybe defining groups in __groups__ = [...], I'm not sure.

I guess the needsinfo here just means Proof-of-concept to push is forwards.

Yes, that was my intention. I would be very happy to review a patch.

in reply to:  4 ; comment:7 by Shai Berger, 4 years ago

Replying to felixxm:

Replying to Adam (Chainz) Johnson:

As I wrote on the forum post, we can detect if dict/list/set/tuple is in the MRO and disable this functionality in those (probably very rare) cases.

Can we? e.g. MoonLandings each option is defined by tuple but none of them is a group.

I'm pretty sure we can't. The way Enums work is that the value assigned to elements in the code is not an instance of the target type, but rather a set of arguments for constructing one.

Further, an important part of Choices is the fact you can add (translatable) display strings, as a last element of a tuple.

in reply to:  7 comment:8 by Shai Berger, 4 years ago

Replying to Shai Berger:

Replying to felixxm:

Replying to Adam (Chainz) Johnson:

As I wrote on the forum post, we can detect if dict/list/set/tuple is in the MRO and disable this functionality in those (probably very rare) cases.

Can we? e.g. MoonLandings each option is defined by tuple but none of them is a group.

I'm pretty sure we can't.

Actually, looking again at the forum discussion, the original suggestion was to only special-case dict/list/set. This would, generally, work, all the reservations above are specifically about tuples; and further, Choices values need to be also dictionary keys, so mutable types (like dict, set and list) are already out anyway.

comment:9 by Tom Forbes, 4 years ago

I can construct a Choices object with mutable values, it fails if .choices is called on the object but otherwise it works fine.

I've found that the current code checks if the value is a list or a tuple, so something like this is currently supported:

SOME_KEY = [1, "display value]

I can't find tests for this and didn't know this was possible, but this does make it quite hard to make work as described in the ticket description without a lot of magic.

However what what about abandoning the idea of defining things using literals and use a special class:

class MoonMissions(models.IntegerChoices):
    ...
    SUCCESSFUL_MISSIONS = ChoiceSet(APOLLO_10, APOLLO_11)

    SUCCESS_GROUPING = DisplayGroup({
        "Failed": FAILED_MISSIONS,
        "Successful": SUCCESSFUL_MISSIONS
    })

From the point of view of the metaclass this makes things a _lot_ simpler, as you don't need to try and work out if the given value is an enum value, a value with a display.

It's not as nice as using a literal but it does sidestep a lot of issues. Another thing that might be simpler to do with a class is "stitching up" the choice/group values. Ideally we'd want the values of SUCCESSFUL_MISSIONS to be an Enum object, not the primitive type (or the tuple type if it has a display value), so the code would have to update these classes after the enum is initialized and all enum members created, replacing the raw references with their corresponding Enum objects.

Note: See TracTickets for help on using tickets.
Back to Top