Source code for django_enum.choices

"""
Support for symmetrical property enumeration types derived from Django choice
types. These choices types are drop in replacements for the Django
IntegerChoices and TextChoices.
"""

import enum
import typing as t

from django import VERSION as django_version
from django.db.models import Choices
from django.db.models import IntegerChoices as DjangoIntegerChoices
from django.db.models import TextChoices as DjangoTextChoices
from django.db.models import enums as model_enums
from enum_properties import (
    DecomposeMixin,
    EnumPropertiesMeta,
    SymmetricMixin,
)

from django_enum.utils import choices, names

__all__ = ["TextChoices", "IntegerChoices", "FloatChoices", "FlagChoices"]

ChoicesType = (
    model_enums.ChoicesType
    if django_version[0:2] >= (5, 0)
    else getattr(model_enums, "ChoicesMeta")  # removed in Django 5.0
)

DEFAULT_BOUNDARY = getattr(enum, "KEEP", None)


class DjangoEnumPropertiesMeta(EnumPropertiesMeta, ChoicesType):  # type: ignore
    """
    A composite meta class that combines Django's Choices metaclass with
    enum-properties metaclass. This metaclass will add Django's expected
    choices attribute and label properties to enumerations and
    enum-properties' generic property support.
    """

    def __new__(mcs, classname, bases, classdict, **kwargs):
        cls = super().__new__(mcs, classname, bases, classdict, **kwargs)
        # choices does not allow duplicates, but base class construction breaks
        # this member, so we alias it here to stay compatible with enum-properties
        # interface
        # TODO - is this a fixable bug in ChoicesType?
        cls._member_names_ = (
            list(classdict._member_names.keys())
            if isinstance(classdict._member_names, dict)  # changes based on py ver
            else classdict._member_names
        )
        cls.__first_class_members__ = cls._member_names_
        return cls

    @property
    def names(self) -> list[str]:
        """
        For some eccentric enums list(Enum) is empty, so we override names
        if empty.

        :returns: list of enum value names
        """
        return super().names or names(self, override=True)  # type: ignore[misc]

    @property
    def choices(self) -> list[tuple[t.Any, str]]:
        """
        For some eccentric enums list(Enum) is empty, so we override
        choices if empty

        :returns: list of enum value choices
        """
        return super().choices or choices(self, override=True)  # type: ignore[misc]


class DjangoSymmetricMixin(SymmetricMixin):
    """
    An enumeration mixin that makes Django's Choices type label field
    symmetric.
    """

    _symmetric_builtins_ = ["name", "label"]


[docs] class TextChoices( DjangoSymmetricMixin, DjangoTextChoices, metaclass=DjangoEnumPropertiesMeta ): """ A character enumeration type that extends Django's TextChoices and accepts enum-properties property lists. """ def __hash__(self): return DjangoTextChoices.__hash__(self)
[docs] class IntegerChoices( # type: ignore[metaclass] DjangoSymmetricMixin, DjangoIntegerChoices, metaclass=DjangoEnumPropertiesMeta ): """ An integer enumeration type that extends Django's IntegerChoices and accepts enum-properties property lists. """ def __hash__(self): return DjangoIntegerChoices.__hash__(self)
[docs] class FloatChoices( # type: ignore[metaclass] DjangoSymmetricMixin, float, Choices, metaclass=DjangoEnumPropertiesMeta ): """ A floating point enumeration type that accepts enum-properties property lists. """ def __hash__(self): return float.__hash__(self) def __str__(self): return str(self.value)
# multiple inheritance type hint bug
[docs] class FlagChoices( # type: ignore DecomposeMixin, DjangoSymmetricMixin, enum.IntFlag, Choices, metaclass=DjangoEnumPropertiesMeta, # default boundary argument gets lost in the inheritance when choices # is included if it is not explicitly specified **({"boundary": DEFAULT_BOUNDARY} if DEFAULT_BOUNDARY is not None else {}), ): """ An integer flag enumeration type that accepts enum-properties property lists. Note that on Pythons before 3.14 there is a quirk to the choices type where member tuples including the label are not unpacked on declaration. This means if you want to define composite fields on these versions it might look like this on Python < 3.14: .. code-block:: python class MyFlag(FlagChoices): A = 1 << 0, "a" B = 1 << 1, "b" C = 1 << 2, "c" AB = A[0] | B[0], "ab" # Python < 3.14 BC = B[0] | C[0], "bc" # Python < 3.14 ABC = A[0] | B[0] | C[0], "abc" # Python < 3.14 And this on Python >= 3.14: .. code-block:: python class MyFlag(FlagChoices): A = 1 << 0, "a" B = 1 << 1, "b" C = 1 << 2, "c" AB = A | B, "ab" # Python >= 3.14 BC = B | C, "bc" # Python >= 3.14 ABC = A | B | C, "abc" # Python >= 3.14 """ def __hash__(self): return enum.IntFlag.__hash__(self)