Merge lp:~jml/testtools/assert-raises-lambda into lp:~testtools-committers/testtools/trunk

Proposed by Jonathan Lange
Status: Merged
Merged at revision: 240
Proposed branch: lp:~jml/testtools/assert-raises-lambda
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 171 lines (+93/-2)
4 files modified
NEWS (+6/-0)
doc/for-test-authors.rst (+21/-0)
testtools/testcase.py (+21/-2)
testtools/tests/test_testcase.py (+45/-0)
To merge this branch: bzr merge lp:~jml/testtools/assert-raises-lambda
Reviewer Review Type Date Requested Status
Robert Collins Approve
Review via email: mp+90574@code.launchpad.net

Description of the change

When assertRaises currently fails, it doesn't mention the callable that it was invoked with. Instead it mentions a lambda that we make up in order to be able to re-use the Raises matcher.

This patch addresses this problem by using a wrapper object instead of a lambda. The wrapper object is a nullary callable and has a custom __repr__ method that forwards to the callable that assertRaises was invoked with.

To post a comment you must log in.
239. By Jonathan Lange

NEWS update

240. By Jonathan Lange

Format properly

Revision history for this message
Jonathan Lange (jml) wrote :

Note that this is the sort of thing that's likely to trip up with cross-Python compatibility – I've only tested on Python 2.7.

We need to make sure that trunk passes on all supported Pythons before releasing.

Revision history for this message
Robert Collins (lifeless) wrote :

