From fe19b33e2f76045617067dd5123041ae4d3a91ee Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Wed, 23 Aug 2023 10:58:17 +0100 Subject: [PATCH] Modernized enumeration helpers on Python 3.11+. - use @enum.property https://docs.python.org/3/library/enum.html#enum.property - use @enum.nonmember Using @property on an enum class does not yield the expected result. do_not_call_in_templates attribute works because a @property instance is truthy. We can make this a literal True value as expected by using @enum.nonmember in Python 3.11+. https://docs.python.org/3/library/enum.html#enum.nonmember - used enum.IntEnum/StrEnum Python 3.11+ has ReprEnum which uses int.__str__() and str.__str__() for __str__() in the `IntEnum` and `StrEnum` subclasses. We can emulate that for Python < 3.11. https://docs.python.org/3/library/enum.html#enum.ReprEnum https://docs.python.org/3/library/enum.html#enum.IntEnum https://docs.python.org/3/library/enum.html#enum.StrEnum --- django/db/models/enums.py | 41 +++++++++++++++++++++++--------------- tests/model_enums/tests.py | 12 +++++++++++ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/django/db/models/enums.py b/django/db/models/enums.py index 24bec6b87b..bdc669bff0 100644 --- a/django/db/models/enums.py +++ b/django/db/models/enums.py @@ -1,15 +1,27 @@ import enum import warnings -from types import DynamicClassAttribute from django.utils.deprecation import RemovedInDjango60Warning from django.utils.functional import Promise from django.utils.version import PY311, PY312 if PY311: - from enum import EnumType + from enum import EnumType, IntEnum, StrEnum + from enum import property as enum_property else: from enum import EnumMeta as EnumType + from types import DynamicClassAttribute as enum_property + + class ReprEnum(enum.Enum): + def __str__(self): + return str(self.value) + + class IntEnum(int, ReprEnum): + pass + + class StrEnum(str, ReprEnum): + pass + __all__ = ["Choices", "IntegerChoices", "TextChoices"] @@ -69,33 +81,30 @@ class ChoicesType(EnumType): class Choices(enum.Enum, metaclass=ChoicesType): """Class for creating enumerated choices.""" - @DynamicClassAttribute + if PY311: + do_not_call_in_templates = enum.nonmember(True) + else: + + @property + def do_not_call_in_templates(self): + return True + + @enum_property def label(self): return self._label_ - @property - def do_not_call_in_templates(self): - return True - - def __str__(self): - """ - Use value when cast to str, so that Choices set as model instance - attributes are rendered as expected in templates and similar contexts. - """ - return str(self.value) - # A similar format was proposed for Python 3.10. def __repr__(self): return f"{self.__class__.__qualname__}.{self._name_}" -class IntegerChoices(int, Choices): +class IntegerChoices(Choices, IntEnum): """Class for creating enumerated integer choices.""" pass -class TextChoices(str, Choices): +class TextChoices(Choices, StrEnum): """Class for creating enumerated string choices.""" @staticmethod diff --git a/tests/model_enums/tests.py b/tests/model_enums/tests.py index 264a4b1703..32f0ad3668 100644 --- a/tests/model_enums/tests.py +++ b/tests/model_enums/tests.py @@ -9,6 +9,7 @@ from django.test import SimpleTestCase from django.utils.deprecation import RemovedInDjango60Warning from django.utils.functional import Promise from django.utils.translation import gettext_lazy as _ +from django.utils.version import PY311 class Suit(models.IntegerChoices): @@ -187,6 +188,7 @@ class ChoicesTests(SimpleTestCase): def test_do_not_call_in_templates_member(self): # do_not_call_in_templates is not implicitly treated as a member. Special = models.IntegerChoices("Special", "do_not_call_in_templates") + self.assertIn("do_not_call_in_templates", Special.__members__) self.assertEqual( Special.do_not_call_in_templates.label, "Do Not Call In Templates", @@ -197,6 +199,16 @@ class ChoicesTests(SimpleTestCase): "do_not_call_in_templates", ) + def test_do_not_call_in_templates_nonmember(self): + self.assertNotIn("do_not_call_in_templates", Suit.__members__) + if PY311: + self.assertIs(Suit.do_not_call_in_templates, True) + else: + # Using @property on an enum does not behave as expected. + self.assertTrue(Suit.do_not_call_in_templates) + self.assertIsNot(Suit.do_not_call_in_templates, True) + self.assertIsInstance(Suit.do_not_call_in_templates, property) + class Separator(bytes, models.Choices): FS = b"\x1c", "File Separator"