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