Latest Version: 0.9.6.2
/Users/bbangert/Programming/Python/FormEncode/formencode/schema.py
0001from interfaces import *
0002from api import *
0003import declarative
0004
0005__all__ = ['Schema']
0006
0007class Schema(FancyValidator):
0008
0009    """
0010    A schema validates a dictionary of values, applying different
0011    validators (be key) to the different values.  If
0012    allow_extra_fields=True, keys without validators will be allowed;
0013    otherwise they will raise Invalid. If filter_extra_fields is
0014    set to true, then extra fields are not passed back in the results.
0015
0016    Validators are associated with keys either with a class syntax, or
0017    as keyword arguments (class syntax is usually easier).  Something
0018    like::
0019
0020        class MySchema(Schema):
0021            name = Validators.PlainText()
0022            phone = Validators.PhoneNumber()
0023
0024    These will not be available as actual instance variables, but will
0025    be collected in a dictionary.  To remove a validator in a subclass
0026    that is present in a superclass, set it to None, like::
0027
0028        class MySubSchema(MySchema):
0029            name = None
0030    """
0031
0032    # These validators will be applied before this schema:
0033    pre_validators = []
0034    # These validators will be applied after this schema:
0035    chained_validators = []
0036    # If true, then it is not an error when keys that aren't
0037    # associated with a validator are present:
0038    allow_extra_fields = False
0039    # If true, then keys that aren't associated with a validator
0040    # are removed:
0041    filter_extra_fields = False
0042    # If this is given, then any keys that aren't available but
0043    # are expected  will be replaced with this value (and then
0044    # validated!)  This does not override a present .if_missing
0045    # attribute on validators:
0046    if_key_missing = NoDefault
0047    # If true, then missing keys will be missing in the result,
0048    # if the validator doesn't have if_missing on it already:
0049    ignore_key_missing = False
0050    compound = True
0051    fields = {}
0052    order = []
0053
0054    messages = {
0055        'notExpected': 'The input field %(name)s was not expected.',
0056        'missingValue': "Missing value",
0057        }
0058
0059    __mutableattributes__ = ('fields', 'chained_validators',
0060                             'pre_validators')
0061
0062    def __classinit__(cls, new_attrs):
0063        FancyValidator.__classinit__(cls, new_attrs)
0064        # Don't bother doing anything if this is the most parent
0065        # Schema class (which is the only class with just
0066        # FancyValidator as a superclass):
0067        if cls.__bases__ == (FancyValidator,):
0068            return cls
0069        # Scan through the class variables we've defined *just*
0070        # for this subclass, looking for validators (both classes
0071        # and instances):
0072        for key, value in new_attrs.items():
0073            if key in ('pre_validators', 'chained_validators',
0074                       'view'):
0075                continue
0076            if is_validator(value):
0077                cls.fields[key] = value
0078                delattr(cls, key)
0079            # This last case means we're overwriting a validator
0080            # from a superclass:
0081            elif cls.fields.has_key(key):
0082                del cls.fields[key]
0083        for name, value in cls.fields.items():
0084            cls.add_field(name, value)
0085
0086    def __initargs__(self, new_attrs):
0087        for key, value in new_attrs.items():
0088            if key in ('pre_validators', 'chained_validators',
0089                       'view'):
0090                continue
0091            if is_validator(value):
0092                self.fields[key] = value
0093                delattr(self, key)
0094            # This last case means we're overwriting a validator
0095            # from a superclass:
0096            elif self.fields.has_key(key):
0097                del self.fields[key]
0098        for name, value in self.fields.items():
0099            self.add_field(name, value)
0100
0101    def _to_python(self, value_dict, state):
0102        if not value_dict and self.if_empty is not NoDefault:
0103            return self.if_empty
0104
0105        for validator in self.pre_validators:
0106            value_dict = validator.to_python(value_dict, state)
0107
0108        new = {}
0109        errors = {}
0110        unused = self.fields.keys()
0111        if state is not None:
0112            previous_key = getattr(state, 'key', None)
0113            previous_full_dict = getattr(state, 'full_dict', None)
0114            state.full_dict = value_dict
0115        try:
0116            for name, value in value_dict.items():
0117                try:
0118                    unused.remove(name)
0119                except ValueError:
0120                    if not self.allow_extra_fields:
0121                        raise Invalid(
0122                            self.message('notExpected', state,
0123                                         name=repr(name)),
0124                            value_dict, state)
0125                    else:
0126                        if not self.filter_extra_fields:
0127                            new[name] = value
0128                        continue
0129                validator = self.fields[name]
0130
0131                try:
0132                    new[name] = validator.to_python(value, state)
0133                except Invalid, e:
0134                    errors[name] = e
0135
0136            for name in unused:
0137                validator = self.fields[name]
0138                try:
0139                    if_missing = validator.if_missing
0140                except AttributeError:
0141                    if_missing = NoDefault
0142                if if_missing is NoDefault:
0143                    if self.ignore_key_missing:
0144                        continue
0145                    if self.if_key_missing is NoDefault:
0146                        errors[name] = Invalid(
0147                            self.message('missingValue', state),
0148                            None, state)
0149                    else:
0150                        try:
0151                            new[name] = validator.to_python(self.if_key_missing, state)
0152                        except Invalid, e:
0153                            errors[name] = e
0154                else:
0155                    new[name] = validator.if_missing
0156
0157            if errors:
0158                for validator in self.chained_validators:
0159                    if (not hasattr(validator, 'validate_partial')
0160                        or not getattr(validator, 'validate_partial_form', False)):
0161                        continue
0162                    try:
0163                        validator.validate_partial(value_dict, state)
0164                    except Invalid, e:
0165                        sub_errors = e.unpack_errors()
0166                        if not isinstance(sub_errors, dict):
0167                            # Can't do anything here
0168                            continue
0169                        merge_dicts(errors, sub_errors)
0170
0171            if errors:
0172                raise Invalid(
0173                    format_compound_error(errors),
0174                    value_dict, state,
0175                    error_dict=errors)
0176
0177            for validator in self.chained_validators:
0178                new = validator.to_python(new, state)
0179
0180            return new
0181
0182        finally:
0183            if state is not None:
0184                state.key = previous_key
0185                state.full_dict = previous_full_dict
0186
0187    def _from_python(self, value_dict, state):
0188        chained = self.chained_validators[:]
0189        chained.reverse()
0190        finished = []
0191        for validator in chained:
0192            __traceback_info__ = 'for_python chained_validator %s (finished %s)' % (validator, ', '.join(map(repr, finished)) or 'none')
0193            finished.append(validator)
0194            value_dict = validator.from_python(value_dict, state)
0195        new = {}
0196        errors = {}
0197        unused = self.fields.keys()
0198        if state is not None:
0199            previous_key = getattr(state, 'key', None)
0200            previous_full_dict = getattr(state, 'full_dict', None)
0201            state.full_dict = value_dict
0202        try:
0203            __traceback_info__ = None
0204            for name, value in value_dict.items():
0205                __traceback_info__ = 'for_python in %s' % name
0206                try:
0207                    unused.remove(name)
0208                except ValueError:
0209                    if not self.allow_extra_fields:
0210                        raise Invalid(
0211                            self.message('notExpected', state,
0212                                         name=repr(name)),
0213                            value_dict, state)
0214                    if not self.filter_extra_fields:
0215                        new[name] = value
0216                else:
0217                    try:
0218                        new[name] = self.fields[name].from_python(value, state)
0219                    except Invalid, e:
0220                        errors[name] = e
0221
0222            del __traceback_info__
0223
0224            for name in unused:
0225                validator = self.fields[name]
0226                try:
0227                    new[name] = validator.from_python(None, state)
0228                except Invalid, e:
0229                    errors[name] = e
0230
0231            if errors:
0232                raise Invalid(
0233                    format_compound_error(errors),
0234                    value_dict, state,
0235                    error_dict=errors)
0236
0237            pre = self.pre_validators[:]
0238            pre.reverse()
0239            for validator in pre:
0240                __traceback_info__ = 'for_python pre_validator %s' % validator
0241                new = validator.from_python(new, state)
0242
0243            return new
0244
0245        finally:
0246            if state is not None:
0247                state.key = previous_key
0248                state.full_dict = previous_full_dict
0249
0250
0251
0252    def add_chained_validator(self, cls, validator):
0253        if self is not None:
0254            if self.chained_validators is cls.chained_validators:
0255                self.chained_validators = cls.chained_validators[:]
0256            self.chained_validators.append(validator)
0257        else:
0258            cls.chained_validators.append(validator)
0259
0260    add_chained_validator = declarative.classinstancemethod(
0261        add_chained_validator)
0262
0263    def add_field(self, cls, name, validator):
0264        if self is not None:
0265            if self.fields is cls.fields:
0266                self.fields = cls.fields.copy()
0267            self.fields[name] = validator
0268        else:
0269            cls.fields[name] = validator
0270
0271    add_field = declarative.classinstancemethod(add_field)
0272
0273    def add_pre_validator(self, cls, validator):
0274        if self is not None:
0275            if self.pre_validators is cls.pre_validators:
0276                self.pre_validators = cls.pre_validators[:]
0277            self.pre_validators.append(validator)
0278        else:
0279            cls.pre_validators.append(validator)
0280
0281    add_pre_validator = declarative.classinstancemethod(add_pre_validator)
0282
0283    def subvalidators(self):
0284        result = []
0285        result.extend(self.pre_validators)
0286        result.extend(self.chained_validators)
0287        result.extend(self.fields.values())
0288        return result
0289
0290def format_compound_error(v, indent=0):
0291    if isinstance(v, Exception):
0292        try:
0293            return str(v)
0294        except (UnicodeDecodeError, UnicodeEncodeError):
0295            # There doesn't seem to be a better way to get a str()
0296            # version if possible, and unicode() if necessary, because
0297            # testing for the presence of a __unicode__ method isn't
0298            # enough
0299            return unicode(v)
0300    elif isinstance(v, dict):
0301        l = v.items()
0302        l.sort()
0303        return ('%s\n' % (' '*indent)).join(
0304            ["%s: %s" % (k, format_compound_error(value, indent=len(k)+2))
0305             for k, value in l
0306             if value is not None])
0307    elif isinstance(v, list):
0308        return ('%s\n' % (' '*indent)).join(
0309            ['%s' % (format_compound_error(value, indent=indent))
0310             for value in v
0311             if value is not None])
0312    elif isinstance(v, basestring):
0313        return v
0314    else:
0315        assert 0, "I didn't expect something like %s" % repr(v)
0316
0317def merge_dicts(d1, d2):
0318    for key in d2:
0319        if key in d1:
0320            d1[key] = merge_values(d1[key], d2[key])
0321        else:
0322            d1[key] = d2[key]
0323    return d1
0324
0325def merge_values(v1, v2):
0326    if (isinstance(v1, (str, unicode))
0327        and isinstance(v2, (str, unicode))):
0328        return v1 + '\n' + v2
0329    elif (isinstance(v1, (list, tuple))
0330          and isinstance(v2, (list, tuple))):
0331        return merge_lists(v1, v2)
0332    elif isinstance(v1, dict) and isinstance(v2, dict):
0333        return merge_dicts(v1, v2)
0334    else:
0335        # @@: Should we just ignore errors?  Seems we do...
0336        return v1
0337
0338def merge_lists(l1, l2):
0339    if len(l1) < len(l2):
0340        l1 = l1 + [None]*(len(l2)-len(l1))
0341    elif len(l2) < len(l1):
0342        l2 = l2 + [None]*(len(l1)-len(l2))
0343    result = []
0344    for l1item, l2item in zip(l1, l2):
0345        item = None
0346        if l1item is None:
0347            item = l2item
0348        elif l2item is None:
0349            item = l1item
0350        else:
0351            item = merge_values(l1item, l2item)
0352        result.append(item)
0353    return result

Top