formencode.schema
Covered: 133 lines
Missed: 236 lines
Skipped 98 lines
Percent: 36 %
  1
from interfaces import *
  2
from api import *
  3
from api import _
  4
import declarative
  6
__all__ = ['Schema']
  8
class Schema(FancyValidator):
 10
    """
 11
    A schema validates a dictionary of values, applying different
 12
    validators (be key) to the different values.  If
 13
    allow_extra_fields=True, keys without validators will be allowed;
 14
    otherwise they will raise Invalid. If filter_extra_fields is
 15
    set to true, then extra fields are not passed back in the results.
 17
    Validators are associated with keys either with a class syntax, or
 18
    as keyword arguments (class syntax is usually easier).  Something
 19
    like::
 21
        class MySchema(Schema):
 22
            name = Validators.PlainText()
 23
            phone = Validators.PhoneNumber()
 25
    These will not be available as actual instance variables, but will
 26
    be collected in a dictionary.  To remove a validator in a subclass
 27
    that is present in a superclass, set it to None, like::
 29
        class MySubSchema(MySchema):
 30
            name = None
 32
    Note that missing fields are handled at the Schema level.  Missing
 33
    fields can have the 'missing' message set to specify the error
 34
    message, or if that does not exist the *schema* message
 35
    'missingValue' is used.
 36
    """
 39
    pre_validators = []
 41
    chained_validators = []
 44
    allow_extra_fields = False
 47
    filter_extra_fields = False
 52
    if_key_missing = NoDefault
 55
    ignore_key_missing = False
 56
    compound = True
 57
    fields = {}
 58
    order = []
 60
    messages = {
 61
        'notExpected': _('The input field %(name)s was not expected.'),
 62
        'missingValue': _("Missing value"),
 63
        'badDictType': _("The input must be dict-like (not a %(type)s: %(value)r)"),
 64
        }
 66
    __mutableattributes__ = ('fields', 'chained_validators',
 67
                             'pre_validators')
 69
    def __classinit__(cls, new_attrs):
 70
        FancyValidator.__classinit__(cls, new_attrs)
 74
        if cls.__bases__ == (FancyValidator,):
 75
            return cls
 79
        for key, value in new_attrs.items():
 80
            if key in ('pre_validators', 'chained_validators',
 81
                       'view'):
 82
                continue
 83
            if is_validator(value):
 84
                cls.fields[key] = value
 85
                delattr(cls, key)
 88
            elif cls.fields.has_key(key):
 89
                del cls.fields[key]
 90
        for name, value in cls.fields.items():
 91
            cls.add_field(name, value)
 93
    def __initargs__(self, new_attrs):
 94
        for key, value in new_attrs.items():
 95
            if key in ('pre_validators', 'chained_validators',
 96
                       'view'):
 97
                continue
 98
            if is_validator(value):
 99
                self.fields[key] = value
100
                delattr(self, key)
103
            elif self.fields.has_key(key):
104
                del self.fields[key]
105
        for name, value in self.fields.items():
106
            self.add_field(name, value)
108
    def assert_dict(self, value, state):
109
        """
110
        Helper to assure we have proper input
111
        """
112
        if not hasattr(value, 'items'):
114
            raise Invalid(self.message('badDictType', state, type=type(value), value=value),
115
                          value, state)
117
    def _to_python(self, value_dict, state):
118
        if not value_dict:
119
            if self.if_empty is not NoDefault:
120
                return self.if_empty
121
            else:
122
                value_dict = {}
124
        for validator in self.pre_validators:
125
            value_dict = validator.to_python(value_dict, state)
127
        self.assert_dict(value_dict, state)
129
        new = {}
130
        errors = {}
131
        unused = self.fields.keys()
132
        if state is not None:
133
            previous_key = getattr(state, 'key', None)
134
            previous_full_dict = getattr(state, 'full_dict', None)
135
            state.full_dict = value_dict
136
        try:
137
            for name, value in value_dict.items():
138
                try:
139
                    unused.remove(name)
140
                except ValueError:
141
                    if not self.allow_extra_fields:
142
                        raise Invalid(
143
                            self.message('notExpected', state,
144
                                         name=repr(name)),
145
                            value_dict, state)
146
                    else:
147
                        if not self.filter_extra_fields:
148
                            new[name] = value
149
                        continue
150
                validator = self.fields[name]
152
                try:
153
                    new[name] = validator.to_python(value, state)
154
                except Invalid, e:
155
                    errors[name] = e
157
            for name in unused:
158
                validator = self.fields[name]
159
                try:
160
                    if_missing = validator.if_missing
161
                except AttributeError:
162
                    if_missing = NoDefault
163
                if if_missing is NoDefault:
164
                    if self.ignore_key_missing:
165
                        continue
166
                    if self.if_key_missing is NoDefault:
