diff --git a/formencode/api.py b/formencode/api.py index 7b2bb6f4..cbf1f446 100644 --- a/formencode/api.py +++ b/formencode/api.py @@ -195,6 +195,7 @@ class Validator(declarative.Declarative): if_missing = NoDefault repeating = False compound = False + accept_iterator = False gettextargs = {} use_builtins_gettext = True # In case you don't want to use __builtins__._ # although it may be defined, set this to False diff --git a/formencode/compound.py b/formencode/compound.py index 2890d54a..04dfea24 100644 --- a/formencode/compound.py +++ b/formencode/compound.py @@ -22,6 +22,7 @@ def from_python(validator, value, state): class CompoundValidator(FancyValidator): if_invalid = NoDefault + accept_iterator = False validators = [] @@ -105,6 +106,13 @@ def is_empty(self, value): # sub-validators should handle emptiness. return False + def accept_iterator__get(self): + accept_iterator = False + for validator in self.validators: + accept_iterator = accept_iterator or getattr(validator, 'accept_iterator', False) + return accept_iterator + accept_iterator = property(accept_iterator__get) + class All(CompoundValidator): """ @@ -201,6 +209,13 @@ def is_empty(self, value): # sub-validators should handle emptiness. return False + def accept_iterator__get(self): + accept_iterator = True + for validator in self.validators: + accept_iterator = accept_iterator and getattr(validator, 'accept_iterator', False) + return accept_iterator + accept_iterator = property(accept_iterator__get) + class Pipe(All): """ diff --git a/formencode/foreach.py b/formencode/foreach.py index af59a4f0..3740748d 100644 --- a/formencode/foreach.py +++ b/formencode/foreach.py @@ -49,6 +49,7 @@ class ForEach(CompoundValidator): convert_to_list = True if_empty = NoDefault + accept_iterator = True repeating = True _if_missing = () diff --git a/formencode/schema.py b/formencode/schema.py index 39729378..46e07993 100644 --- a/formencode/schema.py +++ b/formencode/schema.py @@ -65,7 +65,7 @@ class MySubSchema(MySchema): compound = True fields = {} order = [] - fields_accept_list_values = set() + accept_iterator = True messages = dict( notExpected=_('The input field %(name)s was not expected.'), @@ -102,7 +102,6 @@ def __classinit__(cls, new_attrs): elif key in cls.fields: del cls.fields[key] - cls.fields_accept_list_values = set() for name, value in cls.fields.items(): cls.add_field(name, value) @@ -121,10 +120,6 @@ def __initargs__(self, new_attrs): # from a superclass: elif key in self.fields: del self.fields[key] - - self.fields_accept_list_values = set() - for name, value in self.fields.items(): - self.add_field(name, value) def assert_dict(self, value, state): """ @@ -169,25 +164,11 @@ def _to_python(self, value_dict, state): new[name] = value continue validator = self.fields[name] - - # Some data types require an extra check to be performed - # in order to guarantee unambiguous connection between - # input and output data. - # Let's say we have to validate two different URLS: - # 1. "site.com/?username=['John', 'Mike']" and - # 2. "site.com/?username=John&username=Mike" - # Without the following check, - # formencode.validators.String (and all other inherited validators) - # will set the username value to "['John', 'Mike']" for both URLs. - # In terms of URL design, it is considered bad if the same - # resource is accessible by two different URLs. - # Therefore, if you really want to validate a field with multiple - # values, you have to set self.allow_extra_fields to True or - # wrap the username validator with formencode.validators.ForEach() - if isinstance(value, (list, tuple)): - if name not in self.fields_accept_list_values: - raise Invalid(self.message('singleValueExpected', state), - value_dict, state) + + # are iterators (list, tuple, set, etc) allowed? + if self._value_is_iterator(value) and not getattr(validator, 'accept_iterator', False): + errors[name] = Invalid(self.message('singleValueExpected', state), + value_dict, state) if state is not None: state.key = name @@ -331,71 +312,14 @@ def add_chained_validator(self, cls, validator): add_chained_validator = declarative.classinstancemethod( add_chained_validator) - def if_validator_accepts_list(self, cls, validator, type_check): - these_accept_list = (foreach.ForEach, Set) - we_need_to_go_deeper = (compound.All, compound.Pipe) - - if type_check(validator, these_accept_list): - return True - - elif type_check(validator, compound.Any): - # Any() validator is valid if at least one of its sub-validators is valid - for sub_validator in validator.validators: - if type_check(sub_validator, these_accept_list): - return True - elif type_check(sub_validator, we_need_to_go_deeper): - return cls.if_validator_accepts_list(sub_validator, type_check) - return False - - elif type_check(validator, we_need_to_go_deeper): - # We have to check sub-sub-...-validators - sub_validators = validator.validators - if sub_validators: - # We need to check only the first validator. - # All() evaluates its sub-validators in reverse order. - significant_validator = type_check(validator, compound.All) and sub_validators[-1] or sub_validators[0] - if type_check(significant_validator, these_accept_list): - return True - elif type_check(significant_validator, we_need_to_go_deeper): - return cls.if_validator_accepts_list(significant_validator, type_check) - return False - return False - - elif type_check(validator, declarative.DeclarativeMeta): - # Cases like - # class SiteForm(Schema): - # class addresses(foreach.ForEach): - # class schema(Schema): - # name = Name() - # email = validators.Email() - # - # require a subclass-based check to be performed instead of instance-based. - return cls.if_validator_accepts_list(validator, issubclass) - - return False - if_validator_accepts_list = declarative.classinstancemethod(if_validator_accepts_list) - - def add_field(self, cls, name, validator): if self is not None: if self.fields is cls.fields: self.fields = cls.fields.copy() self.fields[name] = validator - if not self.allow_extra_fields: - accept_list = self.if_validator_accepts_list(validator, isinstance) - if accept_list: - self.fields_accept_list_values.add(name) - else: - self.fields_accept_list_values.add(name) else: cls.fields[name] = validator - if not cls.allow_extra_fields: - accept_list = cls.if_validator_accepts_list(validator, isinstance) - if accept_list: - cls.fields_accept_list_values.add(name) - else: - cls.fields_accept_list_values.add(name) add_field = declarative.classinstancemethod(add_field) @@ -423,6 +347,20 @@ def is_empty(self, value): def empty_value(self, value): return {} + def _value_is_iterator(self, value): + if isinstance(value, (str, unicode)): + return False + elif isinstance(value, (list, tuple)): + return True + + try: + for n in value: + break + return True + ## @@: Should this catch any other errors?: + except TypeError: + return False + def format_compound_error(v, indent=0): if isinstance(v, Exception): diff --git a/formencode/validators.py b/formencode/validators.py index 4f4c6ed9..75573a6c 100644 --- a/formencode/validators.py +++ b/formencode/validators.py @@ -1191,6 +1191,7 @@ class Set(FancyValidator): use_set = False if_missing = () + accept_iterator = True def _to_python(self, value, state): if self.use_set: