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    # These validators will be applied before this schema:
0039    pre_validators = []
0040    # These validators will be applied after this schema:
0041    chained_validators = []
0042    # If true, then it is not an error when keys that aren't
0043    # associated with a validator are present:
0044    allow_extra_fields = False
0045    # If true, then keys that aren't associated with a validator
0046    # are removed:
0047    filter_extra_fields = False
0048    # If this is given, then any keys that aren't available but
0049    # are expected  will be replaced with this value (and then
0050    # validated!)  This does not override a present .if_missing
0051    # attribute on validators:
0052    if_key_missing = NoDefault
0053    # If true, then missing keys will be missing in the result,
0054    # if the validator doesn't have if_missing on it already:
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        # Don't bother doing anything if this is the most parent
0072        # Schema class (which is the only class with just
0073        # FancyValidator as a superclass):
0074        if cls.__bases__ == (FancyValidator,):
0075            return cls
0076        # Scan through the class variables we've defined *just*
0077        # for this subclass, looking for validators (both classes
0078        # and instances):
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            # This last case means we're overwriting a validator
0087            # from a superclass:
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            # This last case means we're overwriting a validator
0102            # from a superclass:
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            # Not a dict or dict-like object
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                        # Can't do anything here
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        ## Generally nothing is empty for us
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            # There doesn't seem to be a better way to get a str()
0326            # version if possible, and unicode() if necessary, because
0327            # testing for the presence of a __unicode__ method isn't
0328            # enough
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        # @@: Should we just ignore errors?  Seems we do...
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        # Since we aren't really supposed to modify things in-place,
0439        # we'll give the validation function a copy:
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)