167
                        try:
168
                            message = validator.message('missing', state)
169
                        except KeyError:
170
                            message = self.message('missingValue', state)
171
                        errors[name] = Invalid(message, None, state)
172
                    else:
173
                        try:
174
                            new[name] = validator.to_python(self.if_key_missing, state)
175
                        except Invalid, e:
176
                            errors[name] = e
177
                else:
178
                    new[name] = validator.if_missing
180
            for validator in self.chained_validators:
181
                if (not hasattr(validator, 'validate_partial')
182
                    or not getattr(validator, 'validate_partial_form', False)):
183
                    continue
184
                try:
185
                    validator.validate_partial(value_dict, state)
186
                except Invalid, e:
187
                    sub_errors = e.unpack_errors()
188
                    if not isinstance(sub_errors, dict):
190
                        continue
191
                    merge_dicts(errors, sub_errors)
193
            if errors:
194
                raise Invalid(
195
                    format_compound_error(errors),
196
                    value_dict, state,
197
                    error_dict=errors)
199
            for validator in self.chained_validators:
200
                new = validator.to_python(new, state)
202
            return new
204
        finally:
205
            if state is not None:
206
                state.key = previous_key
207
                state.full_dict = previous_full_dict
209
    def _from_python(self, value_dict, state):
210
        chained = self.chained_validators[:]
211
        chained.reverse()
212
        finished = []
213
        for validator in chained:
214
            __traceback_info__ = 'for_python chained_validator %s (finished %s)' % (validator, ', '.join(map(repr, finished)) or 'none')
215
            finished.append(validator)
216
            value_dict = validator.from_python(value_dict, state)
217
        self.assert_dict(value_dict, state)
218
        new = {}
219
        errors = {}
220
        unused = self.fields.keys()
221
        if state is not None:
222
            previous_key = getattr(state, 'key', None)
223
            previous_full_dict = getattr(state, 'full_dict', None)
224
            state.full_dict = value_dict
225
        try:
226
            __traceback_info__ = None
227
            for name, value in value_dict.items():
228
                __traceback_info__ = 'for_python in %s' % name
229
                try:
230
                    unused.remove(name)
231
                except ValueError:
232
                    if not self.allow_extra_fields:
233
                        raise Invalid(
234
                            self.message('notExpected', state,
235
                                         name=repr(name)),
236
                            value_dict, state)
237
                    if not self.filter_extra_fields:
238
                        new[name] = value
239
                else:
240
                    try:
241
                        new[name] = self.fields[name].from_python(value, state)
242
                    except Invalid, e:
243
                        errors[name] = e
245
            del __traceback_info__
247
            for name in unused:
248
                validator = self.fields[name]
249
                try:
250
                    new[name] = validator.from_python(None, state)
251
                except Invalid, e:
252
                    errors[name] = e
254
            if errors:
255
                raise Invalid(
256
                    format_compound_error(errors),
257
                    value_dict, state,
258
                    error_dict=errors)
260
            pre = self.pre_validators[:]
261
            pre.reverse()
262
            for validator in pre:
263
                __traceback_info__ = 'for_python pre_validator %s' % validator
264
                new = validator.from_python(new, state)
266
            return new
268
        finally:
269
            if state is not None:
270
                state.key = previous_key
271
                state.full_dict = previous_full_dict
275
    def add_chained_validator(self, cls, validator):
276
        if self is not None:
277
            if self.chained_validators is cls.chained_validators:
278
                self.chained_validators = cls.chained_validators[:]
279
            self.chained_validators.append(validator)
280
        else:
281
            cls.chained_validators.append(validator)
283
    add_chained_validator = declarative.classinstancemethod(
284
        add_chained_validator)
286
    def add_field(self, cls, name, validator):
287
        if self is not None:
288
            if self.fields is cls.fields:
289
                self.fields = cls.fields.copy()
290
            self.fields[name] = validator
291
        else:
292
            cls.fields[name] = validator
294
    add_field = declarative.classinstancemethod(add_field)
296
    def add_pre_validator(self, cls, validator):
297
        if self is not None:
298
            if self.pre_validators is cls.pre_validators:
299
                self.pre_validators = cls.pre_validators[:]
300
            self.pre_validators.append(validator)
301
        else:
302
            cls.pre_validators.append(validator)
304
    add_pre_validator = declarative.classinstancemethod(add_pre_validator)
306
    def subvalidators(self):
307
        result = []
308
        result.extend(self.pre_validators)
309
        result.extend(self.chained_validators)
310
        result.extend(self.fields.values())
311
        return result
313
    def is_empty(self, value):
315
        return False
317
    def empty_value(self, value):
318
        return {}
320
def format_compound_error(v, indent=0):
321
    if isinstance(v, Exception):
322
        try:
323
            return str(v)
