Opened 5 years ago

Closed 4 years ago

Last modified 4 years ago

#30902 closed Bug (fixed)

The value of a TextChoices/IntegerChoices field has a differing type

Reported by: Mikail Owned by: nobody
Component: Database layer (models, ORM) Version: 3.0
Severity: Release blocker Keywords: enum
Cc: pope1ni, Shai Berger Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

If we create an instance of a model having a CharField or IntegerField with the keyword choices pointing to IntegerChoices or TextChoices, the value returned by the getter of the field will be of the same type as the one created by enum.Enum (enum value).

For example, this model:

from django.db import models
from django.utils.translation import gettext_lazy as _


class MyChoice(models.TextChoices):
    FIRST_CHOICE = "first", _("The first choice, it is")
    SECOND_CHOICE = "second", _("The second choice, it is")


class MyObject(models.Model):
    my_str_value = models.CharField(max_length=10, choices=MyChoice.choices)

Then this test:

from django.test import TestCase
from testing.pkg.models import MyObject, MyChoice


class EnumTest(TestCase):
    def setUp(self) -> None:
        self.my_object = MyObject.objects.create(my_str_value=MyChoice.FIRST_CHOICE)

    def test_created_object_is_str(self):
        my_object = self.my_object
        self.assertIsInstance(my_object.my_str_value, str)
        self.assertEqual(str(my_object.my_str_value), "first")

    def test_retrieved_object_is_str(self):
        my_object = MyObject.objects.last()
        self.assertIsInstance(my_object.my_str_value, str)
        self.assertEqual(str(my_object.my_str_value), "first")

And then the results:

(django30-venv) ➜  django30 ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F.
======================================================================
FAIL: test_created_object_is_str (testing.tests.EnumTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mikailkocak/Development/django30/testing/tests.py", line 14, in test_created_object_is_str
    self.assertEqual(str(my_object.my_str_value), "first")
AssertionError: 'MyChoice.FIRST_CHOICE' != 'first'
- MyChoice.FIRST_CHOICE
+ first


----------------------------------------------------------------------
Ran 2 tests in 0.002s

FAILED (failures=1)

We notice when invoking __str__(...) we don't actually get the value property of the enum value which can lead to some unexpected issues, especially when communicating to an external API with a freshly created instance that will send MyEnum.MyValue, and the one that was retrieved would send my_value.

Attachments (1)

ticket_30902.zip (14.3 KB ) - added by Carlton Gibson 5 years ago.
Sample project with provided models. Run ./manage.py test

Download all attachments as: .zip

Change History (9)

comment:1 by Carlton Gibson, 5 years ago

Cc: pope1ni added
Severity: NormalRelease blocker
Triage Stage: UnreviewedAccepted

Hi NyanKiyoshi, what a lovely report. Thank you.

Clearly :) the expected behaviour is that test_created_object_is_str should pass.

It's interesting that the underlying __dict__ values differ, which explains all I guess:

Created: {'_state': <django.db.models.base.ModelState object at 0x10730efd0>, 'id': 1, 'my_str_value': <MyChoice.FIRST_CHOICE: 'first'>}
Retrieved: {'_state': <django.db.models.base.ModelState object at 0x1072b5eb8>, 'id': 1, 'my_str_value': 'first'}

Good catch. Thanks again.

comment:2 by Carlton Gibson, 5 years ago

Type: UncategorizedBug

by Carlton Gibson, 5 years ago

Attachment: ticket_30902.zip added

Sample project with provided models. Run ./manage.py test

comment:3 by Carlton Gibson, 5 years ago

Has patch: set

Possible PR.

comment:4 by Shai Berger, 5 years ago

Cc: Shai Berger added
Patch needs improvement: set

This issue only arises if one explicitly calls str() on the enum value. Otherwise, as the supplied test shows, the value is an instance of str. As such, it serializes e.g. to JSON correctly with json.dumps(), so I don't see an issue with external APIs either.

There's deeper discussion in the PR.

comment:5 by Carlton Gibson, 5 years ago

So discussion is on the PR, but, summary of one-point here, we need this to work:

>>> t = Template("Will '{{ object.my_str_value }}' render how users expect it to?")
>>> c = Context({"object": my_object })
>>> t.render(c)
"Will 'MyChoice.FIRST_CHOICE' render how users expect it to?"

Where currently it doesn't.

Last edited 5 years ago by Carlton Gibson (previous) (diff)

comment:6 by Mariusz Felisiak <felisiak.mariusz@…>, 4 years ago

Resolution: fixed
Status: newclosed

In dbcd7b06:

Fixed #30902 -- Added str() for model choice enums.

Allows expected behavior when cast to str, also matching behaviour of
created instances with those fetched from the DB.

Thanks to Simon Charette, Nick Pope, and Shai Berger for reviews.

comment:7 by Mariusz Felisiak <felisiak.mariusz@…>, 4 years ago

In 8740ff33:

[3.0.x] Fixed #30902 -- Added str() for model choice enums.

Allows expected behavior when cast to str, also matching behaviour of
created instances with those fetched from the DB.

Thanks to Simon Charette, Nick Pope, and Shai Berger for reviews.

Backport of dbcd7b064e7278614f29fc45468d461e263d4da7 from master

comment:8 by Mariusz Felisiak, 4 years ago

Patch needs improvement: unset
Note: See TracTickets for help on using tickets.
Back to Top