This will certainly work. Its a bit sad that folk using lambda's directly won't get the benefit. Perhaps the wrapper should be a public helper? [short of introspecting lambdas...

review: Approve
241. By Jonathan Lange

Extract Nullary and move it into helpers.

242. By Jonathan Lange

Move to testcase so it doesn't appear in stack traces.

243. By Jonathan Lange

In the NEWS

244. By Jonathan Lange

Document

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2012-01-10 17:59:27 +0000
3+++ NEWS 2012-01-29 14:12:23 +0000
4@@ -21,6 +21,9 @@
5 previous release promised clean stack, but now we actually provide it.
6 (Jonathan Lange, #854769)
7
8+* ``assertRaises`` now includes the ``repr`` of the callable that failed to raise
9+ properly. (Jonathan Lange, #881052)
10+
11 * Failed equality assertions now line up. (Jonathan Lange, #879339)
12
13 * ``MatchesAll`` and ``MatchesListwise`` both take a ``first_only`` keyword
14@@ -28,6 +31,9 @@
15 and not continue looking for other possible mismatches.
16 (Jonathan Lange)
17
18+* New helper, ``Nullary`` that turns callables with arguments into ones that
19+ don't take arguments. (Jonathan Lange)
20+
21 * New matchers:
22
23 * ``DirContains`` matches the contents of a directory.
24
25=== modified file 'doc/for-test-authors.rst'
26--- doc/for-test-authors.rst 2011-12-07 11:32:45 +0000
27+++ doc/for-test-authors.rst 2012-01-29 14:12:23 +0000
28@@ -1322,6 +1322,27 @@
29 particular attribute.
30
31
32+Nullary callables
33+-----------------
34+
35+Sometimes you want to be able to pass around a function with the arguments
36+already specified. The normal way of doing this in Python is::
37+
38+ nullary = lambda: f(*args, **kwargs)
39+ nullary()
40+
41+Which is mostly good enough, but loses a bit of debugging information. If you
42+take the ``repr()`` of ``nullary``, you're only told that it's a lambda, and
43+you get none of the juicy meaning that you'd get from the ``repr()`` of ``f``.
44+
45+The solution is to use ``Nullary`` instead::
46+
47+ nullary = Nullary(f, *args, **kwargs)
48+ nullary()
49+
50+Here, ``repr(nullary)`` will be the same as ``repr(f)``.
51+
52+
53 .. _testrepository: https://launchpad.net/testrepository
54 .. _Trial: http://twistedmatrix.com/documents/current/core/howto/testing.html
55 .. _nose: http://somethingaboutorange.com/mrl/projects/nose/
56
57=== modified file 'testtools/testcase.py'
58--- testtools/testcase.py 2011-12-05 15:21:33 +0000
59+++ testtools/testcase.py 2012-01-29 14:12:23 +0000
60@@ -384,8 +384,8 @@
61 capture = CaptureMatchee()
62 matcher = Raises(MatchesAll(ReRaiseOtherTypes(),
63 MatchesException(excClass), capture))
64-
65- self.assertThat(lambda: callableObj(*args, **kwargs), matcher)
66+ our_callable = Nullary(callableObj, *args, **kwargs)
67+ self.assertThat(our_callable, matcher)
68 return capture.matchee
69 failUnlessRaises = assertRaises
70
71@@ -777,6 +777,25 @@
72 return True
73
74
75+class Nullary(object):
76+ """Turn a callable into a nullary callable.
77+
78+ The advantage of this over ``lambda: f(*args, **kwargs)`` is that it
79+ preserves the ``repr()`` of ``f``.
80+ """
81+
82+ def __init__(self, callable_object, *args, **kwargs):
83+ self._callable_object = callable_object
84+ self._args = args
85+ self._kwargs = kwargs
86+
87+ def __call__(self):
88+ return self._callable_object(*self._args, **self._kwargs)
89+
90+ def __repr__(self):
91+ return repr(self._callable_object)
92+
93+
94 # Signal that this is part of the testing framework, and that code from this
95 # should not normally appear in tracebacks.
96 __unittest = True
97
98=== modified file 'testtools/tests/test_testcase.py'
99--- testtools/tests/test_testcase.py 2011-10-30 16:27:05 +0000
100+++ testtools/tests/test_testcase.py 2012-01-29 14:12:23 +0000
101@@ -25,11 +25,13 @@
102 )
103 from testtools.matchers import (
104 Annotate,
105+ Contains,
106 DocTestMatches,
107 Equals,
108 MatchesException,
109 Raises,
110 )
111+from testtools.testcase import Nullary
112 from testtools.testresult.doubles import (
113 Python26TestResult,
114 Python27TestResult,
115@@ -309,6 +311,17 @@
116 self.assertFails('<function <lambda> at ...> returned None',
117 self.assertRaises, expectedExceptions, lambda: None)
118
119+ def test_assertRaises_function_repr_in_exception(self):
120+ # When assertRaises fails, it includes the repr of the invoked
121+ # function in the error message, so it's easy to locate the problem.
122+ def foo():
123+ """An arbitrary function."""
124+ pass
125+ self.assertThat(
126+ lambda: self.assertRaises(Exception, foo),
127+ Raises(
128+ MatchesException(self.failureException, '.*%r.*' % (foo,))))
129+
130 def assertFails(self, message, function, *args, **kwargs):
131 """Assert that function raises a failure with the given message."""
132 failure = self.assertRaises(
133@@ -1283,6 +1296,38 @@
134 self.assertTrue(test.teardown_called)
135
136
137+class TestNullary(TestCase):
138+
139+ def test_repr(self):
140+ # The repr() of nullary is the same as the repr() of the wrapped
141+ # function.
142+ def foo():
143+ pass
144+ wrapped = Nullary(foo)
145+ self.assertEqual(repr(wrapped), repr(foo))
146+
147+ def test_called_with_arguments(self):
148+ # The function is called with the arguments given to Nullary's
149+ # constructor.
150+ l = []
151+ def foo(*args, **kwargs):
152+ l.append((args, kwargs))
153+ wrapped = Nullary(foo, 1, 2, a="b")
154+ wrapped()
155+ self.assertEqual(l, [((1, 2), {'a': 'b'})])
156+
157+ def test_returns_wrapped(self):
158+ # Calling Nullary returns whatever the function returns.
159+ ret = object()
160+ wrapped = Nullary(lambda: ret)
161+ self.assertIs(ret, wrapped())
162+
163+ def test_raises(self):
164+ # If the function raises, so does Nullary when called.
165+ wrapped = Nullary(lambda: 1/0)
166+ self.assertRaises(ZeroDivisionError, wrapped)
167+
168+
169 def test_suite():
170 from unittest import TestLoader
171 return TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches