Opened 5 years ago

Closed 4 years ago

Last modified 4 years ago

#30920 closed New feature (wontfix)

Choice enumeration types should not enforce enum.unique().

Reported by: Steve Jorgensen Owned by: nobody
Component: Database layer (models, ORM) Version: 3.0
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

The stated reason that Choice enumeration types enforce enum.unique() is "to ensure that values cannot be defined multiple times" but that's a misunderstanding of how Enum works with values and what enum.unique() is for.

Enum already enforces that no 2 instances may have the same value, but by default, it allows 2 or more attributes to refer to that same instance.

I believe that is desirable to allow situations like the following:

class Vehicle(models.TextChoices):
    CAR = 'C'
    TRUCK = 'T'
    JET_SKI = 'J'
    DEFAULT = 'C'

With enum.unique() not enforced, this will create 3 entries (not 4) and make the single CAR entry instance available through either Vehicle.CAR or Vehicle.DEFAULT. By enforcing enum.unique(), we cause an exception to be raised at DEFAULT = 'C' instead of making an alias to Vehicle.CAR, and that appears to be purely a limitation and not a benefit.

Change History (3)

comment:1 by Mariusz Felisiak, 4 years ago

Cc: Shai Berger pope1ni added
Resolution: wontfix
Status: newclosed
Summary: Choice enumeration types should not enforce enum.unique(). It does not mean what it seems like it should mean.Choice enumeration types should not enforce enum.unique().
Type: BugNew feature

Thanks for this report, however I don't think that it's the desired behavior. All properties (.names, .labels, .choices, .names ) will return only the first value without enforcing uniqueness, for example:

class Fruit(models.IntegerChoices):
    APPLE = 1, 'Apple'
    PINEAPPLE = 1, 'Pineapple'
    DEFAULT 1

>>> Fruit.choices
[(1, 'Apple')]
>>> Fruit.labels
['Apple']
>>> Fruit.values
[1]
>>> Fruit.names
['APPLE']

What can be confusing. We made few design decisions to create the most usable and less error-prone classes for defining choices, IMO. Choices is not a strict enum implementation.

comment:2 by Steve Jorgensen, 4 years ago

Makes sense. Thanks for explaining the decision in your response, @felixxm

comment:3 by Nick Pope, 4 years ago

Hi Steve,

As Felix says, we're using enums under the hood, but we've added some additional constraints on top.
These helper classes are intended to make working with field choices easier and more structured.

I see the following problems with allowing duplicate values:

  1. It is most likely not intended, e.g. a copy-paste error.
  2. Where we use display labels, it may be unclear that multiple entries refer to the same value.
  3. Following your example, when we load the value back from the database, is it "Default" or "Car"?

This is why we decided to enforce unique values. There is nothing to stop someone from doing things as they did before, i.e. not using Choices.

For your example of a default case, I'd suggest that the blank value is the default, e.g. set __empty__ = _('(Default)').
That way you can change the default easily without needing to make a new migration.
Alternatively, if you need to enforce the value in the database, you can just set it in clean() on your form or as the Field.default.

I hope that helps.

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