Skip to content

Commit

Permalink
Add allow_iter() and allow_each() to serve as common base classes for
Browse files Browse the repository at this point in the history
all other allowances.  This is the first step toward cleaning up the
arguments and message handling for all allowance context managers.
  • Loading branch information
shawnbrown committed Jul 23, 2016
1 parent 7b8eaa6 commit f8bca62
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 1 deletion.
81 changes: 81 additions & 0 deletions datatest/allow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
from math import isnan
from .utils.builtins import *
from .utils import functools
from .utils import itertools

from .differences import _make_decimal
Expand Down Expand Up @@ -31,6 +33,85 @@ def _walk_diff(diff):
yield item


class allow_iter(object):
"""Context manager to allow differences without triggering a test
failure. *function* should accept an iterable of differences and
return an iterable of only those differences which are not allowed.
.. note::
:class:`allow_iter` is the base context manager on which all
other allowences are implemented.
"""
def __init__(self, function, msg=None, **kwds):
assert callable(function), 'must be function or other callable'
self.function = function
self.msg = msg
self.kwds = kwds

def __enter__(self):
return self

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('Allowed differences not 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:
exc = DataError(self.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.

@staticmethod
def _partition_kwds(differences, **kwds):
"""Takes an iterable of *differences* and keyword filters,
returns a 2-tuple of lists containing *nonmatches* and
*matches* differences.
"""
if not kwds:
return ([], differences) # <- EXIT!

# Normalize values.
for k, v in kwds.items():
if isinstance(v, str):
kwds[k] = (v,)
filter_items = tuple(kwds.items())

# Make predicate and partition into "rejected" and "accepted".
def predicate(obj):
for k, v in filter_items: # Closes over filter_items.
if (k not in obj.kwds) or (obj.kwds[k] not in v):
return False
return True
t1, t2 = itertools.tee(differences)
return itertools.filterfalse(predicate, t1), filter(predicate, t2)


class allow_each(allow_iter):
def __init__(self, function, msg=None, **kwds):
@functools.wraps(function)
def filterfalse(iterable): # Returns elements where function evals to False.
return (x for x in iterable if not function(x))
super(allow_each, self).__init__(filterfalse, msg, **kwds)


class _BaseAllowance(object):
"""Base class for DataTestCase.allow...() context managers."""
def __init__(self, test_case, msg=None):
Expand Down
137 changes: 136 additions & 1 deletion tests/test_allow.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# -*- coding: utf-8 -*-
from . import _unittest as unittest
from datatest import Missing

from datatest import DataError
from datatest import Missing
from datatest import Extra
from datatest.allow import allow_iter
from datatest.allow import allow_each
from datatest.allow import _walk_diff

# NOTE!!!: Currently, the allowance context managers are being tested
Expand All @@ -10,6 +14,137 @@
# these classes should be moved out of test_case and into this
# sub-module.

class TestAllowIter(unittest.TestCase):
def test_function_all_bad(self):
function = lambda iterable: iterable # <- Rejects everything.
in_diffs = [
Extra('foo'),
Extra('bar'),
]
with self.assertRaises(DataError) as cm:
with allow_iter(function, 'example allowance'):
raise DataError('example error', in_diffs)

rejected = cm.exception.differences
self.assertEqual(rejected, in_diffs)

def test_function_all_ok(self):
function = lambda iterable: list() # <- Accepts everything.

with allow_iter(function, 'example allowance'):
raise DataError('example error', [Missing('foo'), Missing('bar')])

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')])

def test_kwds_all_bad(self):
function = lambda iterable: list() # <- Accepts everything.
in_diffs = [
Missing('foo', aaa='x', bbb='y'),
Missing('bar', aaa='x', bbb='z'),
]
with self.assertRaises(DataError) as cm:
# Using keyword bbb='j' should reject all in_diffs.
with allow_iter(function, 'example allowance', bbb='j'):
raise DataError('example error', in_diffs)

rejected = list(cm.exception.differences)
self.assertEqual(rejected, 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 test_kwds_some_ok(self):
function = lambda iterable: list() # <- Accepts everything.
in_diffs = [
Missing('foo', aaa='x', bbb='y'),
Missing('bar', aaa='x', bbb='z'),
]
with self.assertRaises(DataError) as cm:
# Keyword bbb='y' should reject second in_diffs element.
with allow_iter(function, 'example allowance', bbb='y'):
raise DataError('example error', in_diffs)

rejected = list(cm.exception.differences)
self.assertEqual(rejected, [Missing('bar', aaa='x', bbb='z')])

def test_no_exception(self):
function = lambda iterable: list() # <- Accepts everything.

with self.assertRaises(AssertionError) as cm:
with allow_iter(function):
pass # No exceptions raised

exc = cm.exception
self.assertEqual('Allowed differences not found: <lambda>', str(exc))


class TestAllowEach(unittest.TestCase):
"""Using allow_each() requires an element-wise function."""
def test_allow_some(self):
function = lambda x: x.value == 'bar'
in_diffs = [
Missing('foo'),
Missing('bar'),
]
with self.assertRaises(DataError) as cm:
with allow_each(function, 'example allowance'):
raise DataError('example error', in_diffs)

rejected = list(cm.exception.differences)
self.assertEqual(rejected, [Missing('foo')])

def test_allow_all(self):
function = lambda x: isinstance(x, Missing) # <- Allow only missing.

with allow_each(function, 'example allowance'):
raise DataError('example error', [Missing('xxx'), Missing('yyy')])

def test_kwds(self):
function = lambda x: True # <- Accepts everything.
in_diffs = [
Missing('foo', aaa='x', bbb='y'),
Missing('bar', aaa='x', bbb='z'),
]
with self.assertRaises(DataError) as cm:
# Keyword bbb='y' should reject second in_diffs element.
with allow_each(function, 'example allowance', bbb='y'):
raise DataError('example error', in_diffs)

rejected = list(cm.exception.differences)
self.assertEqual(rejected, [Missing('bar', aaa='x', bbb='z')])

def test_no_exception(self):
function = lambda x: False # <- Rejects everything.

with self.assertRaises(AssertionError) as cm:
with allow_each(function):
pass # No exceptions raised

exc = cm.exception
self.assertEqual('Allowed differences not found: <lambda>', str(exc))


class TestWalkValues(unittest.TestCase):
def test_list_input(self):
Expand Down

0 comments on commit f8bca62

Please sign in to comment.