Merge lp:~lifeless/testtools/haslength into lp:~testtools-committers/testtools/trunk

Proposed by Robert Collins
Status: Merged
Merged at revision: 312
Proposed branch: lp:~lifeless/testtools/haslength
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 305 lines (+178/-1)
7 files modified
NEWS (+6/-0)
doc/for-test-authors.rst (+37/-0)
testtools/matchers/__init__.py (+4/-0)
testtools/matchers/_basic.py (+12/-1)
testtools/matchers/_higherorder.py (+76/-0)
testtools/tests/matchers/test_basic.py (+16/-0)
testtools/tests/matchers/test_higherorder.py (+27/-0)
To merge this branch: bzr merge lp:~lifeless/testtools/haslength
Reviewer Review Type Date Requested Status
Vincent Ladeuil Approve
testtools committers Pending
Review via email: mp+144578@code.launchpad.net

Description of the change

I keep wanting a HasLength. And MatchesWithPredicate can't do it well. So, new helper to do it well, and an implementation.

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :

Nice, I was searching for the moral equivalent of bzr's assertLength and couldn't find it last week ;)

25 +HasLength
26 +~~~~~~~~~
27 +
28 +Check the length of a collection. For example::
29 +
30 + self.assertThat([1, 2, 3], HasLength(2))

I can see this assertion will fail but stating so in the text would be
clearer IMHO. The other examples in this file I've looked at are of the
form: "Matches if <...>: <example>"

160 + HasLength = MatchesPredicate(
161 + lambda x, y: len(x) == y, 'len({0}) is not {1}')
162 + self.assertThat([1, 2], HasLength(3))

Same here ?

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

