1# Copyright (c) 2010-2024 openpyxl
2
3
4"""
5Based on Python Cookbook 3rd Edition, 8.13
6http://chimera.labs.oreilly.com/books/1230000000393/ch08.html#_discussiuncion_130
7"""
8
9import datetime
10import re
11
12from openpyxl import DEBUG
13from openpyxl.utils.datetime import from_ISO8601
14
15from .namespace import namespaced
16
17class Descriptor:
18
19 def __init__(self, name=None, **kw):
20 self.name = name
21 for k, v in kw.items():
22 setattr(self, k, v)
23
24 def __set__(self, instance, value):
25 instance.__dict__[self.name] = value
26
27
28class Typed(Descriptor):
29 """Values must of a particular type"""
30
31 expected_type = type(None)
32 allow_none = False
33 nested = False
34
35 def __init__(self, *args, **kw):
36 super().__init__(*args, **kw)
37 self.__doc__ = f"Values must be of type {self.expected_type}"
38
39 def __set__(self, instance, value):
40 if not isinstance(value, self.expected_type):
41 if (not self.allow_none
42 or (self.allow_none and value is not None)):
43 msg = f"{instance.__class__}.{self.name} should be {self.expected_type} but value is {type(value)}"
44 if DEBUG:
45 msg = f"{instance.__class__}.{self.name} should be {self.expected_type} but {value} is {type(value)}"
46 raise TypeError(msg)
47 super().__set__(instance, value)
48
49 def __repr__(self):
50 return self.__doc__
51
52
53def _convert(expected_type, value):
54 """
55 Check value is of or can be converted to expected type.
56 """
57 if not isinstance(value, expected_type):
58 try:
59 value = expected_type(value)
60 except:
61 raise TypeError('expected ' + str(expected_type))
62 return value
63
64
65class Convertible(Typed):
66 """Values must be convertible to a particular type"""
67
68 def __set__(self, instance, value):
69 if ((self.allow_none and value is not None)
70 or not self.allow_none):
71 value = _convert(self.expected_type, value)
72 super().__set__(instance, value)
73
74
75class Max(Convertible):
76 """Values must be less than a `max` value"""
77
78 expected_type = float
79 allow_none = False
80
81 def __init__(self, **kw):
82 if 'max' not in kw and not hasattr(self, 'max'):
83 raise TypeError('missing max value')
84 super().__init__(**kw)
85
86 def __set__(self, instance, value):
87 if ((self.allow_none and value is not None)
88 or not self.allow_none):
89 value = _convert(self.expected_type, value)
90 if value > self.max:
91 raise ValueError('Max value is {0}'.format(self.max))
92 super().__set__(instance, value)
93
94
95class Min(Convertible):
96 """Values must be greater than a `min` value"""
97
98 expected_type = float
99 allow_none = False
100
101 def __init__(self, **kw):
102 if 'min' not in kw and not hasattr(self, 'min'):
103 raise TypeError('missing min value')
104 super().__init__(**kw)
105
106 def __set__(self, instance, value):
107 if ((self.allow_none and value is not None)
108 or not self.allow_none):
109 value = _convert(self.expected_type, value)
110 if value < self.min:
111 raise ValueError('Min value is {0}'.format(self.min))
112 super().__set__(instance, value)
113
114
115class MinMax(Min, Max):
116 """Values must be greater than `min` value and less than a `max` one"""
117 pass
118
119
120class Set(Descriptor):
121 """Value can only be from a set of know values"""
122
123 def __init__(self, name=None, **kw):
124 if not 'values' in kw:
125 raise TypeError("missing set of values")
126 kw['values'] = set(kw['values'])
127 super().__init__(name, **kw)
128 self.__doc__ = "Value must be one of {0}".format(self.values)
129
130 def __set__(self, instance, value):
131 if value not in self.values:
132 raise ValueError(self.__doc__)
133 super().__set__(instance, value)
134
135
136class NoneSet(Set):
137
138 """'none' will be treated as None"""
139
140 def __init__(self, name=None, **kw):
141 super().__init__(name, **kw)
142 self.values.add(None)
143
144 def __set__(self, instance, value):
145 if value == 'none':
146 value = None
147 super().__set__(instance, value)
148
149
150class Integer(Convertible):
151
152 expected_type = int
153
154
155class Float(Convertible):
156
157 expected_type = float
158
159
160class Bool(Convertible):
161
162 expected_type = bool
163
164 def __set__(self, instance, value):
165 if isinstance(value, str):
166 if value in ('false', 'f', '0'):
167 value = False
168 super().__set__(instance, value)
169
170
171class String(Typed):
172
173 expected_type = str
174
175
176class Text(String, Convertible):
177
178 pass
179
180
181class ASCII(Typed):
182
183 expected_type = bytes
184
185
186class Tuple(Typed):
187
188 expected_type = tuple
189
190
191class Length(Descriptor):
192
193 def __init__(self, name=None, **kw):
194 if "length" not in kw:
195 raise TypeError("value length must be supplied")
196 super().__init__(**kw)
197
198
199 def __set__(self, instance, value):
200 if len(value) != self.length:
201 raise ValueError("Value must be length {0}".format(self.length))
202 super().__set__(instance, value)
203
204
205class Default(Typed):
206 """
207 When called returns an instance of the expected type.
208 Additional default values can be passed in to the descriptor
209 """
210
211 def __init__(self, name=None, **kw):
212 if "defaults" not in kw:
213 kw['defaults'] = {}
214 super().__init__(**kw)
215
216 def __call__(self):
217 return self.expected_type()
218
219
220class Alias(Descriptor):
221 """
222 Aliases can be used when either the desired attribute name is not allowed
223 or confusing in Python (eg. "type") or a more descriptive name is desired
224 (eg. "underline" for "u")
225 """
226
227 def __init__(self, alias):
228 self.alias = alias
229
230 def __set__(self, instance, value):
231 setattr(instance, self.alias, value)
232
233 def __get__(self, instance, cls):
234 return getattr(instance, self.alias)
235
236
237class MatchPattern(Descriptor):
238 """Values must match a regex pattern """
239 allow_none = False
240
241 def __init__(self, name=None, **kw):
242 if 'pattern' not in kw and not hasattr(self, 'pattern'):
243 raise TypeError('missing pattern value')
244
245 super().__init__(name, **kw)
246 self.test_pattern = re.compile(self.pattern, re.VERBOSE)
247
248
249 def __set__(self, instance, value):
250
251 if value is None and not self.allow_none:
252 raise ValueError("Value must not be none")
253
254 if ((self.allow_none and value is not None)
255 or not self.allow_none):
256 if not self.test_pattern.match(value):
257 raise ValueError('Value does not match pattern {0}'.format(self.pattern))
258
259 super().__set__(instance, value)
260
261
262class DateTime(Typed):
263
264 expected_type = datetime.datetime
265
266 def __set__(self, instance, value):
267 if value is not None and isinstance(value, str):
268 try:
269 value = from_ISO8601(value)
270 except ValueError:
271 raise ValueError("Value must be ISO datetime format")
272 super().__set__(instance, value)