Permalink
Browse files

the error messages are about 100 billion times better

  • Loading branch information...
1 parent 73211b1 commit 77dd1bd5daa16f4ead12b2ca8e8d4f5ebca504a1 ehuber committed Mar 8, 2012
Showing with 127 additions and 56 deletions.
  1. +1 −5 TODO
  2. +69 −21 contract.py
  3. +57 −30 tests.py
View
6 TODO
@@ -1,10 +1,6 @@
wish bucket:
. split earley parser out
. check_value() should probably produce a magical function
- that someone dissociates itself from the references to the
+ that somehow dissociates itself from the references to the
parse tree
. grep for 'horrible'
-
-more schemas that ought to work:
- . int -> int? # takes an int, will return either an int or None
- . lists
View
@@ -1,11 +1,18 @@
+import termcolor
import re
+def red(s):
+ return termcolor.colored(s, 'red')
+
class InvalidContract(Exception):
pass
class AmbiguousContract(Exception):
pass
+class InternalFailedContract(Exception):
+ pass
+
class FailedContract(Exception):
pass
@@ -127,7 +134,7 @@ def earley(s):
chart[i] = new_states
else:
break
- # return the last column after we pretty it up a bit.
+ # return the last column of states after we pretty it up a bit.
complete = filter(lambda state: state['begin'] == 0 and not state['uncompleted_rhs'] and state['lhs'] == root, chart[-1])
def pretty(state):
if 'term' in state:
@@ -139,16 +146,16 @@ def pretty(state):
def check_value(schema, value):
# fun
if rule_matcher(schema, 'fun', 'fixed_tup', t_arrow, 'typ'):
+ expected_contract = '%s->%s' % (schema['rhs'][0]['span'], schema['rhs'][2]['span'])
if type(value).__name__ == 'function':
if getattr(value, '__contract__', None) is not None:
# this is so horrible, i am a horrible
- expected_contract = '%s->%s' % (schema['rhs'][0]['span'], schema['rhs'][2]['span'])
if value.__contract__ != expected_contract:
- raise FailedContract('the contract is %s, we wanted %s' % (value.__contract__, expected_contract))
+ raise InternalFailedContract(expected_contract, red(value.__contract__))
else:
- raise FailedContract('expected a contract-wrapped method')
+ raise InvalidContract('expected a contract-wrapped method') #maybe??????
else:
- raise FailedContract('expected method, got %s' % type(value).__name__)
+ raise InternalFailedContract(expected_contract, red(type(value).__name__))
# t
elif rule_matcher(schema, 't', 'fixed_tup'):
@@ -162,7 +169,7 @@ def check_value(schema, value):
elif rule_matcher(schema, 't', t_type):
expect_type = schema['rhs'][0]['token']
if expect_type != type(value).__name__:
- raise FailedContract('expected type %s, got type %s' % (expect_type, type(value).__name__))
+ raise InternalFailedContract(expect_type, red(type(value).__name__))
elif rule_matcher(schema, 't', t_lparen, 'typ', t_rparen):
check_value(schema['rhs'][1], value)
elif rule_matcher(schema, 't', 'fun'):
@@ -179,31 +186,53 @@ def check_value(schema, value):
elif rule_matcher(schema, 'list', t_lbrack, 'typ', t_rbrack):
if type(value) == list:
for v in value:
- check_value(schema['rhs'][1], v)
+ try:
+ check_value(schema['rhs'][1], v)
+ except InternalFailedContract, e:
+ # unlike tuples, shortcircuit, because lists are supposed to be homogenous.
+ raise InternalFailedContract(schema['span'], '[..' + red(e.args[1]) + '..]')
else:
- raise FailedContract('expected a list, got a %s' % type(value).__name__)
+ raise InternalFailedContract(schema['span'], red(type(value).__name__))
# set
elif rule_matcher(schema, 'set', t_lbrace, 'typ', t_rbrace):
if type(value) == set:
for v in value:
- check_value(schema['rhs'][1], v)
+ try:
+ check_value(schema['rhs'][1], v)
+ except InternalFailedContract, e:
+ # just like for lists, we shortcircuit, because sets are homogenous.
+ raise InternalFailedContract(schema['span'], '{..' + red(e.args[1]) + '..}')
else:
- raise FailedContract('expected a set, got a %s' % type(value).__name__)
+ raise InternalFailedContract(schema['span'], red(type(value).__name__))
# dict
elif rule_matcher(schema, 'dict', 'typ', t_colon, 'typ'):
if type(value) == dict:
for k, v in value.iteritems():
- check_value(schema['rhs'][0], k)
- check_value(schema['rhs'][2], v)
+ is_okay = True
+ key_span = schema['rhs'][0]['span']
+ value_span = schema['rhs'][2]['span']
+ try:
+ check_value(schema['rhs'][0], k)
+ except InternalFailedContract, e:
+ # " " " dicts are homogenous.
+ key_span = red(e.args[1])
+ is_okay = False
+ try:
+ check_value(schema['rhs'][2], v)
+ except InternalFailedContract, e:
+ value_span = red(e.args[1])
+ is_okay = False
+ if not is_okay:
+ raise InternalFailedContract(schema['span'], '{..' + key_span + ':' + value_span + '..}')
else:
- raise FailedContract('expected a dict, got a %s' % type(value).__name__)
+ raise InternalFailedContract(schema['span'], red(type(value).__name__))
# fixed_tup, more_fixed_tup
elif rule_matcher(schema, 'fixed_tup', t_lparen, t_comma, t_rparen):
if value != ():
- raise FailedContract('expected the empty tuple, got %s' % value)
+ raise InternalFailedContract(schema['span'], type(value).__name__)
elif rule_matcher(schema, 'fixed_tup', t_lparen, 'typ', t_comma, 'more_fixed_tup', t_rparen):
expected = [schema['rhs'][1]]
p = schema['rhs'][3]
@@ -216,11 +245,22 @@ def check_value(schema, value):
else:
raise InternalContractError('the parsetree is fucked right here')
if type(value) != tuple:
- raise FailedContract('expected a %s-ple, got a %s' % (len(expected), type(value).__name__))
+ raise InternalFailedContract(schema['span'], type(value).__name__)
if len(value) != len(expected):
- raise FailedContract('expected a %s-ple, got a %s-ple' % (len(expected), len(value)))
- [check_value(e, v) for e, v in zip(expected, value)]
-
+ raise InternalFailedContract(schema['span'], '(' + ('_,' * len(value)) + ')')
+ # complicated error reporting follows
+ matches = True
+ discovered = []
+ for e, v in zip(expected, value):
+ try:
+ check_value(e, v)
+ except InternalFailedContract, e:
+ discovered.append(e.args[1])
+ matches = False
+ else:
+ discovered.append(e['span'])
+ if not matches:
+ raise InternalFailedContract(schema['span'], '(' + ','.join(discovered) + ',)')
# we're fucked!
else:
import pprint
@@ -242,10 +282,18 @@ def contract(s, debug=False):
def wrapped(f):
def inner(*args):
# check the input..
- check_value(parse['rhs'][0], args)
- # check the output..
+ try:
+ check_value(parse['rhs'][0], args)
+ except InternalFailedContract, e:
+ raise FailedContract('expected input is %s, but got %s' % (e.args[0], e.args[1]))
+ # Now check the output. We do input and output checking
+ # separately, because we don't want to run the inner
+ # method with input which we know is wrong.
output = f(*args)
- check_value(parse['rhs'][2], output)
+ try:
+ check_value(parse['rhs'][2], output)
+ except InternalFailedContract, e:
+ raise FailedContract('expected output is %s, but got %s' % (e.args[0], e.args[1]))
# if it got this far, we're good.
return output
# this is horrible, there needs to be some enforcement of a canonical form for this to really work
View
@@ -1,7 +1,14 @@
-from contract import contract, InvalidContract, FailedContract
+from contract import contract, InvalidContract, FailedContract, red
import unittest
-class TestContracts(unittest.TestCase):
+class BetterTestCase(unittest.TestCase):
+
+ def assertRaisesString(self, e_class, e_message, f, *args, **kwargs):
+ with self.assertRaises(e_class) as cm:
+ f(*args, **kwargs)
+ self.assertEqual(cm.exception.message, e_message)
+
+class TestContracts(BetterTestCase):
def test_str_to_str(self):
@contract('(str,) -> str')
@@ -10,7 +17,7 @@ def exclaim(s):
# this is ok
self.assertEqual(exclaim('hello'), 'hello!')
# this is not ok
- self.assertRaisesRegexp(FailedContract, r'^expected type str, got type int$', exclaim, 5)
+ self.assertRaisesString(FailedContract, 'expected input is (str,), but got (%s,)' % red('int'), exclaim, 5)
def test_str_to_str_to_str(self):
@contract('(str,) -> (str,) -> str')
@@ -22,8 +29,8 @@ def wrapper(s2):
# this is ok
self.assertEqual(prepender('hello, ')('dave'), 'hello, dave')
# these are not ok
- self.assertRaisesRegexp(FailedContract, r'^expected type str, got type int$', prepender, 5)
- self.assertRaisesRegexp(FailedContract, r'^expected type str, got type int$', prepender('hello, '), 5)
+ self.assertRaisesString(FailedContract, 'expected input is (str,), but got (%s,)' % red('int'), prepender, 5)
+ self.assertRaisesString(FailedContract, 'expected input is (str,), but got (%s,)' % red('int'), prepender('hello, '), 5)
def test_argle_bargle(self):
@contract('((str,) -> str,) -> str')
@@ -35,9 +42,12 @@ def joy_joy(s):
# this is ok
self.assertEqual(i_give_you_happy(joy_joy), 'happy happy joy joy')
# these are not ok
- self.assertRaisesRegexp(FailedContract, r'^expected a 1-ple, got a 0-ple$', i_give_you_happy) # TODO: this could be a more useful message.
- self.assertRaisesRegexp(FailedContract, r'^expected method, got str$', i_give_you_happy, 'joy joy')
- self.assertRaisesRegexp(FailedContract, r'^expected a contract-wrapped method$', i_give_you_happy, lambda s: s)
+ self.assertRaisesString(FailedContract, 'expected input is ((str,)->str,), but got ()', i_give_you_happy)
+ self.assertRaisesString(FailedContract, 'expected input is ((str,)->str,), but got (%s,)' % red('str'), i_give_you_happy, 'joy joy')
+ # I opine that this contract was never valid in the first
+ # place. This is the only case that this exception should ever
+ # be raised after a contract has been parsed.
+ self.assertRaisesString(InvalidContract, 'expected a contract-wrapped method', i_give_you_happy, lambda s: s)
def test_unit(self):
@contract('(,) -> int')
@@ -46,8 +56,8 @@ def f():
# this is ok
self.assertEqual(f(), 42)
# these are not ok
- self.assertRaisesRegexp(FailedContract, r'^expected the empty tuple, got \(\)$', f, ()) # TODO, heh
- self.assertRaisesRegexp(FailedContract, r'^expected the empty tuple, got \(5,\)$', f, (5,))
+ self.assertRaisesString(FailedContract, 'expected input is (,), but got tuple', f, ()) # TODO, heh
+ self.assertRaisesString(FailedContract, 'expected input is (,), but got tuple', f, (5,))
def test_class(self):
class C(object):
@@ -58,10 +68,10 @@ def f(c):
# this is ok
self.assertEqual(f(C()), 42)
# these are not ok
- self.assertRaisesRegexp(FailedContract, r'^expected type C, got type type$', f, C)
- self.assertRaisesRegexp(FailedContract, r'^expected type C, got type int$', f, 42)
- self.assertRaisesRegexp(FailedContract, r'^expected type C, got type type$', f, object)
- self.assertRaisesRegexp(FailedContract, r'^expected type C, got type object$', f, object())
+ self.assertRaisesString(FailedContract, 'expected input is (C,), but got (%s,)' % red('type'), f, C)
+ self.assertRaisesString(FailedContract, 'expected input is (C,), but got (%s,)' % red('int'), f, 42)
+ self.assertRaisesString(FailedContract, 'expected input is (C,), but got (%s,)' % red('type'), f, object)
+ self.assertRaisesString(FailedContract, 'expected input is (C,), but got (%s,)' % red('object'), f, object())
def test_nested_unit(self):
@contract('(((,),),) -> str')
@@ -70,8 +80,8 @@ def f(unit_unit):
# this is ok
self.assertEqual(f(((),)), 'hello')
# these are not ok
- self.assertRaisesRegexp(FailedContract, r'^expected a 1-ple, got a 0-ple$', f, ())
- self.assertRaisesRegexp(FailedContract, r'^expected a 1-ple, got a str$', f, ('hi'))
+ self.assertRaisesString(FailedContract, 'expected input is (((,),),), but got ((),)', f, ())
+ self.assertRaisesString(FailedContract, 'expected input is (((,),),), but got (str,)', f, ('hi'))
def test_list_of_int(self):
@contract('([int],) -> str')
@@ -81,9 +91,9 @@ def f(l):
self.assertEqual(f([1, 2, 3]), '1,2,3')
self.assertEqual(f([]), '')
# these are not ok
- self.assertRaisesRegexp(FailedContract, r'^expected a list, got a NoneType$', f, None)
- self.assertRaisesRegexp(FailedContract, r'^expected type int, got type str$', f, ['hi'])
- self.assertRaisesRegexp(FailedContract, r'^expected type int, got type str$', f, [42, 'hi'])
+ self.assertRaisesString(FailedContract, 'expected input is ([int],), but got (%s,)' % red('NoneType'), f, None)
+ self.assertRaisesString(FailedContract, 'expected input is ([int],), but got ([..%s..],)' % red('str'), f, ['hi'])
+ self.assertRaisesString(FailedContract, 'expected input is ([int],), but got ([..%s..],)' % red('str'), f, [42, 'hi'])
def test_list_of_list_of_int(self):
@contract('([[int]],) -> str')
@@ -94,9 +104,9 @@ def f(l_o_l):
self.assertEqual(f([[1, 2, 3]]), '1,2,3')
self.assertEqual(f([[]]), '')
self.assertEqual(f([]), '')
- # these are not ok
- self.assertRaisesRegexp(FailedContract, r'^expected a list, got a int$', f, [1, 2, 3])
- self.assertRaisesRegexp(FailedContract, r'^expected type int, got type str$', f, [[], ['hi']])
+ # these are not ok
+ self.assertRaisesString(FailedContract, 'expected input is ([[int]],), but got ([..%s..],)' % red('int'), f, [1, 2, 3])
+ self.assertRaisesString(FailedContract, 'expected input is ([[int]],), but got ([..%s..],)' % red('[..str..]'), f, [[], ['hi']])
def test_set_of_int(self):
@contract('({int},) -> int')
@@ -106,8 +116,8 @@ def f(s):
self.assertEqual(f(set([1, 2, 3])), 3)
self.assertEqual(f(set()), 0)
# these are not ok
- self.assertRaisesRegexp(FailedContract, r'^expected a set, got a list$', f, [1, 2, 3])
- self.assertRaisesRegexp(FailedContract, r'^expected type int, got type str$', f, set([1, 2, 'hi']))
+ self.assertRaisesString(FailedContract, 'expected input is ({int},), but got (%s,)' % red('list'), f, [1, 2, 3])
+ self.assertRaisesString(FailedContract, 'expected input is ({int},), but got ({..%s..},)' % red('str'), f, set([1, 2, 'hi']))
def test_list_of_set_of_int(self):
@contract('([{int}],) -> [int]')
@@ -118,8 +128,9 @@ def f(l_o_s):
self.assertEqual(f([]), [])
self.assertEqual(f([set()]), [0])
# these are not ok
- self.assertRaisesRegexp(FailedContract, r'^expected a set, got a int$', f, [1, 2, 3])
- self.assertRaisesRegexp(FailedContract, r'^expected type int, got type str$', f, [set(), set(['hi'])])
+ self.assertRaisesString(FailedContract, 'expected input is ([{int}],), but got (%s,)' % red('set'), f, {1})
+ self.assertRaisesString(FailedContract, 'expected input is ([{int}],), but got ([..%s..],)' % red('int'), f, [1, 2, 3])
+ self.assertRaisesString(FailedContract, 'expected input is ([{int}],), but got ([..%s..],)' % red('{..str..}'), f, [set(), set(['hi'])])
def test_dict(self):
class C(object):
@@ -130,8 +141,11 @@ def f(m, i):
# this is ok
self.assertEqual(type(f({5: 'hi'}, 5)), C)
# this is not ok
- self.assertRaisesRegexp(FailedContract, r'^expected type str, got type int$', f, {5: 10}, 5)
- self.assertRaisesRegexp(FailedContract, r'^expected type int, got type str$', f, {'hello': 'hi'}, 5)
+ self.assertRaisesString(FailedContract, 'expected input is (int:str,int), but got ({..int:%s..},int,)' % red('int'), f, {5: 10}, 5)
+ self.assertRaisesString(FailedContract, 'expected input is (int:str,int), but got ({..%s:str..},int,)' % red('str'), f, {'hello': 'hi'}, 5)
+ self.assertRaisesString(FailedContract, 'expected input is (int:str,int), but got ({..%s:%s..},int,)' % (red('str'), red('int')), f, {'hello': 5}, 5)
+ self.assertRaisesString(FailedContract, 'expected input is (int:str,int), but got ({..%s:%s..},%s,)' % (red('str'), red('int'), red('str')), f, {'hello': 5}, 'derp')
+ self.assertRaisesString(FailedContract, 'expected input is (int:str,int), but got (int:str,%s,)' % red('str'), f, {5: 'hello'}, 'derp')
def test_nullable(self):
@contract('(int?,) -> int')
@@ -144,7 +158,7 @@ def f(i):
self.assertEqual(f(None), 5)
self.assertEqual(f(2), 4)
# not ok
- self.assertRaisesRegexp(FailedContract, r'^expected type int, got type str$', f, 'blargh')
+ self.assertRaisesString(FailedContract, 'expected input is (int?,), but got (%s,)' % red('str'), f, 'blargh')
def test_nullable_dict(self):
@contract('(int, int:(str?)) -> str')
@@ -154,7 +168,7 @@ def f(i, m):
self.assertEqual(f(5, {5: 'z'}), 'z')
self.assertEqual(f(5, {5: None}), 'bloop')
# not ok
- self.assertRaisesRegexp(FailedContract, r'^expected type int, got type NoneType$', f, 5, {None: 'aaa'})
+ self.assertRaisesString(FailedContract, 'expected input is (int,int:(str?)), but got (int,{..%s:(str?)..},)' % red('NoneType'), f, 5, {None: 'aaa'})
def test_invalid_contracts(self):
# that's just not a valid type.
@@ -168,5 +182,18 @@ def f():
def f():
pass
+ def test_awesome_error_messages(self):
+ @contract('([int],) -> str')
+ def f(l_o_i):
+ return 'aaaaaa'
+ self.assertRaisesString(FailedContract, 'expected input is ([int],), but got (%s,)' % red('int'), f, 5)
+ @contract('([int], [[str]]) -> str')
+ def f(l_o_i, l_o_l_o_i):
+ return 'aaaaaa'
+ # ehhhhh
+ #f([5, 'hehe'], [['derp', 'durp'], [4, 'lawl']])
+ #self.assertRaisesString(FailedContract, 'expected input is ([int],[[str]]), but got ([..%s..],[..%s..],)' % (red('str'), red('[..int..]')),
+ # f, [5, 'hehe'], [['derp', 'durp'], [4, 'lawl']])
+
if __name__ == '__main__':
unittest.main()

0 comments on commit 77dd1bd

Please sign in to comment.