1from __future__ import annotations
2
3import enum
4from types import DynamicClassAttribute
5from typing import Any
6
7from plain.utils.functional import Promise
8
9__all__ = ["Choices", "IntegerChoices", "TextChoices"]
10
11
12class ChoicesMeta(enum.EnumMeta):
13 """A metaclass for creating a enum choices."""
14
15 def __new__(
16 metacls: type,
17 classname: str,
18 bases: tuple[type, ...],
19 classdict: Any,
20 **kwds: Any,
21 ) -> type:
22 labels = []
23 for key in classdict._member_names:
24 value = classdict[key]
25 if (
26 isinstance(value, list | tuple)
27 and len(value) > 1
28 and isinstance(value[-1], Promise | str)
29 ):
30 *value, label = value
31 value = tuple(value)
32 else:
33 label = key.replace("_", " ").title()
34 labels.append(label)
35 # Use dict.__setitem__() to suppress defenses against double
36 # assignment in enum's classdict.
37 dict.__setitem__(classdict, key, value)
38 cls = super().__new__(metacls, classname, bases, classdict, **kwds) # type: ignore[misc]
39 for member, label in zip(cls.__members__.values(), labels):
40 member._label_ = label
41 return enum.unique(cls)
42
43 def __contains__(cls, member: object) -> bool:
44 if not isinstance(member, enum.Enum):
45 # Allow non-enums to match against member values.
46 return any(x.value == member for x in cls)
47 return super().__contains__(member)
48
49 @property
50 def names(cls) -> list[str]:
51 empty = ["__empty__"] if hasattr(cls, "__empty__") else []
52 return empty + [member.name for member in cls]
53
54 @property
55 def choices(cls) -> list[tuple[Any, str]]:
56 empty = [(None, cls.__empty__)] if hasattr(cls, "__empty__") else []
57 return empty + [(member.value, member.label) for member in cls]
58
59 @property
60 def labels(cls) -> list[str]:
61 return [label for _, label in cls.choices]
62
63 @property
64 def values(cls) -> list[Any]:
65 return [value for value, _ in cls.choices]
66
67
68class Choices(enum.Enum, metaclass=ChoicesMeta):
69 """Class for creating enumerated choices."""
70
71 @DynamicClassAttribute
72 def label(self) -> str:
73 return self._label_
74
75 def __str__(self) -> str:
76 """
77 Use value when cast to str, so that Choices set as model instance
78 attributes are rendered as expected in templates and similar contexts.
79 """
80 return str(self.value)
81
82 # A similar format was proposed for Python 3.10.
83 def __repr__(self) -> str:
84 return f"{self.__class__.__qualname__}.{self._name_}"
85
86
87class IntegerChoices(int, Choices):
88 """Class for creating enumerated integer choices."""
89
90 pass
91
92
93class TextChoices(str, Choices):
94 """Class for creating enumerated string choices."""
95
96 def _generate_next_value_(
97 name: str, start: int, count: int, last_values: list[str]
98 ) -> str:
99 return name