324
        except (UnicodeDecodeError, UnicodeEncodeError):
329
            return unicode(v)
330
    elif isinstance(v, dict):
331
        l = v.items()
332
        l.sort()
333
        return ('%s\n' % (' '*indent)).join(
334
            ["%s: %s" % (k, format_compound_error(value, indent=len(k)+2))
335
             for k, value in l
336
             if value is not None])
337
    elif isinstance(v, list):
338
        return ('%s\n' % (' '*indent)).join(
339
            ['%s' % (format_compound_error(value, indent=indent))
340
             for value in v
341
             if value is not None])
342
    elif isinstance(v, basestring):
343
        return v
344
    else:
345
        assert 0, "I didn't expect something like %s" % repr(v)
347
def merge_dicts(d1, d2):
348
    for key in d2:
349
        if key in d1:
350
            d1[key] = merge_values(d1[key], d2[key])
351
        else:
352
            d1[key] = d2[key]
353
    return d1
355
def merge_values(v1, v2):
356
    if (isinstance(v1, (str, unicode))
357
        and isinstance(v2, (str, unicode))):
358
        return v1 + '\n' + v2
359
    elif (isinstance(v1, (list, tuple))
360
          and isinstance(v2, (list, tuple))):
361
        return merge_lists(v1, v2)
362
    elif isinstance(v1, dict) and isinstance(v2, dict):
363
        return merge_dicts(v1, v2)
364
    else:
366
        return v1
368
def merge_lists(l1, l2):
369
    if len(l1) < len(l2):
370
        l1 = l1 + [None]*(len(l2)-len(l1))
371
    elif len(l2) < len(l1):
372
        l2 = l2 + [None]*(len(l1)-len(l2))
373
    result = []
374
    for l1item, l2item in zip(l1, l2):
375
        item = None
376
        if l1item is None:
377
            item = l2item
378
        elif l2item is None:
379
            item = l1item
380
        else:
381
            item = merge_values(l1item, l2item)
382
        result.append(item)
383
    return result
385
class SimpleFormValidator(FancyValidator):
386
    """
387
    This validator wraps a simple function that validates the form.
389
    The function looks something like this::
391
      >>> def validate(form_values, state, validator):
392
      ...     if form_values.get('country', 'US') == 'US':
393
      ...         if not form_values.get('state'):
394
      ...             return {'state': 'You must enter a state'}
395
      ...     if not form_values.get('country'):
396
      ...         form_values['country'] = 'US'
398
    This tests that the field 'state' must be filled in if the country
399
    is US, and defaults that country value to 'US'.  The ``validator``
400
    argument is the SimpleFormValidator instance, which you can use to
401
    format messages or keep configuration state in if you like (for
402
    simple ad hoc validation you are unlikely to need it).
404
    To create a validator from that function, you would do::
406
      >>> from formencode.schema import SimpleFormValidator
407
      >>> validator = SimpleFormValidator(validate)
408
      >>> validator.to_python({'country': 'US', 'state': ''}, None)
409
      Traceback (most recent call last):
410
          ...
411
      Invalid: state: You must enter a state
412
      >>> validator.to_python({'state': 'IL'}, None)
413
      {'country': 'US', 'state': 'IL'}
415
    The validate function can either return a single error message
416
    (that applies to the whole form), a dictionary that applies to the
417
    fields, None which means the form is valid, or it can raise
418
    Invalid.
420
    Note that you may update the value_dict *in place*, but you cannot
421
    return a new value.
423
    Another way to instantiate a validator is like this::
425
      >>> @SimpleFormValidator.decorate()
426
      ... def MyValidator(value_dict, state):
427
      ...     return None # or some more useful validation
429
    After this ``MyValidator`` will be a ``SimpleFormValidator``
430
    instance (it won't be your function).
431
    """
433
    __unpackargs__ = ('func',)
435
    validate_partial_form = False
437
    def __initargs__(self, new_attrs):
438
        self.__doc__ = getattr(self.func, '__doc__', None)
440
    def to_python(self, value_dict, state):
443
        value_dict = value_dict.copy()
444
        errors = self.func(value_dict, state, self)
445
        if not errors:
446
            return value_dict
447
        if isinstance(errors, basestring):
448
            raise Invalid(errors, value_dict, state)
449
        elif isinstance(errors, dict):
450
            raise Invalid(format_compound_error(errors),
451
                          value_dict, state, error_dict=errors)
452
        elif isinstance(errors, Invalid):
453
            raise errors
454
        else:
455
            raise TypeError(
456
                "Invalid error value: %r" % errors)
457
        return value_dict
459
    validate_partial = to_python
461
    def decorate(cls, **kw):
462
        def decorator(func):
463
            return cls(func, **kw)
464
        return decorator
466
    decorate = classmethod(decorate)