Skip to content

Commit

Permalink
Make validators specify that they allow iterators
Browse files Browse the repository at this point in the history
  • Loading branch information
lambacck committed Dec 15, 2012
1 parent 5516d93 commit df66b54
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 82 deletions.
1 change: 1 addition & 0 deletions formencode/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions formencode/compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def from_python(validator, value, state):
class CompoundValidator(FancyValidator):

if_invalid = NoDefault
accept_iterator = False

validators = []

Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand Down
1 change: 1 addition & 0 deletions formencode/foreach.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class ForEach(CompoundValidator):

convert_to_list = True
if_empty = NoDefault
accept_iterator = True
repeating = True
_if_missing = ()

Expand Down
102 changes: 20 additions & 82 deletions formencode/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.'),
Expand Down Expand Up @@ -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)

Expand All @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions formencode/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit df66b54

Please sign in to comment.