Package grassyknoll :: Package lib :: Module Norman
[hide private]

Source Code for Module grassyknoll.lib.Norman

  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   
30 -def lowerString(f):
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
39 -class NormanError(ValueError):
40 """error raised when a value cannot be normalized""" 41 pass
42 43 # XXX could also use a class that's responsible for cleaning up *keys* (like 44 # name=>id for XML or somesuch) 45 46 # XXX more informative error messages would sure be nice. Perhaps we can catch 47 # NormanErrors & add the field name in ObjectNorman? 48
49 -class Norman(object):
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
71 - def __call__(self, x):
72 raise NotImplementedError
73
74 -class ObjectNorman(Norman):
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
102 - def __setupCaches(self):
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 # don't treat __unknown as required, regardless of what it says 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
171 -class MultiValueNorman(Norman):
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
178 - def __init__(self, subnorman):
179 assert isinstance(subnorman, Norman) 180 self.subnorman=subnorman
181
182 - def __call__(self, x):
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
189 -class FunctionNorman(Norman):
190 """Norman that calls a function with curried kwargs""" 191
192 - def __init__(self, func, optional=False, filled=False, prohibited=False, 193 **kwargs):
194 self.func=func 195 self.kwargs=kwargs 196 super(FunctionNorman, self).__init__(optional=optional, filled=filled, 197 prohibited=prohibited)
198
199 - def __call__(self, x, **kwargs):
200 if kwargs: 201 kw=self.kwargs.copy() 202 kw.update(kwargs) 203 else: 204 kw=self.kwargs 205 206 return self.func(x, **kw)
207
208 -class IdemNorman(Norman):
209 """a Norman that just returns whatever you pass it 210 211 Perhaps useful for testing 212 """ 213
214 - def __call__(self, x):
215 return x
216
217 -class NoneNorman(Norman):
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):
229 if nones is not None: 230 self.nones=nones 231 super(NoneNorman, self).__init__(optional=optional, filled=filled, 232 prohibited=prohibited)
233 234 @lowerString
235 - def __call__(self, x):
236 if x in self.nones: 237 return None 238 else: 239 raise NormanError, x
240
241 -class BoolNorman(Norman):
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):
258 if trues is not None: 259 self.trues=trues 260 261 if falses is not None: 262 self.falses=falses 263 264 super(BoolNorman, self).__init__(optional=optional, filled=filled, 265 prohibited=prohibited)
266 267 @lowerString
268 - def __call__(self, x):
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
276 -class IntegerNorman(Norman):
277 """normalizes to an integer""" 278 279 @staticmethod
280 - def __call__(x):
281 try: 282 return int(x) 283 except ValueError: 284 raise NormanError, x
285
286 -class FloatNorman(Norman):
287 """normalizes to a float""" 288 289 @staticmethod
290 - def __call__(x):
291 try: 292 return float(x) 293 except ValueError: 294 raise NormanError, x
295
296 -class UnicodeNorman(Norman):
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):
305 self.encoding=encoding 306 self.errors=errors 307 super(UnicodeNorman, self).__init__(optional=optional, filled=filled, 308 prohibited=prohibited)
309
310 - def __call__(self, x):
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
321 -class StrNorman(Norman):
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
335 - def __call__(self, x):
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
346 -class MapNorman(Norman):
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
358 - def __call__(self, x):
359 try: 360 return self.map[x] 361 except KeyError: 362 raise NormanError, x
363
364 -class DateTimeNorman(Norman):
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):
376 self.kwargs=kwargs 377 super(DateTimeNorman, self).__init__(optional=optional, filled=filled, 378 prohibited=prohibited)
379
380 - def __call__(self, x):
381 # NOTE we could handle existing date()/time() like dateutil does (by 382 # filling defaults). This sounds unfun though. 383 if isinstance(x, datetime.datetime): 384 return x 385 elif isinstance(x, float): 386 # epoch 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
396 -class DateNorman(DateTimeNorman):
397 """Normalizes to L{datetime.date}"""
398 - def __call__(self, x):
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
408 -class TimeNorman(DateTimeNorman):
409 """Normalizes to L{datetime.time}"""
410 - def __call__(self, x):
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
420 -def TimestampNorman(optional=False, filled=True, prohibited=True):
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