0001from interfaces import *
0002from api import *
0003from api import _
0004import declarative
0005
0006__all__ = ['Schema']
0007
0008class Schema(FancyValidator):
0009
0010 """
0011 A schema validates a dictionary of values, applying different
0012 validators (be key) to the different values. If
0013 allow_extra_fields=True, keys without validators will be allowed;
0014 otherwise they will raise Invalid. If filter_extra_fields is
0015 set to true, then extra fields are not passed back in the results.
0016
0017 Validators are associated with keys either with a class syntax, or
0018 as keyword arguments (class syntax is usually easier). Something
0019 like::
0020
0021 class MySchema(Schema):
0022 name = Validators.PlainText()
0023 phone = Validators.PhoneNumber()
0024
0025 These will not be available as actual instance variables, but will
0026 be collected in a dictionary. To remove a validator in a subclass
0027 that is present in a superclass, set it to None, like::
0028
0029 class MySubSchema(MySchema):
0030 name = None
0031
0032 Note that missing fields are handled at the Schema level. Missing
0033 fields can have the 'missing' message set to specify the error
0034 message, or if that does not exist the *schema* message
0035 'missingValue' is used.
0036 """
0037
0038
0039 pre_validators = []
0040
0041 chained_validators = []
0042
0043
0044 allow_extra_fields = False
0045
0046
0047 filter_extra_fields = False
0048
0049
0050
0051
0052 if_key_missing = NoDefault
0053
0054
0055 ignore_key_missing = False
0056 compound = True
0057 fields = {}
0058 order = []
0059
0060 messages = {
0061 'notExpected': _('The input field %(name)s was not expected.'),
0062 'missingValue': _("Missing value"),
0063 'badDictType': _("The input must be dict-like (not a %(type)s: %(value)r)"),
0064 }
0065
0066 __mutableattributes__ = ('fields', 'chained_validators',
0067 'pre_validators')
0068
0069 def __classinit__(cls, new_attrs):
0070 FancyValidator.__classinit__(cls, new_attrs)
0071
0072
0073
0074 if cls.__bases__ == (FancyValidator,):
0075 return cls
0076
0077
0078
0079 for key, value in new_attrs.items():
0080 if key in ('pre_validators', 'chained_validators',
0081 'view'):
0082 continue
0083 if is_validator(value):
0084 cls.fields[key] = value
0085 delattr(cls, key)
0086
0087
0088 elif cls.fields.has_key(key):
0089 del cls.fields[key]
0090 for name, value in cls.fields.items():
0091 cls.add_field(name, value)
0092
0093 def __initargs__(self, new_attrs):
0094 for key, value in new_attrs.items():
0095 if key in ('pre_validators', 'chained_validators',
0096 'view'):
0097 continue
0098 if is_validator(value):
0099 self.fields[key] = value
0100 delattr(self, key)
0101
0102
0103 elif self.fields.has_key(key):
0104 del self.fields[key]
0105 for name, value in self.fields.items():
0106 self.add_field(name, value)
0107
0108 def assert_dict(self, value, state):
0109 """
0110 Helper to assure we have proper input
0111 """
0112 if not hasattr(value, 'items'):
0113
0114 raise Invalid(self.message('badDictType', state, type=type(value), value=value),
0115 value, state)
0116
0117 def _to_python(self, value_dict, state):
0118 if not value_dict:
0119 if self.if_empty is not NoDefault:
0120 return self.if_empty
0121 else:
0122 value_dict = {}
0123
0124 for validator in self.pre_validators:
0125 value_dict = validator.to_python(value_dict, state)
0126
0127 self.assert_dict(value_dict, state)
0128
0129 new = {}
0130 errors = {}
0131 unused = self.fields.keys()
0132 if state is not None:
0133 previous_key = getattr(state, 'key', None)
0134 previous_full_dict = getattr(state, 'full_dict', None)
0135 state.full_dict = value_dict
0136 try:
0137 for name, value in value_dict.items():
0138 try:
0139 unused.remove(name)
0140 except ValueError:
0141 if not self.allow_extra_fields:
0142 raise Invalid(
0143 self.message('notExpected', state,
0144 name=repr(name)),
0145 value_dict, state)
0146 else:
0147 if not self.filter_extra_fields:
0148 new[name] = value
0149 continue
0150 validator = self.fields[name]
0151
0152 try:
0153 new[name] = validator.to_python(value, state)
0154 except Invalid, e:
0155 errors[name] = e
0156
0157 for name in unused:
0158 validator = self.fields[name]
0159 try:
0160 if_missing = validator.if_missing
0161 except AttributeError:
0162 if_missing = NoDefault
0163 if if_missing is NoDefault:
0164 if self.ignore_key_missing:
0165 continue
0166 if self.if_key_missing is NoDefault:
0167 try:
0168 message = validator.message('missing', state)
0169 except KeyError:
0170 message = self.message('missingValue', state)
0171 errors[name] = Invalid(message, None, state)
0172 else:
0173 try:
0174 new[name] = validator.to_python(self.if_key_missing, state)
0175 except Invalid, e:
0176 errors[name] = e
0177 else:
0178 new[name] = validator.if_missing
0179
0180 for validator in self.chained_validators:
0181 if (not hasattr(validator, 'validate_partial')
0182 or not getattr(validator, 'validate_partial_form', False)):
0183 continue
0184 try:
0185 validator.validate_partial(value_dict, state)
0186 except Invalid, e:
0187 sub_errors = e.unpack_errors()
0188 if not isinstance(sub_errors, dict):
0189
0190 continue
0191 merge_dicts(errors, sub_errors)
0192
0193 if errors:
0194 raise Invalid(
0195 format_compound_error(errors),
0196 value_dict, state,
0197 error_dict=errors)
0198
0199 for validator in self.chained_validators:
0200 new = validator.to_python(new, state)
0201
0202 return new
0203
0204 finally:
0205 if state is not None:
0206 state.key = previous_key
0207 state.full_dict = previous_full_dict
0208
0209 def _from_python(self, value_dict, state):
0210 chained = self.chained_validators[:]
0211 chained.reverse()
0212 finished = []
0213 for validator in chained:
0214 __traceback_info__ = 'for_python chained_validator %s (finished %s)' % (validator, ', '.join(map(repr, finished)) or 'none')
0215 finished.append(validator)
0216 value_dict = validator.from_python(value_dict, state)
0217 self.assert_dict(value_dict, state)
0218 new = {}
0219 errors = {}
0220 unused = self.fields.keys()
0221 if state is not None:
0222 previous_key = getattr(state, 'key', None)
0223 previous_full_dict = getattr(state, 'full_dict', None)
0224 state.full_dict = value_dict
0225 try:
0226 __traceback_info__ = None
0227 for name, value in value_dict.items():
0228 __traceback_info__ = 'for_python in %s' % name
0229 try:
0230 unused.remove(name)
0231 except ValueError:
0232 if not self.allow_extra_fields:
0233 raise Invalid(
0234 self.message('notExpected', state,
0235 name=repr(name)),
0236 value_dict, state)
0237 if not self.filter_extra_fields:
0238 new[name] = value
0239 else:
0240 try:
0241 new[name] = self.fields[name].from_python(value, state)
0242 except Invalid, e:
0243 errors[name] = e
0244
0245 del __traceback_info__
0246
0247 for name in unused:
0248 validator = self.fields[name]
0249 try:
0250 new[name] = validator.from_python(None, state)
0251 except Invalid, e:
0252 errors[name] = e
0253
0254 if errors:
0255 raise Invalid(
0256 format_compound_error(errors),
0257 value_dict, state,
0258 error_dict=errors)
0259
0260 pre = self.pre_validators[:]
0261 pre.reverse()
0262 for validator in pre:
0263 __traceback_info__ = 'for_python pre_validator %s' % validator
0264 new = validator.from_python(new, state)
0265
0266 return new
0267
0268 finally:
0269 if state is not None:
0270 state.key = previous_key
0271 state.full_dict = previous_full_dict
0272
0273
0274
0275 def add_chained_validator(self, cls, validator):
0276 if self is not None:
0277 if self.chained_validators is cls.chained_validators:
0278 self.chained_validators = cls.chained_validators[:]
0279 self.chained_validators.append(validator)
0280 else:
0281 cls.chained_validators.append(validator)
0282
0283 add_chained_validator = declarative.classinstancemethod(
0284 add_chained_validator)
0285
0286 def add_field(self, cls, name, validator):
0287 if self is not None:
0288 if self.fields is cls.fields:
0289 self.fields = cls.fields.copy()
0290 self.fields[name] = validator
0291 else:
0292 cls.fields[name] = validator
0293
0294 add_field = declarative.classinstancemethod(add_field)
0295
0296 def add_pre_validator(self, cls, validator):
0297 if self is not None:
0298 if self.pre_validators is cls.pre_validators:
0299 self.pre_validators = cls.pre_validators[:]
0300 self.pre_validators.append(validator)
0301 else:
0302 cls.pre_validators.append(validator)
0303
0304 add_pre_validator = declarative.classinstancemethod(add_pre_validator)
0305
0306 def subvalidators(self):
0307 result = []
0308 result.extend(self.pre_validators)
0309 result.extend(self.chained_validators)
0310 result.extend(self.fields.values())
0311 return result
0312
0313 def is_empty(self, value):
0314
0315 return False
0316
0317 def empty_value(self, value):
0318 return {}
0319
0320def format_compound_error(v, indent=0):
0321 if isinstance(v, Exception):
0322 try:
0323 return str(v)
0324 except (UnicodeDecodeError, UnicodeEncodeError):
0325
0326
0327
0328
0329 return unicode(v)
0330 elif isinstance(v, dict):
0331 l = v.items()
0332 l.sort()
0333 return ('%s\n' % (' '*indent)).join(
0334 ["%s: %s" % (k, format_compound_error(value, indent=len(k)+2))
0335 for k, value in l
0336 if value is not None])
0337 elif isinstance(v, list):
0338 return ('%s\n' % (' '*indent)).join(
0339 ['%s' % (format_compound_error(value, indent=indent))
0340 for value in v
0341 if value is not None])
0342 elif isinstance(v, basestring):
0343 return v
0344 else:
0345 assert 0, "I didn't expect something like %s" % repr(v)
0346
0347def merge_dicts(d1, d2):
0348 for key in d2:
0349 if key in d1:
0350 d1[key] = merge_values(d1[key], d2[key])
0351 else:
0352 d1[key] = d2[key]
0353 return d1
0354
0355def merge_values(v1, v2):
0356 if (isinstance(v1, (str, unicode))
0357 and isinstance(v2, (str, unicode))):
0358 return v1 + '\n' + v2
0359 elif (isinstance(v1, (list, tuple))
0360 and isinstance(v2, (list, tuple))):
0361 return merge_lists(v1, v2)
0362 elif isinstance(v1, dict) and isinstance(v2, dict):
0363 return merge_dicts(v1, v2)
0364 else:
0365
0366 return v1
0367
0368def merge_lists(l1, l2):
0369 if len(l1) < len(l2):
0370 l1 = l1 + [None]*(len(l2)-len(l1))
0371 elif len(l2) < len(l1):
0372 l2 = l2 + [None]*(len(l1)-len(l2))
0373 result = []
0374 for l1item, l2item in zip(l1, l2):
0375 item = None
0376 if l1item is None:
0377 item = l2item
0378 elif l2item is None:
0379 item = l1item
0380 else:
0381 item = merge_values(l1item, l2item)
0382 result.append(item)
0383 return result
0384
0385class SimpleFormValidator(FancyValidator):
0386 """
0387 This validator wraps a simple function that validates the form.
0388
0389 The function looks something like this::
0390
0391 >>> def validate(form_values, state, validator):
0392 ... if form_values.get('country', 'US') == 'US':
0393 ... if not form_values.get('state'):
0394 ... return {'state': 'You must enter a state'}
0395 ... if not form_values.get('country'):
0396 ... form_values['country'] = 'US'
0397
0398 This tests that the field 'state' must be filled in if the country
0399 is US, and defaults that country value to 'US'. The ``validator``
0400 argument is the SimpleFormValidator instance, which you can use to
0401 format messages or keep configuration state in if you like (for
0402 simple ad hoc validation you are unlikely to need it).
0403
0404 To create a validator from that function, you would do::
0405
0406 >>> from formencode.schema import SimpleFormValidator
0407 >>> validator = SimpleFormValidator(validate)
0408 >>> validator.to_python({'country': 'US', 'state': ''}, None)
0409 Traceback (most recent call last):
0410 ...
0411 Invalid: state: You must enter a state
0412 >>> validator.to_python({'state': 'IL'}, None)
0413 {'country': 'US', 'state': 'IL'}
0414
0415 The validate function can either return a single error message
0416 (that applies to the whole form), a dictionary that applies to the
0417 fields, None which means the form is valid, or it can raise
0418 Invalid.
0419
0420 Note that you may update the value_dict *in place*, but you cannot
0421 return a new value.
0422
0423 Another way to instantiate a validator is like this::
0424
0425 >>> @SimpleFormValidator.decorate()
0426 ... def MyValidator(value_dict, state):
0427 ... return None # or some more useful validation
0428
0429 After this ``MyValidator`` will be a ``SimpleFormValidator``
0430 instance (it won't be your function).
0431 """
0432
0433 __unpackargs__ = ('func',)
0434
0435 validate_partial_form = False
0436
0437 def to_python(self, value_dict, state):
0438
0439
0440 value_dict = value_dict.copy()
0441 errors = self.func(value_dict, state, self)
0442 if not errors:
0443 return value_dict
0444 if isinstance(errors, basestring):
0445 raise Invalid(errors, value_dict, state)
0446 elif isinstance(errors, dict):
0447 raise Invalid(format_compound_error(errors),
0448 value_dict, state, error_dict=errors)
0449 elif isinstance(errors, Invalid):
0450 raise errors
0451 else:
0452 raise TypeError(
0453 "Invalid error value: %r" % errors)
0454
0455 def decorate(cls, **kw):
0456 def decorator(func):
0457 return cls(func, **kw)
0458 return decorator
0459
0460 decorate = classmethod(decorate)