New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixed #27910: Added a ChoiceEnum class for use in field choices #11223
Conversation
|
||
Since the case where the enum values need to be integers is extremely | ||
common, Django provides a ``ChoiceIntEnum`` class (inheriting int and | ||
``ChoiceEnum``). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect the case where enum values are strings to be similarly common. Perhaps we should provide ChoiceStrEnum
too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may. The real argument, actually, was mirroring the stdlib's Enum
and IntEnum
. More opinions welcome.
d6d47f1
to
64125b8
Compare
Hello Shai,
Reason for this change proposal is, that I somehow dislike storing redundant information together within an enum field. In your implementation, choices would be stored side-by-side with the enum members. Here the label instead is stored together with the enum member. This imo is somehow cleaner. We could of course rename our special label field to What do you think of it? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @shaib.
This looks really cool and would neatly replace the rubbishy way that I've done this in the past.
I've had an initial pass at reviewing this, but some improvements came to mind:
- It would be nice to have a
values
andlabels
property on the class to access a list of each. - I would then rename
display
tolabel
to match the newlabels
asdisplays
sounds off... - I think that everything in
ChoiceEnum
can be pushed into the metaclass instead. - Defining
ChoiceStrEnum
will make using it easier, akin toChoiceIntEnum
. - We should enforce unique mapping of name to values via
enum.unique()
.
I have whipped up an improved version for consideration: https://gist.github.com/pope1ni/f4bf33bb408f95cbf8791b3b0332b002
We will also need to think how this works with blank=True
automatically adding a blank value and possible some other cases, e.g. system checks, etc., but none of these things are insurmountable problems.
django/db/models/fields/__init__.py
Outdated
|
||
|
||
class ChoiceIntEnum(int, ChoiceEnum): | ||
"""Included for symmetry with IntEnum""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would remove this docstring. The documentation also lacks an example of usage which I see is in the docstring above.
django/db/models/fields/__init__.py
Outdated
return self._choices[self] | ||
|
||
@classmethod | ||
def validate(cls, value, message=_("Invalid choice")): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the use of this .validate()
method documented anywhere? What is it used by?
@@ -96,36 +96,42 @@ and the second element is the human-readable name. For example:: | |||
('SR', 'Senior'), | |||
) | |||
|
|||
Generally, it's best to define choices inside a model class, and to | |||
define a suitably-named constant for each value:: | |||
Django provides a special Enum type you can subclass to define this iterable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... a special ...
- it already provides more than one.
We should probably use :class:`django.db.models.ChoiceEnum`
, etc. and define some proper documentation sections for these new types to aid discovery.
django/db/models/fields/__init__.py
Outdated
try: | ||
cls(value) | ||
except ValueError: | ||
raise exceptions.ValidationError(message) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does the message need to be passed into the class method? (Maybe there is a good reason I've missed.)
I also think we want to state which choice enum and what value caused this to happen, e.g.
@classmethod
def validate(cls, value):
try:
cls(value)
except ValueError as e:
raise exceptions.ValidationError('Invalid choice: %s.%s' % (cls.__name__, value)) from e
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great work! don't forget to rebase after addressing the review comments.
I really like this implementation. So much clearer when you have lots of options. I tried it out and I think it could be improved in regards of using it in querysets. Now just using the Student.objects.filter(year_in_school=YearInSchool.FRESHMAN) would result in a Of course you can make sure to use In previous implementation I have seen the way to deal with this is to add: def __str__(self):
return self.value Works for me using string enums, but not sure how this would work on integer enums. Also Not really sure on what is the correct way of handling it, I just know it might introduce lots of mistyping and errors when people forget the .value in a comparison or queryset filter. |
Perhaps the |
Yeah. that would be cool, but wouldn't it introduce a much large change. Using the ChoiceEnum is still optional. I fiddled a little bit more on my ChoiceEnum class and added: def __str__(self):
return self.value
def __eq__(self, other):
if isinstance(other, self.__class__):
return super().__eq__(other)
else:
return self.value == other which made it work i a nice way. I can still compare Enums to Enums but if it is not an enum I am comparing to the value of the enum.
|
Why not def __str__(self):
return str(self.label) This in my opinion is somehow more meaningful. |
Hi all, In the last few weeks, I have been unable to give this PR and discussion the attention they deserve, and I'm not sure when I'll be able to do so. So if anyone wants to claim ownership and push this forward, go ahead. On today's discussion, though, I'd like to add that my favorite tenet of the Zen, one which I find grossly under-appreciated, is "In the face of ambiguity, resist the temptation to guess". What @Krolken and @jrief together point out is, that in the case of a I think the regrettable, but only choice we have is to make |
@shaib are you open to accepting PR on your branch? |
@auvipy in principle, of course. In practice, can't promise anything. |
As far as I understand Python, the |
If the ChoiceEnum needs to be subclassed for usage I don't see why it should be part of the codebase. Am I right in assuming what we want with the ChoiceEnum is to get away from writing: class Student(models.Model):
FRESHMAN = 'FR'
SOPHOMORE = 'SO'
JUNIOR = 'JR'
SENIOR = 'SR'
YEAR_IN_SCHOOL_CHOICES = (
(FREHSMAN, _('Freshman')),
(SOPHOMORE, _('Sophomore')),
(JUNIOR, _('Junior')),
(SENIOR, _('Senior'))
)
years_in_school = modesl.Charfield(max_length=20, choices=YEAR_IN_SCHOOL_CHOICES)
#####
Student.objects.filter(year_in_school=Student.FRESHMAN) # Here I need to know that FRESHMAN is one of the choices. The benefit I saw with this implementation was that I can define clear sets of choices to be used across several models, classes and functions. And I could provide a nice, translatable string for the Django Admin. |
To prevent boilerplate?! |
I believe this comment is mistaken, and I just added tests that prove it. That is, the clauses you quote are what you see when you ask Django for a string representation of the query, but the actual queries executed in the database are the correct ones (note that if you send
doesn't hold even for
That is generally correct, but the objects under discussion are So, the way I see it, we can do one of several things:
Further comments welcome, and I think I'll take this back to the developers' list as well. |
I suggest to return the label as the human-readable string. This is in contrast to Python's The reason I proposed def __str__(self):
return str(self.label) is, that Anyway, I also would implement def __repr__(self):
return self.value # or maybe wrapped inside '<...>' |
@shaib I went over my implementation and realised that I was not inheriting from I also wrote some application testing around it and found that messing with the Enum implementation lead to headaches as it stops doing what I expect it to do. I am all for your implementation :) |
The class can serve as a base class for user enums, supporting translatable human-readable names, or names automatically inferred from the enum member name. A ChoiceIntEnum is also provided. Thanks Ian Foote for review.
67c0918
to
be50a9e
Compare
be50a9e
to
7b79203
Compare
@shaib Thanks for this feature 🚀 I rebased from master, added @pope1ni Would you like to push it forward? 💚 It seems that you have ideas how to improve it and we are quite close 🏃♂️ . |
@felixxm Would be happy to, yes. |
@pope1ni Great 🚀 Feel-free to update this patch in your PR. |
Updated in #11540. |
The class can serve as a base class for user enums, supporting
translatable human-readable names, or names automatically inferred
from the enum member name.
A ChoiceIntEnum is also probided.