Skip to content

Commit

Permalink
Created calling/raises matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
perfa committed Jan 25, 2013
1 parent 246d24b commit 8e71212
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 0 deletions.
6 changes: 6 additions & 0 deletions doc/object_matchers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,9 @@ same_instance
^^^^^^^^^^^^^

.. automodule:: hamcrest.core.core.issame

calling, raises
^^^^^^^^^^^^^^^

.. automodule:: hamcrest.core.core.raises

3 changes: 3 additions & 0 deletions doc/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ PyHamcrest comes with a library of useful matchers:
:py:func:`~hamcrest.core.core.isnone.not_none` - match ``None``, or not
``None``
* :py:func:`~hamcrest.core.core.issame.same_instance` - match same object
* :py:func:`~hamcrest.core.core.raises.calling`,
:py:func:`~hamcrest.core.core.raises.raises` - wrap a method call and assert
that it raises an exception

* Number

Expand Down
1 change: 1 addition & 0 deletions hamcrest/core/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from isnone import none, not_none
from isnot import is_not
from issame import same_instance
from raises import calling, raises

__author__ = "Jon Reid"
__copyright__ = "Copyright 2011 hamcrest.org"
Expand Down
95 changes: 95 additions & 0 deletions hamcrest/core/core/raises.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import re
from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.helpers.wrap_matcher import wrap_matcher

__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

def _matches(self, function):
if not callable(function):
return False

try:
function()
except Exception as err:
self.actual = err

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 %s' % self.expected)

def describe_mismatch(self, item, description):
if not callable(item):
description.append_text('%s is not callable' % 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_('q'), raises(TypeError))
assert_that(calling(parse, broken_input), raises(ValueError))
"""
return Raises(exception, pattern)


class DelayedCall(object):
def __init__(self, func, *args, **kwargs):
self.func = func
self.args = args
self.kwargs = kwargs

def __call__(self):
self.func(*self.args, **self.kwargs)

def with_(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
return self


def calling(func, *args, **kwargs):
"""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
:param \*args: The positional arguments to pass to the function or method
:param \*\*kwargs: The keyword arguments to pass to the function or method
The arguments can either be provided directly along with the function or you can call
the `with_` function on the returned object as a for of syntactic sugar::
calling(my_method).with_(arguments, and_='keywords')
"""
return DelayedCall(func, *args, **kwargs)
104 changes: 104 additions & 0 deletions hamcrest_unit_test/core/raises_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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, "Bananarama"),
calling(raise_exception))

def testMatchesRegularExpressionToStringifiedException(self):
self.assert_matches('Regex',
raises(AssertionError, "(3, 1, 4)"),
calling(raise_exception).with_(3,1,4))

self.assert_matches('Regex',
raises(AssertionError, "([\d, ]+)"),
calling(raise_exception).with_(3,1,4))


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 testCallingObjectPassesArgsAndKwargs(self):
method = Callable()
calling(method, "Banana", 3, keyword1="happy")()

self.assertEqual(method.args, ("Banana", 3))
self.assertEqual(method.kwargs, {'keyword1': 'happy'})

def testCallingWithFunctionReturnsObject(self):
method = Callable()
callable = calling(method)
returned = callable.with_(3, 1, 4, keyword2="bronze")

self.assertEqual(returned, callable)

def testCallingWithFunctionSetsArgumentList(self):
method = Callable()
calling(method).with_(3, 1, 4, keyword2="bronze")()

self.assertEqual(method.args, (3, 1, 4))
self.assertEqual(method.kwargs, {'keyword2': 'bronze'})


class Callable(object):
def __init__(self):
self.called = False

def __call__(self, *args, **kwargs):
self.called = True
self.args = args
self.kwargs = kwargs

3 comments on commit 8e71212

@dexterous
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has this been merged into master? Is it going to make the next release?
I'd personally love to see this matcher. I've been hand rolling these (with tests!) every time I need them.

@perfa
Copy link
Contributor Author

@perfa perfa commented on 8e71212 Apr 24, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has not been merged yet. It needs to be rewritten slightly, to use weakref as mentioned in the merge request. I'll try to get some time to do that before/during this weekend, and hopefully it will be considered for the next release.

@dexterous
Copy link

@dexterous dexterous commented on 8e71212 Apr 25, 2013 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.