Source code for typedpy.fields.enum

import enum
from typing import Any

from typedpy.commons import first_in, wrap_val
from typedpy.structures import FieldMeta
from .strings import String
from .serializable_field import SerializableField


class _EnumMeta(FieldMeta):
    def __getitem__(cls, values):
        if isinstance(values, (type,)) and issubclass(values, (enum.Enum,)):
            return cls(values=values)  # pylint: disable=E1120, E1123
        return cls(values=list(values))  # pylint: disable=E1120, E1123


def _all_values_from_single_enum(values):
    clazz = first_in(values).__class__
    if not (isinstance(clazz, (type,)) and issubclass(clazz, enum.Enum)):
        return False
    return all(v.__class__ is clazz for v in values)


[docs]class Enum(SerializableField, metaclass=_EnumMeta): """ Enum field. value can be one of predefined values. Arguments: values(`list` or `set` or `tuple`, alternatively an enum Type): allowed values. Can be of any type. Alternatively, can be an enum.Enum type. See example below. When defined with an enum.Enum, serialization converts to strings, while deserialization expects strings (unless using serialization_by_value). In this case, strings are converted to the original enum values. Another option is assign a list of specific values from an enum.Enum class. In this case, it will work like asigning an Enum class, but allowing only specific values of that enum (see example below). serialization_by_value(bool): optional When set to True and the values is an enum.Enum class, then instead of serializing by the name of the enum, serialize by its value, and similarly, when deserializing, expect the enum value instead of the name. Default is False. This is especially useful when you are interfacing with another system and the values are strings that you don't control, like the example below. Examples: .. code-block:: python class Values(enum.Enum): ABC = 1 DEF = 2 GHI = 3 class Example(Structure): arr = Array[Enum[Values]] e = Enum['abc', 'x', 'def', 3] example = Example(arr=[Values.ABC, 'DEF'],e=3) assert example.arr = [Values.ABC, Values.DEF] # deserialization example: deserialized = Deserializer(target_class=Example).deserialize({'arr': ['GHI', 'DEF', 'ABC'], 'e': 3}) assert deserialized.arr == [Values.GHI, Values.DEF, Values.ABC] An example of serialization_by_value: .. code-block:: python class StreamCommand(enum.Enum): open = "open new stream" close = "close current stream" delete = "delete steam" class Action(Structure): command: Enum(values=StreamCommand, serialization_by_value=True) .... assert Deserializer(Action).deserialize({"command": "delete stream"}).command is StreamCommand.delete An example of allowing only specific values of an Enum class, referencging StreamCommand in the previous example: .. code-block:: python class Action(Structure): command: Enum(values=[StreamCommand.open, StreamCommand.close], serialization_by_value=True) .... # command works like in the previous example, but allows open/close values from StreamCommand, thus # the following line results in an exception: Deserializer(Action).deserialize({"command": "delete stream"}) """ def __init__(self, *args, values, serialization_by_value: bool = False, **kwargs): if not values: raise ValueError("Enum requires values parameters") self._is_enum = ( issubclass(values, enum.Enum) if isinstance(values, (type,)) else _all_values_from_single_enum(values) ) self.serialization_by_value = serialization_by_value if self._is_enum: self._enum_class = ( values if isinstance(values, (type,)) else first_in(values).__class__ ) if serialization_by_value: self._enum_by_value = {e.value: e for e in self._enum_class} self._valid_enum_values = ( list(self._enum_class) if isinstance(values, (type,)) else values ) self.values = list(values) else: self.values = values super().__init__(*args, **kwargs) def _validate(self, value): if self._is_enum: enum_names = {v.name for v in self._valid_enum_values} if value not in enum_names and value not in self._valid_enum_values: enum_values = [r.name for r in self._valid_enum_values] if len(enum_values) < 11: raise ValueError( f"{self._name}: Got {value}; Expected one of: {', '.join(enum_values)}" ) raise ValueError( f"{self._name}: Got {value}; Expected a value of {self._enum_class}" ) elif value not in self.values: raise ValueError( f"{self._name}: Got {value}; Expected one of {', '.join([str(v) for v in self.values])}" ) def serialize(self, value): if self._is_enum: if self.serialization_by_value: if not isinstance(value.value, (bool, str, int, float)): raise TypeError( f"{self._name}: Cannot serialize value: {value.value}" ) return value.value if self.serialization_by_value else value.name return value def deserialize(self, value): if self._is_enum: if self.serialization_by_value: if value not in self._enum_by_value: raise ValueError(f"Invalid value: {wrap_val(value)}") return self._enum_by_value[value] if isinstance(value, (str,)): valid_names = {v.name for v in self._valid_enum_values} if value not in valid_names: raise ValueError(f"Invalid value: {wrap_val(value)}") return self._enum_class[value] self._validate(value) return value def __set__(self, instance, value): self._validate(value) if self._is_enum: if isinstance(value, (str,)): value = self._enum_class[value] super().__set__(instance, value) @property def get_type(self): if self._enum_class: return self._enum_class return Any
[docs]class EnumString(Enum, String): """ Combination of :class:`Enum` and :class:`String`. This is useful if you want to further limit your allowable enum values, using :class:`String` attributes, such as pattern, maxLength. Example: .. code-block:: python predefined_list = ['abc', 'x', 'def', 'yy'] EnumString(values=predefined_list, minLength=3) """ pass