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