-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'perfa/feature_raises_matcher'
Resolves #30
- Loading branch information
Showing
6 changed files
with
256 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
__author__ = "Per Fagrell" | ||
__copyright__ = "Copyright 2013 hamcrest.org" | ||
__license__ = "BSD, see License.txt" | ||
|
||
__all__ = ['is_callable'] | ||
|
||
import sys | ||
|
||
# callable was not part of py3k until 3.2, so we create this | ||
# generic is_callable to use callable if possible, otherwise | ||
# we use generic homebrew. | ||
if sys.version_info[0] == 3 and sys.version_info[1] < 2: | ||
def is_callable(function): | ||
"""Return whether the object is callable (i.e., some kind of function).""" | ||
if function is None: | ||
return False | ||
return any("__call__" in klass.__dict__ for klass in type(function).__mro__) | ||
else: | ||
is_callable = callable |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
from weakref import ref | ||
import re | ||
import sys | ||
from hamcrest.core.base_matcher import BaseMatcher | ||
from hamcrest.core.helpers.wrap_matcher import wrap_matcher | ||
from hamcrest.core.compat import is_callable | ||
|
||
__author__ = "Per Fagrell" | ||
__copyright__ = "Copyright 2013 hamcrest.org" | ||
__license__ = "BSD, see License.txt" | ||
|
||
|
||
class Raises(BaseMatcher): | ||
def __init__(self, expected, pattern=None): | ||
self.pattern = pattern | ||
self.expected = expected | ||
self.actual = None | ||
self.function = None | ||
|
||
def _matches(self, function): | ||
if not is_callable(function): | ||
return False | ||
|
||
self.function = ref(function) | ||
return self._call_function(function) | ||
|
||
def _call_function(self, function): | ||
self.actual = None | ||
try: | ||
function() | ||
except Exception: | ||
self.actual = sys.exc_info()[1] | ||
|
||
if isinstance(self.actual, self.expected): | ||
if self.pattern is not None: | ||
return re.search(self.pattern, str(self.actual)) is not None | ||
return True | ||
return False | ||
|
||
def describe_to(self, description): | ||
description.append_text('Expected a callable raising %s' % self.expected) | ||
|
||
def describe_mismatch(self, item, description): | ||
if not is_callable(item): | ||
description.append_text('%s is not callable' % item) | ||
return | ||
|
||
function = None if self.function is None else self.function() | ||
if function is None or function is not item: | ||
self.function = ref(item) | ||
if not self._call_function(item): | ||
return | ||
|
||
if self.actual is None: | ||
description.append_text('No exception raised.') | ||
elif isinstance(self.actual, self.expected) and self.pattern is not None: | ||
description.append_text('Correct assertion type raised, but the expected pattern ("%s") not found.' % self.pattern) | ||
description.append_text('\n message was: "%s"' % str(self.actual)) | ||
else: | ||
description.append_text('%s was raised instead' % type(self.actual)) | ||
|
||
|
||
def raises(exception, pattern=None): | ||
"""Matches if the called function raised the expected exception. | ||
:param exception: The class of the expected exception | ||
:param pattern: Optional regular expression to match exception message. | ||
Expects the actual to be wrapped by using :py:func:`~hamcrest.core.core.raises.calling`, | ||
or a callable taking no arguments. | ||
Optional argument pattern should be a string containing a regular expression. If provided, | ||
the string representation of the actual exception - e.g. `str(actual)` - must match pattern. | ||
Examples:: | ||
assert_that(calling(int).with_args('q'), raises(TypeError)) | ||
assert_that(calling(parse, broken_input), raises(ValueError)) | ||
""" | ||
return Raises(exception, pattern) | ||
|
||
|
||
class DeferredCallable(object): | ||
def __init__(self, func): | ||
self.func = func | ||
self.args = tuple() | ||
self.kwargs = {} | ||
|
||
def __call__(self): | ||
self.func(*self.args, **self.kwargs) | ||
|
||
def with_args(self, *args, **kwargs): | ||
self.args = args | ||
self.kwargs = kwargs | ||
return self | ||
|
||
|
||
def calling(func): | ||
"""Wrapper for function call that delays the actual execution so that | ||
:py:func:`~hamcrest.core.core.raises.raises` matcher can catch any thrown exception. | ||
:param func: The function or method to be called | ||
The arguments can be provided with a call to the `with_args` function on the returned | ||
object:: | ||
calling(my_method).with_args(arguments, and_='keywords') | ||
""" | ||
return DeferredCallable(func) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
if __name__ == '__main__': | ||
import sys | ||
sys.path.insert(0, '..') | ||
sys.path.insert(0, '../..') | ||
|
||
from hamcrest.core.core.raises import * | ||
|
||
from hamcrest.core.core.isequal import equal_to | ||
from hamcrest_unit_test.matcher_test import MatcherTest | ||
import unittest | ||
|
||
__author__ = "Per Fagrell" | ||
__copyright__ = "Copyright 2013 hamcrest.org" | ||
__license__ = "BSD, see License.txt" | ||
|
||
|
||
def no_exception(*args, **kwargs): | ||
return | ||
|
||
|
||
def raise_exception(*args, **kwargs): | ||
raise AssertionError(str(args) + str(kwargs)) | ||
|
||
|
||
class RaisesTest(MatcherTest): | ||
def testMatchesIfFunctionRaisesTheExactExceptionExpected(self): | ||
self.assert_matches('Right exception', | ||
raises(AssertionError), | ||
calling(raise_exception)) | ||
|
||
def testDoesNotMatchTypeErrorIfActualIsNotCallable(self): | ||
self.assert_does_not_match('Not callable', | ||
raises(TypeError), | ||
23) | ||
|
||
def testMatchesIfFunctionRaisesASubclassOfTheExpectedException(self): | ||
self.assert_matches('Subclassed Exception', | ||
raises(Exception), | ||
calling(raise_exception)) | ||
|
||
def testDoesNotMatchIfFunctionDoesNotRaiseException(self): | ||
self.assert_does_not_match('No exception', | ||
raises(ValueError), | ||
calling(no_exception)) | ||
|
||
def testDoesNotMatchExceptionIfRegularExpressionDoesNotMatch(self): | ||
self.assert_does_not_match('Bad regex', | ||
raises(AssertionError, "Phrase not found"), | ||
calling(raise_exception)) | ||
|
||
def testMatchesRegularExpressionToStringifiedException(self): | ||
self.assert_matches('Regex', | ||
raises(AssertionError, "(3, 1, 4)"), | ||
calling(raise_exception).with_args(3,1,4)) | ||
|
||
self.assert_matches('Regex', | ||
raises(AssertionError, "([\d, ]+)"), | ||
calling(raise_exception).with_args(3,1,4)) | ||
|
||
def testDescribeMismatchWillCallItemIfNotTheOriginalMatch(self): | ||
function = Callable() | ||
matcher = raises(AssertionError) | ||
matcher.describe_mismatch(function, object()) | ||
self.assertTrue(function.called) | ||
|
||
class CallingTest(unittest.TestCase): | ||
def testCallingDoesNotImmediatelyExecuteFunction(self): | ||
try: | ||
calling(raise_exception) | ||
except AssertionError: | ||
self.fail() | ||
else: | ||
pass | ||
|
||
def testCallingObjectCallsProvidedFunction(self): | ||
method = Callable() | ||
calling(method)() | ||
self.assertTrue(method.called) | ||
|
||
def testCallingWithFunctionReturnsObject(self): | ||
method = Callable() | ||
callable = calling(method) | ||
returned = callable.with_args(3, 1, 4, keyword1="arg1") | ||
|
||
self.assertEqual(returned, callable) | ||
|
||
def testCallingWithFunctionSetsArgumentList(self): | ||
method = Callable() | ||
calling(method).with_args(3, 1, 4, keyword1="arg1")() | ||
|
||
self.assertEqual(method.args, (3, 1, 4)) | ||
self.assertEqual(method.kwargs, {"keyword1": "arg1"}) | ||
|
||
|
||
class Callable(object): | ||
def __init__(self): | ||
self.called = False | ||
|
||
def __call__(self, *args, **kwargs): | ||
self.called = True | ||
self.args = args | ||
self.kwargs = kwargs |