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
=== modified file 'NEWS'
--- NEWS 2013-01-21 19:37:00 +0000
+++ NEWS 2013-01-23 20:10:27 +0000
@@ -9,6 +9,12 @@
9Improvements9Improvements
10------------10------------
1111
12* New matcher ``HasLength`` for matching the length of a collection.
13 (Robert Collins)
14
15* New matcher ``MatchesPredicateWithParams`` make it still easier to create
16 adhoc matchers. (Robert Collins)
17
12* We have a simpler release process in future - see doc/hacking.rst.18* We have a simpler release process in future - see doc/hacking.rst.
13 (Robert Collins)19 (Robert Collins)
1420
1521
=== modified file 'doc/for-test-authors.rst'
--- doc/for-test-authors.rst 2013-01-18 09:17:19 +0000
+++ doc/for-test-authors.rst 2013-01-23 20:10:27 +0000
@@ -521,6 +521,14 @@
521 self.assertThat('greetings.txt', FileContains(matcher=Contains('!')))521 self.assertThat('greetings.txt', FileContains(matcher=Contains('!')))
522522
523523
524HasLength
525~~~~~~~~~
526
527Check the length of a collection. For example::
528
529 self.assertThat([1, 2, 3], HasLength(2))
530
531
524HasPermissions532HasPermissions
525~~~~~~~~~~~~~~533~~~~~~~~~~~~~~
526534
@@ -780,6 +788,35 @@
780 MismatchError: 42 is not prime.788 MismatchError: 42 is not prime.
781789
782790
791MatchesPredicateWithParams
792~~~~~~~~~~~~~~~~~~~~~~~~~~
793
794Sometimes you can't use a trivial predicate and instead need to pass in some
795parameters each time. In that case, MatchesPredicateWithParams is your go-to
796tool for creating adhoc matchers. MatchesPredicateWithParams takes a predicate
797function and message and returns a factory to produce matchers from that. The
798predicate needs to return a boolean (or any truthy object), and accept the
799object to match + whatever was passed into the factory.
800
801For example, you might have an ``divisible`` function and want to make a
802matcher based on it::
803
804 def test_divisible_numbers(self):
805 IsDisivibleBy = MatchesPredicateWithParams(
806 divisible, '{0} is not divisible by {1}')
807 self.assertThat(7, IsDivisibleBy(1))
808 self.assertThat(7, IsDivisibleBy(7))
809 self.assertThat(7, IsDivisibleBy(2)))
810 # This will fail.
811
812Which will produce the error message::
813
814 Traceback (most recent call last):
815 File "...", line ..., in test_divisible
816 self.assertThat(7, IsDivisibleBy(2))
817 MismatchError: 7 is not divisible by 2.
818
819
783Raises820Raises
784~~~~~~821~~~~~~
785822
786823
=== modified file 'testtools/matchers/__init__.py'
--- testtools/matchers/__init__.py 2012-10-25 14:20:44 +0000
+++ testtools/matchers/__init__.py 2013-01-23 20:10:27 +0000
@@ -28,6 +28,7 @@
28 'FileContains',28 'FileContains',
29 'FileExists',29 'FileExists',
30 'GreaterThan',30 'GreaterThan',
31 'HasLength',
31 'HasPermissions',32 'HasPermissions',
32 'Is',33 'Is',
33 'IsInstance',34 'IsInstance',
@@ -39,6 +40,7 @@
39 'MatchesException',40 'MatchesException',
40 'MatchesListwise',41 'MatchesListwise',
41 'MatchesPredicate',42 'MatchesPredicate',
43 'MatchesPredicateWithParams',
42 'MatchesRegex',44 'MatchesRegex',
43 'MatchesSetwise',45 'MatchesSetwise',
44 'MatchesStructure',46 'MatchesStructure',
@@ -57,6 +59,7 @@
57 EndsWith,59 EndsWith,
58 Equals,60 Equals,
59 GreaterThan,61 GreaterThan,
62 HasLength,
60 Is,63 Is,
61 IsInstance,64 IsInstance,
62 LessThan,65 LessThan,
@@ -101,6 +104,7 @@
101 MatchesAll,104 MatchesAll,
102 MatchesAny,105 MatchesAny,
103 MatchesPredicate,106 MatchesPredicate,
107 MatchesPredicateWithParams,
104 Not,108 Not,
105 )109 )
106110
107111
=== modified file 'testtools/matchers/_basic.py'
--- testtools/matchers/_basic.py 2012-09-10 11:37:46 +0000
+++ testtools/matchers/_basic.py 2013-01-23 20:10:27 +0000
@@ -5,6 +5,7 @@
5 'EndsWith',5 'EndsWith',
6 'Equals',6 'Equals',
7 'GreaterThan',7 'GreaterThan',
8 'HasLength',
8 'Is',9 'Is',
9 'IsInstance',10 'IsInstance',
10 'LessThan',11 'LessThan',
@@ -24,7 +25,10 @@
24 text_repr,25 text_repr,
25 )26 )
26from ..helpers import list_subtract27from ..helpers import list_subtract
27from ._higherorder import PostfixedMismatch28from ._higherorder import (
29 MatchesPredicateWithParams,
30 PostfixedMismatch,
31 )
28from ._impl import (32from ._impl import (
29 Matcher,33 Matcher,
30 Mismatch,34 Mismatch,
@@ -313,3 +317,10 @@
313 pattern = pattern.encode("unicode_escape").decode("ascii")317 pattern = pattern.encode("unicode_escape").decode("ascii")
314 return Mismatch("%r does not match /%s/" % (318 return Mismatch("%r does not match /%s/" % (
315 value, pattern.replace("\\\\", "\\")))319 value, pattern.replace("\\\\", "\\")))
320
321
322def has_len(x, y):
323 return len(x) == y
324
325
326HasLength = MatchesPredicateWithParams(has_len, "len({0}) != {1}", "HasLength")
316327
=== modified file 'testtools/matchers/_higherorder.py'
--- testtools/matchers/_higherorder.py 2012-12-13 15:01:41 +0000
+++ testtools/matchers/_higherorder.py 2013-01-23 20:10:27 +0000
@@ -287,3 +287,79 @@
287 def match(self, x):287 def match(self, x):
288 if not self.predicate(x):288 if not self.predicate(x):
289 return Mismatch(self.message % x)289 return Mismatch(self.message % x)
290
291
292def MatchesPredicateWithParams(predicate, message, name=None):
293 """Match if a given parameterised function returns True.
294
295 It is reasonably common to want to make a very simple matcher based on a
296 function that you already have that returns True or False given some
297 arguments. This matcher makes it very easy to do so. e.g.::
298
299 HasLength = MatchesPredicate(
300 lambda x, y: len(x) == y, 'len({0}) is not {1}')
301 self.assertThat([1, 2], HasLength(3))
302
303 Note that unlike MatchesPredicate MatchesPredicateWithParams returns a
304 factory which you then customise to use by constructing an actual matcher
305 from it.
306
307 The predicate function should take the object to match as its first
308 parameter. Any additional parameters supplied when constructing a matcher
309 are supplied to the predicate as additional parameters when checking for a
310 match.
311
312 :param predicate: The predicate function.
313 :param message: A format string for describing mis-matches.
314 :param name: Optional replacement name for the matcher.
315 """
316 def construct_matcher(*args, **kwargs):
317 return _MatchesPredicateWithParams(
318 predicate, message, name, *args, **kwargs)
319 return construct_matcher
320
321
322class _MatchesPredicateWithParams(Matcher):
323
324 def __init__(self, predicate, message, name, *args, **kwargs):
325 """Create a ``MatchesPredicateWithParams`` matcher.
326
327 :param predicate: A function that takes an object to match and
328 additional params as given in *args and **kwargs. The result of the
329 function will be interpreted as a boolean to determine a match.
330 :param message: A message to describe a mismatch. It will be formatted
331 with .format() and be given a tuple containing whatever was passed
332 to ``match()`` + *args in *args, and whatever was passed to
333 **kwargs as its **kwargs.
334
335 For instance, to format a single parameter::
336
337 "{0} is not a {1}"
338
339 To format a keyword arg::
340
341 "{0} is not a {type_to_check}"
342 :param name: What name to use for the matcher class. Pass None to use
343 the default.
344 """
345 self.predicate = predicate
346 self.message = message
347 self.name = name
348 self.args = args
349 self.kwargs = kwargs
350
351 def __str__(self):
352 args = [str(arg) for arg in self.args]
353 kwargs = ["%s=%s" % item for item in self.kwargs.items()]
354 args = ", ".join(args + kwargs)
355 if self.name is None:
356 name = 'MatchesPredicateWithParams(%r, %r)' % (
357 self.predicate, self.message)
358 else:
359 name = self.name
360 return '%s(%s)' % (name, args)
361
362 def match(self, x):
363 if not self.predicate(x, *self.args, **self.kwargs):
364 return Mismatch(
365 self.message.format(*((x,) + self.args), **self.kwargs))
290366
=== modified file 'testtools/tests/matchers/test_basic.py'
--- testtools/tests/matchers/test_basic.py 2012-09-08 17:21:06 +0000
+++ testtools/tests/matchers/test_basic.py 2013-01-23 20:10:27 +0000
@@ -19,6 +19,7 @@
19 IsInstance,19 IsInstance,
20 LessThan,20 LessThan,
21 GreaterThan,21 GreaterThan,
22 HasLength,
22 MatchesRegex,23 MatchesRegex,
23 NotEquals,24 NotEquals,
24 SameMembers,25 SameMembers,
@@ -369,6 +370,21 @@
369 ]370 ]
370371
371372
373class TestHasLength(TestCase, TestMatchersInterface):
374
375 matches_matcher = HasLength(2)
376 matches_matches = [[1, 2]]
377 matches_mismatches = [[], [1], [3, 2, 1]]
378
379 str_examples = [
380 ("HasLength(2)", HasLength(2)),
381 ]
382
383 describe_examples = [
384 ("len([]) != 1", [], HasLength(1)),
385 ]
386
387
372def test_suite():388def test_suite():
373 from unittest import TestLoader389 from unittest import TestLoader
374 return TestLoader().loadTestsFromName(__name__)390 return TestLoader().loadTestsFromName(__name__)
375391
=== modified file 'testtools/tests/matchers/test_higherorder.py'
--- testtools/tests/matchers/test_higherorder.py 2012-12-13 15:01:41 +0000
+++ testtools/tests/matchers/test_higherorder.py 2013-01-23 20:10:27 +0000
@@ -18,6 +18,7 @@
18 MatchesAny,18 MatchesAny,
19 MatchesAll,19 MatchesAll,
20 MatchesPredicate,20 MatchesPredicate,
21 MatchesPredicateWithParams,
21 Not,22 Not,
22 )23 )
23from testtools.tests.helpers import FullStackRunTest24from testtools.tests.helpers import FullStackRunTest
@@ -222,6 +223,32 @@
222 ]223 ]
223224
224225
226def between(x, low, high):
227 return low < x < high
228
229
230class TestMatchesPredicateWithParams(TestCase, TestMatchersInterface):
231
232 matches_matcher = MatchesPredicateWithParams(
233 between, "{0} is not between {1} and {2}")(1, 9)
234 matches_matches = [2, 4, 6, 8]
235 matches_mismatches = [0, 1, 9, 10]
236
237 str_examples = [
238 ("MatchesPredicateWithParams(%r, %r)(%s)" % (
239 between, "{0} is not between {1} and {2}", "1, 2"),
240 MatchesPredicateWithParams(
241 between, "{0} is not between {1} and {2}")(1, 2)),
242 ("Between(1, 2)", MatchesPredicateWithParams(
243 between, "{0} is not between {1} and {2}", "Between")(1, 2)),
244 ]
245
246 describe_examples = [
247 ('1 is not between 2 and 3', 1, MatchesPredicateWithParams(
248 between, "{0} is not between {1} and {2}")(2, 3)),
249 ]
250
251
225def test_suite():252def test_suite():
226 from unittest import TestLoader253 from unittest import TestLoader
227 return TestLoader().loadTestsFromName(__name__)254 return TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches