1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +00:00

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
This commit is contained in:
Nick Pope 2023-08-23 10:58:17 +01:00 committed by Mariusz Felisiak
parent 170b0a47b0
commit fe19b33e2f
2 changed files with 37 additions and 16 deletions

View File

@ -1,15 +1,27 @@
import enum import enum
import warnings import warnings
from types import DynamicClassAttribute
from django.utils.deprecation import RemovedInDjango60Warning from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.functional import Promise from django.utils.functional import Promise
from django.utils.version import PY311, PY312 from django.utils.version import PY311, PY312
if PY311: if PY311:
from enum import EnumType from enum import EnumType, IntEnum, StrEnum
from enum import property as enum_property
else: else:
from enum import EnumMeta as EnumType 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"] __all__ = ["Choices", "IntegerChoices", "TextChoices"]
@ -69,33 +81,30 @@ class ChoicesType(EnumType):
class Choices(enum.Enum, metaclass=ChoicesType): class Choices(enum.Enum, metaclass=ChoicesType):
"""Class for creating enumerated choices.""" """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): def label(self):
return self._label_ 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. # A similar format was proposed for Python 3.10.
def __repr__(self): def __repr__(self):
return f"{self.__class__.__qualname__}.{self._name_}" return f"{self.__class__.__qualname__}.{self._name_}"
class IntegerChoices(int, Choices): class IntegerChoices(Choices, IntEnum):
"""Class for creating enumerated integer choices.""" """Class for creating enumerated integer choices."""
pass pass
class TextChoices(str, Choices): class TextChoices(Choices, StrEnum):
"""Class for creating enumerated string choices.""" """Class for creating enumerated string choices."""
@staticmethod @staticmethod

View File

@ -9,6 +9,7 @@ from django.test import SimpleTestCase
from django.utils.deprecation import RemovedInDjango60Warning from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.functional import Promise from django.utils.functional import Promise
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.version import PY311
class Suit(models.IntegerChoices): class Suit(models.IntegerChoices):
@ -187,6 +188,7 @@ class ChoicesTests(SimpleTestCase):
def test_do_not_call_in_templates_member(self): def test_do_not_call_in_templates_member(self):
# do_not_call_in_templates is not implicitly treated as a member. # do_not_call_in_templates is not implicitly treated as a member.
Special = models.IntegerChoices("Special", "do_not_call_in_templates") Special = models.IntegerChoices("Special", "do_not_call_in_templates")
self.assertIn("do_not_call_in_templates", Special.__members__)
self.assertEqual( self.assertEqual(
Special.do_not_call_in_templates.label, Special.do_not_call_in_templates.label,
"Do Not Call In Templates", "Do Not Call In Templates",
@ -197,6 +199,16 @@ class ChoicesTests(SimpleTestCase):
"do_not_call_in_templates", "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): class Separator(bytes, models.Choices):
FS = b"\x1c", "File Separator" FS = b"\x1c", "File Separator"