Thanks for the review Vincent, I'll try to get back and improve the docs.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2013-01-21 19:37:00 +0000
3+++ NEWS 2013-01-23 20:10:27 +0000
4@@ -9,6 +9,12 @@
5 Improvements
6 ------------
7
8+* New matcher ``HasLength`` for matching the length of a collection.
9+ (Robert Collins)
10+
11+* New matcher ``MatchesPredicateWithParams`` make it still easier to create
12+ adhoc matchers. (Robert Collins)
13+
14 * We have a simpler release process in future - see doc/hacking.rst.
15 (Robert Collins)
16
17
18=== modified file 'doc/for-test-authors.rst'
19--- doc/for-test-authors.rst 2013-01-18 09:17:19 +0000
20+++ doc/for-test-authors.rst 2013-01-23 20:10:27 +0000
21@@ -521,6 +521,14 @@
22 self.assertThat('greetings.txt', FileContains(matcher=Contains('!')))
23
24
25+HasLength
26+~~~~~~~~~
27+
28+Check the length of a collection. For example::
29+
30+ self.assertThat([1, 2, 3], HasLength(2))
31+
32+
33 HasPermissions
34 ~~~~~~~~~~~~~~
35
36@@ -780,6 +788,35 @@
37 MismatchError: 42 is not prime.
38
39
40+MatchesPredicateWithParams
41+~~~~~~~~~~~~~~~~~~~~~~~~~~
42+
43+Sometimes you can't use a trivial predicate and instead need to pass in some
44+parameters each time. In that case, MatchesPredicateWithParams is your go-to
45+tool for creating adhoc matchers. MatchesPredicateWithParams takes a predicate
46+function and message and returns a factory to produce matchers from that. The
47+predicate needs to return a boolean (or any truthy object), and accept the
48+object to match + whatever was passed into the factory.
49+
50+For example, you might have an ``divisible`` function and want to make a
51+matcher based on it::
52+
53+ def test_divisible_numbers(self):
54+ IsDisivibleBy = MatchesPredicateWithParams(
55+ divisible, '{0} is not divisible by {1}')
56+ self.assertThat(7, IsDivisibleBy(1))
57+ self.assertThat(7, IsDivisibleBy(7))
58+ self.assertThat(7, IsDivisibleBy(2)))
59+ # This will fail.
60+
61+Which will produce the error message::
62+
63+ Traceback (most recent call last):
64+ File "...", line ..., in test_divisible
65+ self.assertThat(7, IsDivisibleBy(2))
66+ MismatchError: 7 is not divisible by 2.
67+
68+
69 Raises
70 ~~~~~~
71
72
73=== modified file 'testtools/matchers/__init__.py'
74--- testtools/matchers/__init__.py 2012-10-25 14:20:44 +0000
75+++ testtools/matchers/__init__.py 2013-01-23 20:10:27 +0000
76@@ -28,6 +28,7 @@
77 'FileContains',
78 'FileExists',
79 'GreaterThan',
80+ 'HasLength',
81 'HasPermissions',
82 'Is',
83 'IsInstance',
84@@ -39,6 +40,7 @@
85 'MatchesException',
86 'MatchesListwise',
87 'MatchesPredicate',
88+ 'MatchesPredicateWithParams',
89 'MatchesRegex',
90 'MatchesSetwise',
91 'MatchesStructure',
92@@ -57,6 +59,7 @@
93 EndsWith,
94 Equals,
95 GreaterThan,
96+ HasLength,
97 Is,
98 IsInstance,
99 LessThan,
100@@ -101,6 +104,7 @@
101 MatchesAll,
102 MatchesAny,
103 MatchesPredicate,
104+ MatchesPredicateWithParams,
105 Not,
106 )
107
108
109=== modified file 'testtools/matchers/_basic.py'
110--- testtools/matchers/_basic.py 2012-09-10 11:37:46 +0000
111+++ testtools/matchers/_basic.py 2013-01-23 20:10:27 +0000
112@@ -5,6 +5,7 @@
113 'EndsWith',
114 'Equals',
115 'GreaterThan',
116+ 'HasLength',
117 'Is',
118 'IsInstance',
119 'LessThan',
120@@ -24,7 +25,10 @@
121 text_repr,
122 )
123 from ..helpers import list_subtract
124-from ._higherorder import PostfixedMismatch
125+from ._higherorder import (
126+ MatchesPredicateWithParams,
127+ PostfixedMismatch,
128+ )
129 from ._impl import (
130 Matcher,
131 Mismatch,
132@@ -313,3 +317,10 @@
133 pattern = pattern.encode("unicode_escape").decode("ascii")
134 return Mismatch("%r does not match /%s/" % (
135 value, pattern.replace("\\\\", "\\")))
136+
137+
138+def has_len(x, y):
139+ return len(x) == y
140+
141+
142+HasLength = MatchesPredicateWithParams(has_len, "len({0}) != {1}", "HasLength")
143
144=== modified file 'testtools/matchers/_higherorder.py'
145--- testtools/matchers/_higherorder.py 2012-12-13 15:01:41 +0000
146+++ testtools/matchers/_higherorder.py 2013-01-23 20:10:27 +0000
147@@ -287,3 +287,79 @@
148 def match(self, x):
149 if not self.predicate(x):
150 return Mismatch(self.message % x)
151+
152+
153+def MatchesPredicateWithParams(predicate, message, name=None):
154+ """Match if a given parameterised function returns True.
155+
156+ It is reasonably common to want to make a very simple matcher based on a
157+ function that you already have that returns True or False given some
158+ arguments. This matcher makes it very easy to do so. e.g.::
159+
160+ HasLength = MatchesPredicate(
161+ lambda x, y: len(x) == y, 'len({0}) is not {1}')
162+ self.assertThat([1, 2], HasLength(3))
163+
164+ Note that unlike MatchesPredicate MatchesPredicateWithParams returns a
165+ factory which you then customise to use by constructing an actual matcher
166+ from it.
167+
168+ The predicate function should take the object to match as its first
169+ parameter. Any additional parameters supplied when constructing a matcher
170+ are supplied to the predicate as additional parameters when checking for a
171+ match.
172+
173+ :param predicate: The predicate function.
174+ :param message: A format string for describing mis-matches.
175+ :param name: Optional replacement name for the matcher.
176+ """
177+ def construct_matcher(*args, **kwargs):
178+ return _MatchesPredicateWithParams(
179+ predicate, message, name, *args, **kwargs)
180+ return construct_matcher
181+
182+
183+class _MatchesPredicateWithParams(Matcher):
184+
185+ def __init__(self, predicate, message, name, *args, **kwargs):
186+ """Create a ``MatchesPredicateWithParams`` matcher.
187+
188+ :param predicate: A function that takes an object to match and
189+ additional params as given in *args and **kwargs. The result of the
190+ function will be interpreted as a boolean to determine a match.
191+ :param message: A message to describe a mismatch. It will be formatted
192+ with .format() and be given a tuple containing whatever was passed
193+ to ``match()`` + *args in *args, and whatever was passed to
194+ **kwargs as its **kwargs.
195+
196+ For instance, to format a single parameter::
197+
198+ "{0} is not a {1}"
199+
200+ To format a keyword arg::
201+
202+ "{0} is not a {type_to_check}"
203+ :param name: What name to use for the matcher class. Pass None to use
204+ the default.
205+ """
206+ self.predicate = predicate
207+ self.message = message
208+ self.name = name
209+ self.args = args
210+ self.kwargs = kwargs
211+
212+ def __str__(self):
213+ args = [str(arg) for arg in self.args]
214+ kwargs = ["%s=%s" % item for item in self.kwargs.items()]
215+ args = ", ".join(args + kwargs)
216+ if self.name is None:
217+ name = 'MatchesPredicateWithParams(%r, %r)' % (
218+ self.predicate, self.message)
219+ else:
220+ name = self.name
221+ return '%s(%s)' % (name, args)
222+
223+ def match(self, x):
224+ if not self.predicate(x, *self.args, **self.kwargs):
225+ return Mismatch(
226+ self.message.format(*((x,) + self.args), **self.kwargs))
227
228=== modified file 'testtools/tests/matchers/test_basic.py'
229--- testtools/tests/matchers/test_basic.py 2012-09-08 17:21:06 +0000
230+++ testtools/tests/matchers/test_basic.py 2013-01-23 20:10:27 +0000
231@@ -19,6 +19,7 @@
232 IsInstance,
233 LessThan,
234 GreaterThan,
235+ HasLength,
236 MatchesRegex,
237 NotEquals,
238 SameMembers,
239@@ -369,6 +370,21 @@
240 ]
241
242
243+class TestHasLength(TestCase, TestMatchersInterface):
244+
245+ matches_matcher = HasLength(2)
246+ matches_matches = [[1, 2]]
247+ matches_mismatches = [[], [1], [3, 2, 1]]
248+
249+ str_examples = [
250+ ("HasLength(2)", HasLength(2)),
251+ ]
252+
253+ describe_examples = [
254+ ("len([]) != 1", [], HasLength(1)),
255+ ]
256+
257+
258 def test_suite():
259 from unittest import TestLoader
260 return TestLoader().loadTestsFromName(__name__)
261
262=== modified file 'testtools/tests/matchers/test_higherorder.py'
263--- testtools/tests/matchers/test_higherorder.py 2012-12-13 15:01:41 +0000
264+++ testtools/tests/matchers/test_higherorder.py 2013-01-23 20:10:27 +0000
265@@ -18,6 +18,7 @@
266 MatchesAny,
267 MatchesAll,
268 MatchesPredicate,
269+ MatchesPredicateWithParams,
270 Not,
271 )
272 from testtools.tests.helpers import FullStackRunTest
273@@ -222,6 +223,32 @@
274 ]
275
276
277+def between(x, low, high):
278+ return low < x < high
279+
280+
281+class TestMatchesPredicateWithParams(TestCase, TestMatchersInterface):
282+
283+ matches_matcher = MatchesPredicateWithParams(
284+ between, "{0} is not between {1} and {2}")(1, 9)
285+ matches_matches = [2, 4, 6, 8]
286+ matches_mismatches = [0, 1, 9, 10]
287+
288+ str_examples = [
289+ ("MatchesPredicateWithParams(%r, %r)(%s)" % (
290+ between, "{0} is not between {1} and {2}", "1, 2"),
291+ MatchesPredicateWithParams(
292+ between, "{0} is not between {1} and {2}")(1, 2)),
293+ ("Between(1, 2)", MatchesPredicateWithParams(
294+ between, "{0} is not between {1} and {2}", "Between")(1, 2)),
295+ ]
296+
297+ describe_examples = [
298+ ('1 is not between 2 and 3', 1, MatchesPredicateWithParams(
299+ between, "{0} is not between {1} and {2}")(2, 3)),
300+ ]
301+
302+
303 def test_suite():
304 from unittest import TestLoader
305 return TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches