def test_duplicates(self): # Three of the exact-same differences. differences = [Extra('xxx'), Extra('xxx'), Extra('xxx')] # Only allow one of them. with self.assertRaises(DataError) as cm: allowed = [Extra('xxx')] with allow_only(allowed): raise DataError('example error', differences) expected = [Extra('xxx'), Extra('xxx')] # Expect two remaining. actual = list(cm.exception.differences) self.assertEqual(expected, actual) # Only allow two of them. with self.assertRaises(DataError) as cm: allowed = [Extra('xxx'), Extra('xxx')] with allow_only(allowed): raise DataError('example error', differences) expected = [Extra('xxx')] # Expect one remaining. actual = list(cm.exception.differences) self.assertEqual(expected, actual) # Allow all three. allowed = [Extra('xxx'), Extra('xxx'), Extra('xxx')] with allow_only(allowed): raise DataError('example error', differences)
def test_keys_mapping(self): # Function accepts single argument. in_diffs = {'AAA': Missing('foo'), 'BBB': Missing('bar')} function = lambda x: x == 'AAA' with self.assertRaises(DataError) as cm: with allow_any(keys=function): raise DataError('example error', in_diffs) rejected = cm.exception.differences self.assertEqual(rejected, {'BBB': Missing('bar')}) # Function accepts multiple arguments. in_diffs = { ('AAA', 'XXX'): Missing('foo'), ('BBB', 'YYY'): Missing('bar') } def function(first, second): # <- Multiple args. return second == 'XXX' with self.assertRaises(DataError) as cm: with allow_any(keys=function): raise DataError('example error', in_diffs) rejected = cm.exception.differences self.assertEqual(rejected, {('BBB', 'YYY'): Missing('bar')})
def test_returns_bad_sequence(self): """In place of mapping objects, *function* may instead return an iterable of two-item sequences but if the sequence contains more or less items, a ValueError should be raised. """ in_diffs = { ('AAA', 'xxx'): Missing('foo'), ('BBB', 'yyy'): Missing('bar') } # mapping / iterable of 1-item sequences. return_val = [ [Missing('foo')], # <- One item. [Missing('bar')] ] # <- One item. regex = ('has length 1.*2 is required') with self.assertRaisesRegex(ValueError, regex): # <- ValueError! with allow_iter(lambda x: return_val): raise DataError('example error', in_diffs) # mapping / iterable of 3-item sequences. return_val = [ [('AAA', 'xxx'), Missing('foo'), None], # <- Three items. [('BBB', 'yyy'), Missing('bar'), None] ] # <- Three items. regex = 'has length 3.*2 is required' with self.assertRaisesRegex(ValueError, regex): # <- ValueError! with allow_iter(lambda x: return_val): raise DataError('example error', in_diffs)
def test_allow_all(self): differences = [Missing('xxx'), Extra('yyy')] with allow_only(differences): raise DataError('example error', [Missing('xxx'), Extra('yyy')]) # Order of differences should not matter! differences = [Extra('yyy'), Missing('xxx')] with allow_only(differences): raise DataError('example error', reversed(differences))
def test_returns_sequence(self): """If given a mapping, *function* may return an iterable of sequences (instead of a mapping). Each sequence must contain exactly two objects---the first object will be use as the key and the second object will be used as the value (same as Python's 'dict' behavior). """ # Simple key. in_diffs = {'AAA': Missing('foo'), 'BBB': Missing('bar')} return_val = [ ['AAA', Missing('foo')], # <- Two items. ['BBB', Missing('bar')] ] # <- Two items. with self.assertRaises(DataError) as cm: with allow_iter(lambda x: return_val): raise DataError('example error', in_diffs) out_diffs = cm.exception.differences self.assertEqual(out_diffs, in_diffs) # Compound key. in_diffs = { ('AAA', 'xxx'): Missing('foo'), ('BBB', 'yyy'): Missing('bar') } return_val = [ [('AAA', 'xxx'), Missing('foo')], # <- Two items. [('BBB', 'yyy'), Missing('bar')] ] # <- Two items. with self.assertRaises(DataError) as cm: with allow_iter(lambda x: return_val): raise DataError('example error', in_diffs) out_diffs = cm.exception.differences self.assertEqual(out_diffs, in_diffs) # Duplicate keys--last defined value (bar) appears in mapping. in_diffs = {'AAA': Missing('foo'), 'BBB': Missing('bar')} return_val = [ ['AAA', Missing('foo')], ['BBB', Missing('xxx')], # <- Duplicate key. ['BBB', Missing('yyy')], # <- Duplicate key. ['BBB', Missing('zzz')], # <- Duplicate key. ['BBB', Missing('bar')] ] # <- Duplicate key. with self.assertRaises(DataError) as cm: with allow_iter(lambda x: return_val): raise DataError('example error', in_diffs) out_diffs = cm.exception.differences self.assertEqual(out_diffs, in_diffs)
def test_kwds_all_ok(self): function = lambda iterable: list() # <- Accepts everything. in_diffs = [ Missing('foo', aaa='x', bbb='y'), Missing('bar', aaa='x', bbb='z'), ] # Using keyword aaa='x' should accept all in_diffs. with allow_iter(function, 'example allowance', aaa='x'): raise DataError('example error', in_diffs) # Using keyword bbb=['y', 'z'] should also accept all in_diffs. with allow_iter(function, 'example allowance', bbb=['y', 'z']): raise DataError('example error', in_diffs)
def __exit__(self, exc_type, exc_value, tb): if exc_type is None: # <- Values are None when no exeption was raised. if self.msg: msg = self.msg else: msg = getattr(self.function, '__name__', str(self.function)) exc = AssertionError('No differences found: ' + str(msg)) exc.__cause__ = None raise exc if not issubclass(exc_type, DataError): raise exc_value # If not DataError, re-raise without changes. diffs = exc_value.differences rejected_kwds, accepted_kwds = self._partition_kwds(diffs, **self.kwds) rejected_func = self.function(accepted_kwds) # <- Apply function! not_allowed = itertools.chain(rejected_kwds, rejected_func) not_allowed = list(not_allowed) if not_allowed: msg = [self.msg, getattr(exc_value, 'msg')] msg = ': '.join(x for x in msg if x) exc = DataError(msg, not_allowed) exc.__cause__ = None # <- Suppress context using verbose raise exc # alternative to support older Python # versions--see PEP 415 (same as # effect as "raise ... from None"). return True # <- Suppress original exception.
def test_allow_one_but_find_duplicate(self): with self.assertRaises(DataError) as cm: with allow_only(Extra('xxx')): raise DataError('example error', [Extra('xxx'), Extra('xxx')]) result_string = str(cm.exception) self.assertEqual("example error:\n Extra('xxx')", result_string)
def test_allow_duplicate_but_find_only_one(self): with self.assertRaises(DataError) as cm: with allow_only([Extra('xxx'), Extra('xxx')]): raise DataError('example error', [Extra('xxx')]) result_string = str(cm.exception) self.assertEqual("Allowed difference not found:\n Extra('xxx')", result_string)
def test_dict_of_diffs_exceeds(self): differences = {'foo': Extra('xxx'), 'bar': Missing('yyy')} with self.assertRaises(DataError) as cm: with allow_limit(1): # <- Allows only 1 but there are 2! raise DataError('example error', differences) rejected = cm.exception.differences self.assertEqual(differences, rejected)
def function(iterable): allowed = self._walk_diff( differences) # <- Closes over *differences*. allowed = collections.Counter(allowed) not_allowed = [] for x in iterable: if allowed[x]: allowed[x] -= 1 else: not_allowed.append(x) if not_allowed: return not_allowed # <- EXIT! not_found = list(allowed.elements()) if not_found: exc = DataError('Allowed difference not found', not_found) exc.__cause__ = None raise exc return iter([])
def test_allow_some(self): differences = [Extra('xxx'), Missing('yyy')] with self.assertRaises(DataError) as cm: with allow_limit(1): # <- Allows only 1 but there are 2! raise DataError('example error', differences) rejected = list(cm.exception.differences) self.assertEqual(differences, rejected)
def test_allow_some(self): differences = [Extra('xxx'), Missing('yyy')] with self.assertRaises(DataError) as cm: with allow_missing(): raise DataError('example error', differences) rejected = list(cm.exception.differences) self.assertEqual(rejected, [Extra('xxx')])
def test_empty_generator(self): """If all items are valid, returning an empty generator or other iterable non-container should work, too. """ function = lambda iterable: (x for x in []) # <- Empty generator. in_diffs = [Missing('foo'), Missing('bar')] with allow_iter(function): # <- Passes without error. raise DataError('example error', in_diffs)
def test_keys_nonmapping(self): # Missing required keyword 'diffs'. in_diffs = [Missing('foo'), Missing('bar')] function = lambda first, second: first == 'AAA' regex = "accepts only 'diffs' keyword, found 'keys'" with self.assertRaisesRegex(ValueError, regex): with allow_any(keys=function): # <- expects 'diffs='. raise DataError('example error', in_diffs) # Disallowed keywords ('keys'). in_diffs = [Missing('foo'), Missing('bar')] function = lambda first, second: first == 'AAA' with self.assertRaisesRegex(ValueError, "found 'keys'"): with allow_any(diffs=function, keys=function): # <- 'keys=' not allowed. raise DataError('example error', in_diffs)
def test_diffs_nonmapping(self): in_diffs = [Missing('foo'), Missing('bar')] function = lambda x: x.value == 'foo' with self.assertRaises(DataError) as cm: with allow_any(diffs=function): # <- Using diffs keyword! raise DataError('example error', in_diffs) rejected = cm.exception.differences self.assertEqual(rejected, [Missing('bar')])
def test_diffs_mapping(self): in_diffs = {'AAA': Missing('foo'), 'BBB': Missing('bar')} function = lambda x: x.value == 'foo' with self.assertRaises(DataError) as cm: with allow_any(diffs=function): raise DataError('example error', in_diffs) rejected = cm.exception.differences self.assertEqual(rejected, {'BBB': Missing('bar')})
def test_not_found(self): with self.assertRaises(DataError) as cm: with allow_only([Extra('xxx'), Missing('yyy')]): raise DataError('example error', [Extra('xxx')]) result_str = str(cm.exception) self.assertTrue(result_str.startswith('Allowed difference not found')) result_diffs = list(cm.exception.differences) self.assertEqual([Missing('yyy')], result_diffs)
def test_iterable_all_good(self): """Given a non-mapping iterable in which all items are valid, *function* should omit all items and simply return an empty iterable. """ function = lambda iterable: list() # <- Accepts everything. in_diffs = [Missing('foo'), Missing('bar')] with allow_iter(function): # <- Passes without error. raise DataError('example error', in_diffs)
def test_items_mapping(self): # Function of one argument. in_diffs = {'AAA': Missing('foo'), 'BBB': Missing('bar')} def function(item): key, diff = item # Unpack item tuple. return key == 'AAA' with self.assertRaises(DataError) as cm: with allow_any(items=function): raise DataError('example error', in_diffs) rejected = cm.exception.differences self.assertEqual(rejected, {'BBB': Missing('bar')}) # Function of two arguments. in_diffs = {'AAA': Missing('foo'), 'BBB': Missing('bar')} def function(key, diff): return key == 'AAA' with self.assertRaises(DataError) as cm: with allow_any(items=function): raise DataError('example error', in_diffs) rejected = cm.exception.differences self.assertEqual(rejected, {'BBB': Missing('bar')}) # Function of three arguments. in_diffs = { ('AAA', 'XXX'): Missing('foo'), ('BBB', 'YYY'): Missing('bar') } def function(key1, key2, diff): return key2 == 'XXX' with self.assertRaises(DataError) as cm: with allow_any(items=function): raise DataError('example error', in_diffs) rejected = cm.exception.differences self.assertEqual(rejected, {('BBB', 'YYY'): Missing('bar')})
def test_allow_some(self): with self.assertRaises(DataError) as cm: with allow_only(Extra('xxx'), 'example allowance'): raise DataError('example error', [Extra('xxx'), Missing('yyy')]) result_str = str(cm.exception) self.assertEqual("example allowance: example error:\n Missing('yyy')", result_str) result_diffs = list(cm.exception.differences) self.assertEqual([Missing('yyy')], result_diffs)
def test_dict_of_diffs_kwds_func_under_limit(self): differences = {'foo': Extra('xxx'), 'bar': Missing('yyy')} with self.assertRaises(DataError) as cm: is_extra = lambda x: isinstance(x, Extra) with allow_limit(2, diffs=is_extra): raise DataError('example error', differences) rejected = cm.exception.differences self.assertEqual({'bar': Missing('yyy')}, rejected)
def test_mapping_none_allowed(self): differences = {'foo': Extra('xxx'), 'bar': Missing('yyy')} allowed = {} with self.assertRaises(DataError) as cm: with allow_only(allowed): raise DataError('example error', differences) actual = cm.exception.differences self.assertEqual(differences, actual)
def test_kwds_under_limit(self): differences = [Extra('xxx'), Missing('yyy'), Extra('zzz')] with self.assertRaises(DataError) as cm: is_extra = lambda x: isinstance(x, Extra) with allow_limit(4, diffs=is_extra): # <- Limit of 4 and is_extra(). raise DataError('example error', differences) rejected = list(cm.exception.differences) self.assertEqual([Missing('yyy')], rejected)
def test_kwds(self): diff_set = set([ Missing('xxx', aaa='foo'), Missing('yyy', aaa='bar'), Extra('zzz', aaa='foo'), ]) with self.assertRaises(DataError) as cm: # Allows 2 with aaa='foo' and there are two (only aaa='bar' is rejected). with allow_limit(2, 'example allowance', aaa='foo'): raise DataError('example error', diff_set) rejected = set(cm.exception.differences) self.assertEqual(rejected, set([Missing('yyy', aaa='bar')])) with self.assertRaises(DataError) as cm: # Allows 1 with aaa='foo' but there are 2 (all are rejected)! with allow_limit(1, 'example allowance', aaa='foo'): raise DataError('example error', diff_set) rejected = set(cm.exception.differences) self.assertEqual(rejected, diff_set)
def test_some_allowed(self): differences = [Extra('xxx'), Missing('yyy')] allowed = [Extra('xxx')] with self.assertRaises(DataError) as cm: with allow_only(allowed): raise DataError('example error', differences) expected = [Missing('yyy')] actual = list(cm.exception.differences) self.assertEqual(expected, actual)
def test_single_diff_without_container(self): differences = [Extra('xxx'), Missing('yyy')] allowed = Extra('xxx') # <- Single diff, not in list. with self.assertRaises(DataError) as cm: with allow_only(allowed): raise DataError('example error', differences) expected = [Missing('yyy')] actual = list(cm.exception.differences) self.assertEqual(expected, actual)
def test_mapping_mismatched_types(self): # Dict of diffs vs list of allowed. differences = {'foo': Extra('xxx'), 'bar': Missing('yyy')} allowed = [Extra('xxx'), Missing('yyy')] regex = ("expects non-mapping differences but found 'dict' of " "differences") with self.assertRaisesRegex(ValueError, regex): with allow_only(allowed): raise DataError('example error', differences) # List of diffs vs dict of allowed. differences = [Extra('xxx'), Missing('yyy')] allowed = {'foo': Extra('xxx'), 'bar': Missing('yyy')} regex = ("expects mapping of differences but found 'list' of " "differences") with self.assertRaisesRegex(ValueError, regex): with allow_only(allowed): raise DataError('example error', differences)
def test_kwds(self): in_diffs = [ Missing('xxx', aaa='foo'), Missing('yyy', aaa='bar'), Extra('zzz', aaa='foo'), ] with self.assertRaises(DataError) as cm: with allow_missing('example allowance', aaa='foo'): raise DataError('example error', in_diffs) rejected = list(cm.exception.differences) self.assertEqual(rejected, [Missing('yyy', aaa='bar'), Extra('zzz', aaa='foo')])
def test_function_some_ok(self): function = lambda iterable: (x for x in iterable if x.value != 'bar') in_diffs = [ Missing('foo'), Missing('bar'), ] with self.assertRaises(DataError) as cm: with allow_iter(function, 'example allowance'): raise DataError('example error', in_diffs) rejected = list(cm.exception.differences) self.assertEqual(rejected, [Missing('foo')])