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

Proposed by Jonathan Lange on 2012-01-28
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 2012-01-28 Approve on 2012-01-28
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 on 2012-01-28

NEWS update

240. By Jonathan Lange on 2012-01-28

Format properly

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.

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 on 2012-01-29

Extract Nullary and move it into helpers.

242. By Jonathan Lange on 2012-01-29

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

243. By Jonathan Lange on 2012-01-29

In the NEWS

244. By Jonathan Lange on 2012-01-29

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