1import warnings 2 3from .api import _, is_validator, FancyValidator, Invalid, NoDefault 4from . import declarative 5from .exc import FERuntimeWarning 6 7__all__ = ['Schema'] 8 9 10class Schema(FancyValidator): 11 12 """ 13 A schema validates a dictionary of values, applying different 14 validators (be key) to the different values. If 15 allow_extra_fields=True, keys without validators will be allowed; 16 otherwise they will raise Invalid. If filter_extra_fields is 17 set to true, then extra fields are not passed back in the results. 18 19 Validators are associated with keys either with a class syntax, or 20 as keyword arguments (class syntax is usually easier). Something 21 like:: 22 23 class MySchema(Schema): 24 name = Validators.PlainText() 25 phone = Validators.PhoneNumber() 26 27 These will not be available as actual instance variables, but will 28 be collected in a dictionary. To remove a validator in a subclass 29 that is present in a superclass, set it to None, like:: 30 31 class MySubSchema(MySchema): 32 name = None 33 34 Note that missing fields are handled at the Schema level. Missing 35 fields can have the 'missing' message set to specify the error 36 message, or if that does not exist the *schema* message 37 'missingValue' is used. 38 """ 39 40 # These validators will be applied before this schema: 41 pre_validators = [] 42 # These validators will be applied after this schema: 43 chained_validators = [] 44 # If true, then it is not an error when keys that aren't 45 # associated with a validator are present: 46 allow_extra_fields = False 47 # If true, then keys that aren't associated with a validator 48 # are removed: 49 filter_extra_fields = False 50 # If this is given, then any keys that aren't available but 51 # are expected will be replaced with this value (and then 52 # validated!) This does not override a present .if_missing 53 # attribute on validators: 54 if_key_missing = NoDefault 55 # If true, then missing keys will be missing in the result, 56 # if the validator doesn't have if_missing on it already: 57 ignore_key_missing = False 58 compound = True 59 fields = {} 60 order = [] 61 accept_iterator = True 62 63 messages = dict( 64 notExpected=_('The input field %(name)s was not expected.'), 65 missingValue=_('Missing value'), 66 badDictType=_('The input must be dict-like' 67 ' (not a %(type)s: %(value)r)'), 68 singleValueExpected=_('Please provide only one value'),) 69 70 __mutableattributes__ = ('fields', 'chained_validators', 71 'pre_validators') 72 73 @staticmethod 74 def __classinit__(cls, new_attrs): 75 FancyValidator.__classinit__(cls, new_attrs) 76 # Don't bother doing anything if this is the most parent 77 # Schema class (which is the only class with just 78 # FancyValidator as a superclass): 79 if cls.__bases__ == (FancyValidator,): 80 return cls 81 # Scan through the class variables we've defined *just* 82 # for this subclass, looking for validators (both classes 83 # and instances): 84 for key, value in new_attrs.iteritems(): 85 if key in ('pre_validators', 'chained_validators'): 86 if is_validator(value): 87 msg = "Any validator with the name %s will be ignored." % \ 88 (key,) 89 warnings.warn(msg, FERuntimeWarning) 90 continue 91 if is_validator(value): 92 cls.fields[key] = value 93 delattr(cls, key) 94 # This last case means we're overwriting a validator 95 # from a superclass: 96 elif key in cls.fields: 97 del cls.fields[key] 98 99 for name, value in cls.fields.iteritems(): 100 cls.add_field(name, value) 101 102 def __initargs__(self, new_attrs): 103 self.fields = self.fields.copy() 104 for key, value in new_attrs.iteritems(): 105 if key in ('pre_validators', 'chained_validators'): 106 if is_validator(value): 107 msg = "Any validator with the name %s will be ignored." % \ 108 (key,) 109 warnings.warn(msg, FERuntimeWarning) 110 continue 111 if is_validator(value): 112 self.fields[key] = value 113 delattr(self, key) 114 # This last case means we're overwriting a validator 115 # from a superclass: 116 elif key in self.fields: 117 del self.fields[key] 118 119 def assert_dict(self, value, state): 120 """ 121 Helper to assure we have proper input 122 """ 123 if not hasattr(value, 'items'): 124 # Not a dict or dict-like object 125 raise Invalid( 126 self.message('badDictType', state, 127 type=type(value), value=value), value, state) 128 129 def _convert_to_python(self, value_dict, state): 130 if not value_dict: 131 if self.if_empty is not NoDefault: 132 return self.if_empty 133 value_dict = {} 134 135 for validator in self.pre_validators: 136 value_dict = validator.to_python(value_dict, state) 137 138 self.assert_dict(value_dict, state) 139 140 new = {} 141 errors = {} 142 unused = self.fields.keys() 143 if state is not None: 144 previous_key = getattr(state, 'key', None) 145 previous_full_dict = getattr(state, 'full_dict', None) 146 state.full_dict = value_dict 147 try: 148 for name, value in value_dict.items(): 149 try: 150 unused.remove(name) 151 except ValueError: 152 if not self.allow_extra_fields: 153 raise Invalid(self.message('notExpected', 154 state, name=repr(name)), value_dict, state) 155 if not self.filter_extra_fields: 156 new[name] = value 157 continue 158 validator = self.fields[name] 159 160 # are iterators (list, tuple, set, etc) allowed? 161 if self._value_is_iterator(value) and not getattr( 162 validator, 'accept_iterator', False): 163 errors[name] = Invalid(self.message( 164 'singleValueExpected', state), value_dict, state) 165 166 if state is not None: 167 state.key = name 168 try: 169 new[name] = validator.to_python(value, state) 170 except Invalid as e: 171 errors[name] = e 172 173 for name in unused: 174 validator = self.fields[name] 175 try: 176 if_missing = validator.if_missing 177 except AttributeError: 178 if_missing = NoDefault 179 if if_missing is NoDefault: 180 if self.ignore_key_missing: 181 continue 182 if self.if_key_missing is NoDefault: 183 try: 184 message = validator.message('missing', state) 185 except KeyError: 186 message = self.message('missingValue', state) 187 errors[name] = Invalid(message, None, state) 188 else: 189 if state is not None: 190 state.key = name 191 try: 192 new[name] = validator.to_python( 193 self.if_key_missing, state) 194 except Invalid as e: 195 errors[name] = e 196 else: 197 new[name] = validator.if_missing 198 199 if state is not None: 200 state.key = previous_key 201 for validator in self.chained_validators: 202 if (not hasattr(validator, 'validate_partial') or not getattr( 203 validator, 'validate_partial_form', False)): 204 continue 205 try: 206 validator.validate_partial(value_dict, state) 207 except Invalid as e: 208 sub_errors = e.unpack_errors() 209 if not isinstance(sub_errors, dict): 210 # Can't do anything here 211 continue 212 merge_dicts(errors, sub_errors) 213 214 if errors: 215 raise Invalid( 216 format_compound_error(errors), 217 value_dict, state, error_dict=errors) 218 219 for validator in self.chained_validators: 220 new = validator.to_python(new, state) 221 222 return new 223 224 finally: 225 if state is not None: 226 state.key = previous_key 227 state.full_dict = previous_full_dict 228 229 def _convert_from_python(self, value_dict, state): 230 chained = self.chained_validators[:] 231 chained.reverse() 232 finished = [] 233 for validator in chained: 234 __traceback_info__ = ( 235 'for_python chained_validator %s (finished %s)') % ( 236 validator, ', '.join(map(repr, finished)) or 'none') 237 finished.append(validator) 238 value_dict = validator.from_python(value_dict, state) 239 self.assert_dict(value_dict, state) 240 new = {} 241 errors = {} 242 unused = self.fields.keys() 243 if state is not None: 244 previous_key = getattr(state, 'key', None) 245 previous_full_dict = getattr(state, 'full_dict', None) 246 state.full_dict = value_dict 247 try: 248 __traceback_info__ = None 249 for name, value in value_dict.iteritems(): 250 __traceback_info__ = 'for_python in %s' % name 251 try: 252 unused.remove(name) 253 except ValueError: 254 if not self.allow_extra_fields: 255 raise Invalid(self.message('notExpected', 256 state, name=repr(name)), value_dict, state) 257 if not self.filter_extra_fields: 258 new[name] = value 259 else: 260 if state is not None: 261 state.key = name 262 try: 263 new[name] = self.fields[name].from_python(value, state) 264 except Invalid as e: 265 errors[name] = e 266 267 del __traceback_info__ 268 269 for name in unused: 270 validator = self.fields[name] 271 if state is not None: 272 state.key = name 273 try: 274 new[name] = validator.from_python(None, state) 275 except Invalid as e: 276 errors[name] = e 277 278 if errors: 279 raise Invalid( 280 format_compound_error(errors), 281 value_dict, state, error_dict=errors) 282 283 pre = self.pre_validators[:] 284 pre.reverse() 285 if state is not None: 286 state.key = previous_key 287 288 for validator in pre: 289 __traceback_info__ = 'for_python pre_validator %s' % validator 290 new = validator.from_python(new, state) 291 292 return new 293 294 finally: 295 if state is not None: 296 state.key = previous_key 297 state.full_dict = previous_full_dict 298 299 @declarative.classinstancemethod 300 def add_chained_validator(self, cls, validator): 301 if self is not None: 302 if self.chained_validators is cls.chained_validators: 303 self.chained_validators = cls.chained_validators[:] 304 self.chained_validators.append(validator) 305 else: 306 cls.chained_validators.append(validator) 307 308 @declarative.classinstancemethod 309 def add_field(self, cls, name, validator): 310 if self is not None: 311 if self.fields is cls.fields: 312 self.fields = cls.fields.copy() 313 self.fields[name] = validator 314 else: 315 cls.fields[name] = validator 316 317 @declarative.classinstancemethod 318 def add_pre_validator(self, cls, validator): 319 if self is not None: 320 if self.pre_validators is cls.pre_validators: 321 self.pre_validators = cls.pre_validators[:] 322 self.pre_validators.append(validator) 323 else: 324 cls.pre_validators.append(validator) 325 326 def subvalidators(self): 327 result = [] 328 result.extend(self.pre_validators) 329 result.extend(self.chained_validators) 330 result.extend(self.fields.itervalues()) 331 return result 332 333 def is_empty(self, value): 334 ## Generally nothing is empty for us 335 return False 336 337 def empty_value(self, value): 338 return {} 339 340 def _value_is_iterator(self, value): 341 if isinstance(value, basestring): 342 return False 343 elif isinstance(value, (list, tuple)): 344 return True 345 346 try: 347 for _v in value: 348 break 349 return True 350 ## @@: Should this catch any other errors?: 351 except TypeError: 352 return False 353 354 355def format_compound_error(v, indent=0): 356 if isinstance(v, Exception): 357 try: 358 return str(v) 359 except (UnicodeDecodeError, UnicodeEncodeError): 360 # There doesn't seem to be a better way to get a str() 361 # version if possible, and unicode() if necessary, because 362 # testing for the presence of a __unicode__ method isn't 363 # enough 364 return unicode(v) 365 elif isinstance(v, dict): 366 return ('%s\n' % (' ' * indent)).join( 367 '%s: %s' % (k, format_compound_error(value, indent=len(k) + 2)) 368 for k, value in sorted(v.iteritems()) if value is not None) 369 elif isinstance(v, list): 370 return ('%s\n' % (' ' * indent)).join( 371 '%s' % (format_compound_error(value, indent=indent)) 372 for value in v if value is not None) 373 elif isinstance(v, basestring): 374 return v 375 else: 376 assert False, "I didn't expect something like %s" % repr(v) 377 378 379def merge_dicts(d1, d2): 380 for key in d2: 381 d1[key] = merge_values(d1[key], d2[key]) if key in d1 else d2[key] 382 return d1 383 384 385def merge_values(v1, v2): 386 if isinstance(v1, basestring) and isinstance(v2, basestring): 387 return v1 + '\n' + v2 388 elif isinstance(v1, (list, tuple)) and isinstance(v2, (list, tuple)): 389 return merge_lists(v1, v2) 390 elif isinstance(v1, dict) and isinstance(v2, dict): 391 return merge_dicts(v1, v2) 392 else: 393 # @@: Should we just ignore errors? Seems we do... 394 return v1 395 396 397def merge_lists(l1, l2): 398 if len(l1) < len(l2): 399 l1 = l1 + [None] * (len(l2) - len(l1)) 400 elif len(l2) < len(l1): 401 l2 = l2 + [None] * (len(l1) - len(l2)) 402 result = [] 403 for l1item, l2item in zip(l1, l2): 404 item = None 405 if l1item is None: 406 item = l2item 407 elif l2item is None: 408 item = l1item 409 else: 410 item = merge_values(l1item, l2item) 411 result.append(item) 412 return result 413 414 415class SimpleFormValidator(FancyValidator): 416 """ 417 This validator wraps a simple function that validates the form. 418 419 The function looks something like this:: 420 421 >>> def validate(form_values, state, validator): 422 ... if form_values.get('country', 'US') == 'US': 423 ... if not form_values.get('state'): 424 ... return dict(state='You must enter a state') 425 ... if not form_values.get('country'): 426 ... form_values['country'] = 'US' 427 428 This tests that the field 'state' must be filled in if the country 429 is US, and defaults that country value to 'US'. The ``validator`` 430 argument is the SimpleFormValidator instance, which you can use to 431 format messages or keep configuration state in if you like (for 432 simple ad hoc validation you are unlikely to need it). 433 434 To create a validator from that function, you would do:: 435 436 >>> from formencode.schema import SimpleFormValidator 437 >>> validator = SimpleFormValidator(validate) 438 >>> validator.to_python({'country': 'US', 'state': ''}, None) 439 Traceback (most recent call last): 440 ... 441 Invalid: state: You must enter a state 442 >>> sorted(validator.to_python({'state': 'IL'}, None).items()) 443 [('country', 'US'), ('state', 'IL')] 444 445 The validate function can either return a single error message 446 (that applies to the whole form), a dictionary that applies to the 447 fields, None which means the form is valid, or it can raise 448 Invalid. 449 450 Note that you may update the value_dict *in place*, but you cannot 451 return a new value. 452 453 Another way to instantiate a validator is like this:: 454 455 >>> @SimpleFormValidator.decorate() 456 ... def MyValidator(value_dict, state): 457 ... return None # or some more useful validation 458 459 After this ``MyValidator`` will be a ``SimpleFormValidator`` 460 instance (it won't be your function). 461 """ 462 463 __unpackargs__ = ('func',) 464 465 validate_partial_form = False 466 467 def __initargs__(self, new_attrs): 468 self.__doc__ = getattr(self.func, '__doc__', None) 469 470 def to_python(self, value_dict, state): 471 # Since we aren't really supposed to modify things in-place, 472 # we'll give the validation function a copy: 473 value_dict = value_dict.copy() 474 errors = self.func(value_dict, state, self) 475 if not errors: 476 return value_dict 477 if isinstance(errors, basestring): 478 raise Invalid(errors, value_dict, state) 479 elif isinstance(errors, dict): 480 raise Invalid( 481 format_compound_error(errors), 482 value_dict, state, error_dict=errors) 483 elif isinstance(errors, Invalid): 484 raise errors 485 else: 486 raise TypeError( 487 "Invalid error value: %r" % errors) 488 return value_dict 489 490 validate_partial = to_python 491 492 @classmethod 493 def decorate(cls, **kw): 494 def decorator(func): 495 return cls(func, **kw) 496 return decorator 497