1 3
import sys
2

3

4 3
class Field:
5

6
    """
7
    Base field for use in
8
    :class:`~mopidy.models.immutable.ValidatedImmutableObject`. These fields
9
    are responsible for type checking and other data sanitation in our models.
10

11
    For simplicity fields use the Python descriptor protocol to store the
12
    values in the instance dictionary. Also note that fields are mutable if
13
    the object they are attached to allow it.
14

15
    Default values will be validated with the exception of :class:`None`.
16

17
    :param default: default value for field
18
    :param type: if set the field value must be of this type
19
    :param choices: if set the field value must be one of these
20
    """
21

22 3
    def __init__(self, default=None, type=None, choices=None):
23 3
        self._name = None  # Set by ValidatedImmutableObjectMeta
24 3
        self._choices = choices
25 3
        self._default = default
26 3
        self._type = type
27

28 3
        if self._default is not None:
29 3
            self.validate(self._default)
30

31 3
    def validate(self, value):
32
        """Validate and possibly modify the field value before assignment"""
33 3
        if self._type and not isinstance(value, self._type):
34 3
            raise TypeError(
35
                f"Expected {self._name} to be a {self._type}, not {value!r}"
36
            )
37 3
        if self._choices and value not in self._choices:
38 3
            raise TypeError(
39
                f"Expected {self._name} to be a one of {self._choices}, not {value!r}"
40
            )
41 3
        return value
42

43 3
    def __get__(self, instance, owner):
44 3
        if not instance:
45 3
            return self
46 3
        return getattr(instance, "_" + self._name, self._default)
47

48 3
    def __set__(self, instance, value):
49 3
        if value is not None:
50 3
            value = self.validate(value)
51

52 3
        if value is None or value == self._default:
53 3
            self.__delete__(instance)
54
        else:
55 3
            setattr(instance, "_" + self._name, value)
56

57 3
    def __delete__(self, instance):
58 3
        if hasattr(instance, "_" + self._name):
59 3
            delattr(instance, "_" + self._name)
60

61

62 3
class String(Field):
63

64
    """
65
    Specialized :class:`Field` which is wired up for bytes and unicode.
66

67
    :param default: default value for field
68
    """
69

70 3
    def __init__(self, default=None):
71
        # TODO: normalize to unicode?
72
        # TODO: only allow unicode?
73
        # TODO: disallow empty strings?
74 3
        super().__init__(type=str, default=default)
75

76

77 3
class Date(String):
78
    """
79
    :class:`Field` for storing ISO 8601 dates as a string.
80

81
    Supported formats are ``YYYY-MM-DD``, ``YYYY-MM`` and ``YYYY``, currently
82
    not validated.
83

84
    :param default: default value for field
85
    """
86

87 3
    pass  # TODO: make this check for YYYY-MM-DD, YYYY-MM, YYYY using strptime.
88

89

90 3
class Identifier(String):
91
    """
92
    :class:`Field` for storing values such as GUIDs or other identifiers.
93

94
    Values will be interned.
95

96
    :param default: default value for field
97
    """
98

99 3
    def validate(self, value):
100 3
        value = super().validate(value)
101 3
        if isinstance(value, bytes):
102 0
            value = value.decode()
103 3
        return sys.intern(value)
104

105

106 3
class URI(Identifier):
107
    """
108
    :class:`Field` for storing URIs
109

110
    Values will be interned, currently not validated.
111

112
    :param default: default value for field
113
    """
114

115 3
    pass  # TODO: validate URIs?
116

117

118 3
class Integer(Field):
119
    """
120
    :class:`Field` for storing integer numbers.
121

122
    :param default: default value for field
123
    :param min: field value must be larger or equal to this value when set
124
    :param max: field value must be smaller or equal to this value when set
125
    """
126

127 3
    def __init__(self, default=None, min=None, max=None):
128 3
        self._min = min
129 3
        self._max = max
130 3
        super().__init__(type=int, default=default)
131

132 3
    def validate(self, value):
133 3
        value = super().validate(value)
134 3
        if self._min is not None and value < self._min:
135 3
            raise ValueError(
136
                f"Expected {self._name} to be at least {self._min}, not {value:d}"
137
            )
138 3
        if self._max is not None and value > self._max:
139 3
            raise ValueError(
140
                f"Expected {self._name} to be at most {self._max}, not {value:d}"
141
            )
142 3
        return value
143

144

145 3
class Boolean(Field):
146
    """
147
    :class:`Field` for storing boolean values
148

149
    :param default: default value for field
150
    """
151

152 3
    def __init__(self, default=None):
153 3
        super().__init__(type=bool, default=default)
154

155

156 3
class Collection(Field):
157
    """
158
    :class:`Field` for storing collections of a given type.
159

160
    :param type: all items stored in the collection must be of this type
161
    :param container: the type to store the items in
162
    """
163

164 3
    def __init__(self, type, container=tuple):
165 3
        super().__init__(type=type, default=container())
166

167 3
    def validate(self, value):
168 3
        if isinstance(value, str):
169 3
            raise TypeError(
170
                f"Expected {self._name} to be a collection of "
171
                f"{self._type.__name__}, not {value!r}"
172
            )
173 3
        for v in value:
174 3
            if not isinstance(v, self._type):
175 3
                raise TypeError(
176
                    f"Expected {self._name} to be a collection of "
177
                    f"{self._type.__name__}, not {value!r}"
178
                )
179 3
        return self._default.__class__(value) or None

Read our documentation on viewing source code .

Loading