Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions pilo/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def field1(self, value):
import decimal
import imp
import inspect
import numbers
import re
import time
import uuid
Expand Down Expand Up @@ -79,6 +80,29 @@ def field1(self, value):
]


class FormError(ValueError):

def __init__(self, *field_errors):
self.field_errors = field_errors
super(FormError, self).__init__(self.message)

def __str__(self):
return self.message

def __repr__(self):
return 'FormError({0})'.format(
', '.join(exc.__class__.__name__ for exc in self.field_errors)
)

@property
def message(self):
msg = '\n* '.join(
'{0}: {1}'.format(exc.__class__.__name__, str(exc))
for exc in self.field_errors
)
return '\n* {0}\n'.format(msg)


class FieldError(ValueError):

def __init__(self, message, field):
Expand Down Expand Up @@ -118,9 +142,12 @@ def invalid(self, violation):

class RaiseErrors(list, Errors):

def __call__(self, *ex):
self.extend(ex)
raise ex[0]
def __call__(self, *excs):
self.extend(excs)
field_errors = [e for e in self if isinstance(e, FieldError)]
if field_errors:
raise FormError(*field_errors)
raise self[0]


class CollectErrors(list, Errors):
Expand Down Expand Up @@ -821,7 +848,7 @@ class Decimal(Number):
def _parse(self, path):
if isinstance(path.value, decimal.Decimal):
return path.value
if isinstance(path.value, float):
if isinstance(path.value, numbers.Number):
return decimal.Decimal(path.value)
value = path.primitive(basestring)
return decimal.Decimal(value)
Expand Down Expand Up @@ -1541,7 +1568,7 @@ def __init__(self, *args, **kwargs):
if src:
errors = self.map(src)
if errors:
raise errors[0]
RaiseErrors()(*errors)

def _map_source(self, obj):
return DefaultSource(obj)
Expand Down Expand Up @@ -1638,7 +1665,7 @@ def map(self, src=None, tags=None, reset=False, unmapped='ignore', error='collec
if error == 'collect':
return errors
if errors:
raise errors[0]
RaiseErrors()(*errors)
return self

def has(self, field):
Expand Down
150 changes: 150 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re

import pilo
from pilo.fields import Integer, List, String, SubForm

from tests import TestCase

Expand Down Expand Up @@ -295,6 +296,15 @@ class MyForm(pilo.Form):

MyForm(src)

def test_decimal_integer(self):
class MyForm(pilo.Form):
amt = pilo.fields.Decimal()

try:
MyForm(dict(amt=0))
except pilo.fields.Invalid:
self.fail("Failed to parse integer decimal value")


class TestFormPolymorphism(TestCase):

Expand Down Expand Up @@ -374,3 +384,143 @@ def send_to_zoo(self):
]:
obj = Animal.type.cast(desc)(desc)
self.assertIsInstance(obj, cls)


class TestFormExceptions(TestCase):

def test_exceptions(self):

class DatingProfile(pilo.Form):

genders = ['male', 'female', 'neutral']

name = String()
email = String()
postal_code = String(length=5)
blurb = String(max_length=100)
gender = String(choices=genders)
preferences = List(String(choices=genders))
likes = List(String())

profile_with_one_error = dict(
name='William Henry Cavendish III',
email='whc@example.org',
postal_code='9021', # Invalid length
blurb='I am a test fixture',
gender='male',
preferences=['female', 'neutral'],
likes=['croquet', 'muesli', 'ruses', 'umbrellas', 'wenches'],
)
profile_with_two_errors = dict(
name='William Henry Cavendish III',
email='whc@example.org',
postal_code='9021', # Invalid postal code
blurb='I am a test fixture',
gender='male',
preferences=['female', 'neutral'],
# Likes parameter missing
)
profile_with_three_errors = dict(
name='William Henry Cavendish III',
email='whc@example.org',
postal_code='9021', # Invalid length
blurb='I am a test fixture',
gender='male',
preferences=['alien'], # Invalid preference
# likes is missing
)

with self.assertRaises(pilo.fields.FormError) as ctx:
DatingProfile(profile_with_one_error)

self.assertEquals(
ctx.exception.message,
'\n'
'* Invalid: postal_code - "9021" must have length >= 5'
'\n'
)
with self.assertRaises(pilo.fields.FormError) as ctx:
DatingProfile(profile_with_two_errors)

self.assertEquals(
ctx.exception.message,
'\n'
'* Invalid: postal_code - "9021" must have length >= 5'
'\n'
'* Missing: likes - missing'
'\n'
)
with self.assertRaises(pilo.fields.FormError) as ctx:
DatingProfile(profile_with_three_errors)

self.assertEquals(
ctx.exception.message,
'\n'
'* Invalid: postal_code - "9021" must have length >= 5'
'\n'
'* Invalid: preferences[0] - "alien" is not one of "male", '
'"female", "neutral"'
'\n'
'* Missing: likes - missing'
'\n'
)

def test_exceptions_in_nested_forms(self):

class DatingProfile(pilo.Form):

genders = ['male', 'female', 'neutral']

name = String()
email = String()
postal_code = String(length=5)
blurb = String(max_length=100)
gender = String(choices=genders)
preferences = List(String(choices=genders))
likes = List(String())

class Matches(pilo.Form):

similarity = Integer()
candidates = List(SubForm(DatingProfile))

with self.assertRaises(pilo.fields.FormError) as ctx:
Matches(
similarity="Not an integer",
candidates=[
dict(
name='William Henry Cavendish III',
email='whc@example.org',
postal_code='9021', # Invalid postal code
blurb='I am a test fixture',
gender='male',
preferences=['female', 'neutral'],
# Likes parameter missing
),
dict(),
]
)
self.assertEquals(
ctx.exception.message,
'\n'
'* Invalid: similarity - "Not an integer" is not an integer'
'\n'
'* Invalid: candidates[0].postal_code - "9021" must have length >= 5'
'\n'
'* Missing: candidates[0].likes - missing'
'\n'
'* Missing: candidates[1].name - missing'
'\n'
'* Missing: candidates[1].email - missing'
'\n'
'* Missing: candidates[1].postal_code - missing'
'\n'
'* Missing: candidates[1].blurb - missing'
'\n'
'* Missing: candidates[1].gender - missing'
'\n'
'* Missing: candidates[1].preferences - missing'
'\n'
'* Missing: candidates[1].likes - missing'
'\n'
)