Как добавить дополнительные данные в TextChoices?

Как добавить дополнительные данные в django.db.models.TextChoices?

class Fruit(models.TextChoices):

    APPLE = ('myvalue', True, 'mylabel')

таким образом, что:

>>> Fruit.APPLE.is_tasty
True
>>> # And it still works otherwise
>>> Fruit.APPLE.value
'myvalue'
>>> Fruit.APPLE.label
'mylabel'

Вам нужно сделать нечто подобное тому, что предлагается в документации python Enum docs, но в отличие от python Enum, о метке уже позаботились models.Choices:

class Fruit(models.TextChoices):

    APPLE = ('myvalue', True, 'mylabel')

    def __new__(cls, value, is_tasty):
        obj = str.__new__(cls, value)
        obj._value_ = value
        obj.is_tasty = is_tasty
        return obj

Если вы используете это на IntegerChoices, вам понадобится int.__new__. Если вы используете __init__ вместо __new__, значение перечисления станет ('myvalue', True), которое используется в Fruit.choices и, вероятно, не подойдет для вашей модели Field.

Обратите внимание, что когда перечисление choices используется для поля модели, вы никогда не передаете ему перечисление, поэтому оно не знает о перечислении. Например, поле формы, производное от него с помощью ModelForm, будет рассматривать их как значения str, и после POST значение поля будет обычным str, а не значением перечисления. Для случая формы вы можете определить MyForm.clean_fruit или предоставить поле формы вручную TypedChoiceField(coerce=Fruit), в других случаях вам, возможно, придется снова искать значение перечисления с помощью Fruit(value) или вы можете добавить этот миксин к вашему полю:

class EnumMixin:

    'Convert a DB value back to its Choices value'

    def __init__(self, *args, enum : models.Choices, **kwargs):
        self.__enum = enum
        # it sets choices for you using the enum
        super().__init__(*args, choices=enum.choices, **kwargs)

    def deconstruct(self):
        'Get constructor args to reconstruct this field with later'
        name, path, args, kwargs = super().deconstruct()
        kwargs['enum'] = self.__enum
        del kwargs['choices']
        return name, path, args, kwargs

    def from_db_value(self, value, expression, connection):
        # Convert from db value
        return self.__to_enum(value)

    def to_python(self, value):
        'Called by deserialization and during clean() method used in forms'
        return self.__to_enum(value)

    def __to_enum(self, value):
        if value is None:
            return None
        return self.__enum(value)

class EnumCharField(EnumMixin, models.CharField):
    pass

class MyModel(models.Model):
    field = EnumCharField(enum=Fruit, ...)

deconstruct используется миграциями django, но обратите внимание, что он не будет восстанавливать Enum в момент создания миграции, он будет использовать тот Enum, который у вас есть на момент применения миграции.

Почему бы не вызвать super?

Хотя super().__new__ работает нормально в общем случае, это не относится к подклассам Enum (models.TextChoices является models.Choices, который является Enum). В документации Python отмечается следующее:

Метод __new__(), если он определен, используется при создании членов Enum; затем он заменяется методом __new__() Enum, который используется после создания класса для поиска существующих членов.

Итак, EnumMeta заменяет __new__ класса на Enum.__new__ на Fruit, TextChoices, .... Если вы вызываете super().__new__ в Fruit.__new__, то это вызывает TextChoices.__new__, который на самом деле является Enum.__new__ и который не будет ожидать передаваемых вами аргументов (и даже если он примет ваши аргументы, он никогда не вызовет super().__new__ сам).

Это повысит:

ValueError: 'myvalue' is not a valid Fruit
Вернуться на верх