1 """Norman likes things neat
2
3 XXX OH GOD NEED TESTS!
4
5 >>> pants=ObjectNorman()
6 >>> pants.sale_date=DateNorman()
7 >>> pants.size=IntegerNorman(optional=True)
8 >>> pants.color=UnicodeNorman()
9 >>> pants.constant=FunctionNorman(lambda x: u"CONSTANT", prohibited=True, filled=True)
10 >>> input=dict(sale_date='April 1, 2007', size='32', color='blue')
11 >>> bluejeans=pants(input)
12 >>> bluejeans is input
13 False
14 >>> bluejeans['size']
15 32
16 >>> bluejeans['color']
17 u'blue'
18 >>> bluejeans['sale_date']
19 datetime.date(2007, 4, 1)
20 >>> bluejeans['constant']
21 u'CONSTANT'
22 """
23
24 import dateutil.parser
25 import datetime
26 from functools import wraps
27
28 from grassyknoll.lib import strUniqueId, util
29
31 @wraps(f)
32 def wrapper(self, x, *args, **kwargs):
33 if isinstance(x, basestring):
34 return f(self, x.lower(), *args, **kwargs)
35 else:
36 return f(self, x, *args, **kwargs)
37 return wrapper
38
40 """error raised when a value cannot be normalized"""
41 pass
42
43
44
45
46
47
48
50 """base class for Normans
51
52 The following are only meaningful when the Norman is attached to a
53 L{ObjectNorman}.
54
55 @ivar optional: may this value be omitted
56 @type optional: bool
57
58 @ivar filled: should this value be filled in if it is not present in user
59 supplied data. The Norman will be called with a value of None.
60 @type filled: bool
61
62 @ivar prohibited: is this value allowed in user supplied data
63 @type prohibited: bool
64 """
65
66 - def __init__(self, optional=False, filled=False, prohibited=False):
67 self.optional=optional
68 self.filled=filled
69 self.prohibited=prohibited
70
72 raise NotImplementedError
73
75 """Norman that operates on dict or object.__dict__
76
77 Attach further Normans as attributes. These will be called on the
78 corresponding input value/attr.
79
80 See module docstring for an example.
81
82 @returns: a new object or dict with the transformation applied
83
84 @warning: instances of this class do some caching on the first __call__.
85 You should not modify attributes after using it to process input data.
86
87 @ivar __unknown: how unknown keys should be handled. One of 'ignore',
88 'delete', 'error'. May also be single-argument callable (perhaps another
89 Norman), which is passed the value.
90 """
91
92 - def __init__(self, unknown='error', optional=False, filled=False, prohibited=False):
93 assert callable(unknown) or unknown in ('ignore', 'delete', 'error')
94 self.__unknown=unknown
95 self.__normans=None
96 self.__prohibited=None
97 self.__required=None
98 self.__filled=None
99 super(ObjectNorman, self).__init__(optional=optional, filled=filled,
100 prohibited=prohibited)
101
103 self.__normans={}
104 self.__prohibited=set()
105 self.__required=[]
106 self.__filled={}
107
108 for name, norman in self.__dict__.iteritems():
109 if isinstance(norman, Norman):
110 self.__normans[name]=norman
111 if norman.prohibited: self.__prohibited.add(name)
112 if norman.filled: self.__filled[name]=norman
113
114 if (not norman.optional and
115 name!=util.privatize_name(ObjectNorman, '__unknown')):
116 self.__required.append(name)
117
118 - def byKey(self, d, unknown=None):
119 if self.__normans is None:
120 self.__setupCaches()
121
122 assert isinstance(d, dict)
123 unknown=unknown or self.__unknown
124
125 ret={}
126
127 for k, v in d.iteritems():
128 if k in self.__prohibited: raise NormanError, "prohibited: %s"%k
129 norman=self.__normans.get(k)
130 if norman is not None:
131 ret[k]=norman(v)
132 else:
133 if callable(unknown):
134 ret[k]=unknown(v)
135 elif unknown=='delete':
136 pass
137 elif unknown=='error':
138 raise NormanError, "unknown: %s"%k
139 elif unknown=='ignore':
140 ret[k]=v
141 else:
142 assert False, "Bad unknown: %r"%unknown
143
144 for name, norman in self.__filled.iteritems():
145 if name not in ret:
146 ret[name]=norman(None)
147
148 for name in self.__required:
149 if name not in ret:
150 raise NormanError, "required: %s"%name
151
152 return ret
153
154 - def byAttr(self, obj, unknown=None):
155 newdict=self.byKey(obj.__dict__, unknown)
156 newobj=type(obj).__new__(type(obj))
157 newobj.__dict__=newdict
158 return newobj
159
160 - def __call__(self, obj=None, unknown=None, **kwargs):
161 if kwargs:
162 assert obj is None
163 return self.byKey(kwargs)
164 elif isinstance(obj, dict):
165 assert not kwargs
166 return self.byKey(obj, unknown)
167 else:
168 assert not kwargs
169 return self.byAttr(obj, unknown)
170
172 """wraps up another L{Norman}, and returns a list of values
173
174 @ivar subnorman: the wrapped-up norman
175 @type subnorman: L{Norman}
176 """
177
179 assert isinstance(subnorman, Norman)
180 self.subnorman=subnorman
181
183 if isinstance(x, tuple):
184 x=list(x)
185 elif not isinstance(x, list):
186 x=[x]
187 return [self.subnorman(_) for _ in x]
188
190 """Norman that calls a function with curried kwargs"""
191
192 - def __init__(self, func, optional=False, filled=False, prohibited=False,
193 **kwargs):
198
207
209 """a Norman that just returns whatever you pass it
210
211 Perhaps useful for testing
212 """
213
216
218 """normalizes to None
219
220 Strings should be lower case.
221
222 @ivar nones: values to interpret as L{None}
223 @type nones: set
224 """
225
226 nones=set((None, "none", "null"))
227
228 - def __init__(self, nones=None, optional=False, filled=False, prohibited=False):
233
234 @lowerString
240
242 """normalizes to boolean
243
244 Strings in these should be lower case.
245
246 @ivar trues: values to interpret as L{True}
247 @type trues: set
248
249 @ivar falses: values to interpret as L{False}
250 @type falses: set
251 """
252
253 trues=set(("yes", "true", True, 1))
254 falses=set(("no", "false", False, 0))
255
256 - def __init__(self, trues=None, falses=None, optional=False, filled=False,
257 prohibited=False):
266
267 @lowerString
269 if x in self.trues:
270 return True
271 elif x in self.falses:
272 return False
273 else:
274 raise NormanError, x
275
277 """normalizes to an integer"""
278
279 @staticmethod
281 try:
282 return int(x)
283 except ValueError:
284 raise NormanError, x
285
287 """normalizes to a float"""
288
289 @staticmethod
291 try:
292 return float(x)
293 except ValueError:
294 raise NormanError, x
295
297 """Normalizes to L{unicode} by decoding.
298
299 Attributes C{encoding} and C{errors} are interpreted as in str.decode(..);
300 see its doc.
301 """
302
303 - def __init__(self, encoding='utf8', errors='strict', optional=False,
304 filled=False, prohibited=False):
309
311 if isinstance(x, unicode):
312 return x
313 elif not isinstance(x, str):
314 raise NormanError, x
315
316 try:
317 return x.decode(self.encoding, self.errors)
318 except UnicodeDecodeError:
319 raise NormanError, x
320
322 """Normalizes to L{str} by encoding.
323
324 Attributes C{encoding} and C{errors} are interpreted as in unicode.encode(..);
325 see its doc.
326 """
327
328 - def __init__(self, encoding='utf8', errors='strict', optional=False,
329 filled=False, prohibited=False):
330 self.encoding=encoding
331 self.errors=errors
332 super(StrNorman, self).__init__(optional=optional, filled=filled,
333 prohibited=prohibited)
334
336 if isinstance(x, str):
337 return x
338 elif not isinstance(x, unicode):
339 raise NormanError, x
340
341 try:
342 return x.encode(self.encoding, self.errors)
343 except UnicodeEncodeError:
344 raise NormanError, x
345
347 """Normalizes using a dict
348
349 @ivar map: input=>output
350 @type map: dict
351 """
352 - def __init__(self, map, optional=False, filled=False, prohibited=False):
353 self.map=map
354 super(MapNorman, self).__init__(optional=optional, filled=filled,
355 prohibited=prohibited)
356
357 @lowerString
359 try:
360 return self.map[x]
361 except KeyError:
362 raise NormanError, x
363
365 """Normalizes to a L{datetime.datetime}
366
367 @note: NOTE: this should probably be stricter. dateutils always returns a full
368 datetime, with date=today or with time=00:00 if not specified. We also
369 don't support tzinfo, largely b/c it sucks.
370
371 @ivar kwargs: keyword arguments to L{dateutil.parser.parse}, see its
372 U{documentation<http://labix.org/python-dateutil#head-a23e8ae0a661d77b89dfb3476f85b26f0b30349c>}
373 """
374
375 - def __init__(self, optional=False, filled=False, prohibited=False, **kwargs):
379
381
382
383 if isinstance(x, datetime.datetime):
384 return x
385 elif isinstance(x, float):
386
387 return datetime.datetime.fromtimestamp(x)
388 elif isinstance(x, basestring):
389 try:
390 return dateutil.parser.parse(x, **self.kwargs)
391 except ValueError:
392 raise NormanError, x
393 else:
394 raise NormanError, x
395
397 """Normalizes to L{datetime.date}"""
399 if isinstance(x, datetime.date):
400 return x
401 elif isinstance(x, datetime.datetime):
402 return x.date()
403 elif isinstance(x, basestring):
404 return super(DateNorman, self).__call__(x).date()
405 else:
406 raise NormanError, x
407
409 """Normalizes to L{datetime.time}"""
411 if isinstance(x, datetime.time):
412 return x
413 elif isinstance(x, datetime.datetime):
414 return x.time()
415 elif isinstance(x, basestring):
416 return super(TimeNorman, self).__call__(x).time()
417 else:
418 raise NormanError, x
419
421 """Norman that returns a current L{datetime.datetime}"""
422 return FunctionNorman(lambda x: datetime.datetime.now(),
423 optional=optional, filled=filled, prohibited=prohibited)
424
425 -def EtagNorman(optional=False, filled=True, prohibited=True):
426 """Norman that returns a reasonable value for use as an etag"""
427 return FunctionNorman(lambda x: unicode(strUniqueId()),
428 optional=optional, filled=filled, prohibited=prohibited)
429
430 if __name__ == "__main__":
431 import doctest
432 doctest.testmod()
433