Merge lp:~rvb/testtools/testtools-contains-all into lp:~testtools-committers/testtools/trunk

Proposed by Raphaël Badin on 2012-04-30
Status: Merged
Merged at revision: 261
Proposed branch: lp:~rvb/testtools/testtools-contains-all
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 72 lines (+29/-1)
2 files modified
testtools/matchers.py (+11/-0)
testtools/tests/test_matchers.py (+18/-1)
To merge this branch: bzr merge lp:~rvb/testtools/testtools-contains-all
Reviewer Review Type Date Requested Status
Jonathan Lange 2012-04-30 Needs Fixing on 2012-04-30
Review via email: mp+104065@code.launchpad.net

Commit Message

New matcher ContainsAll.

Description of the Change

This branch adds a new (shortcut) matcher ContainsAll. ContainsAll(items) that can be used instead of using MatchesAll(*[Contains(item) for item in items])). We use it quite a lot in MAAS so we figured we could submit it upstream.

Drive-by fix: removed unused import.

To post a comment you must log in.
Jonathan Lange (jml) wrote :

Thanks for the patch, Raphaël.

I don't quite understand the matcher though. From the docstring and the name, I would have guess something like this::

  self.assertThat([1, 2, 3], ContainsAll([3, 1]))

That is, self.assertThat(Y, ContainsAll(X)) would pass if and only if X is a subset of Y.

As it is, self.assertThat(Y, ContainsAll(X)) passes iff for all y in Y, for all x in X, x in y. (i.e. ∀ y ∊ Y, ∀ x ∊ X, 'x in y')

So, to me, the name is confusing and my lazy imagination isn't leaping to actual use cases.

Where to from here?

 * Demonstrate use cases

 * Pick a better name, maybe EachContainsAll

Follow up work for testtools more generally:

 * some kind of more generic helper might be interesting, e.g.
     lambda matcher, items: MatchesAll(*[matcher(item) for item in items], first_only=False)
   I don't really know what it ought to be called. Sorry.

 * MatchesAll is really an "And"-style function. It would probably be better if its name reflected that.

 * More generally, thinking a little bit more about predicate & set algebra might provide us with some better combinators

Thanks,
jml

review: Needs Fixing
Raphaël Badin (rvb) wrote :

> Thanks for the patch, Raphaël.
>
> I don't quite understand the matcher though. From the docstring and the name,
> I would have guess something like this::
>
> self.assertThat([1, 2, 3], ContainsAll([3, 1]))
>
> That is, self.assertThat(Y, ContainsAll(X)) would pass if and only if X is a
> subset of Y.

Indeed, that's precisely what this matcher does. Again, it's simply a shortcut, it uses existing functionality.

> As it is, self.assertThat(Y, ContainsAll(X)) passes iff for all y in Y, for
> all x in X, x in y. (i.e. ∀ y ∊ Y, ∀ x ∊ X, 'x in y')

Maybe I'm missing something but that's not what this matcher does. I can't resist using these nice utf8 characters:
self.assertThat(y, ContainsAll(X)) passes iff for all x in X, x in y. (i.e. ∀ x ∊ X, 'x in y')

Jonathan Lange (jml) wrote :

OK, so this does behave as like a subset matcher. Oops.

<jml> rvba: the problem was that I read this:
 + matches_matches = ['foobar', 'foozbar', 'bar foo']
 as this:
 + matches_matches = [['foobar', 'foozbar', 'bar foo']]

Raphaël, could you please update the tests to not rely on string containment -- I think that would be clearer. e.g.

 + matches_matches = [['foo', 'bar'], ['foo', 'z', 'bar'], ['bar', 'foo']]

Also if you could add another sentence to the class docstring using the word 'subset' somewhere that'd be great.

jml

259. By Raphaël Badin on 2012-04-30

Refactor tests. Improve doc.

Raphaël Badin (rvb) wrote :

> Raphaël, could you please update the tests to not rely on string containment
> -- I think that would be clearer. e.g.
>
> + matches_matches = [['foo', 'bar'], ['foo', 'z', 'bar'], ['bar', 'foo']]

Sure, I've refactored matches_matches and matches_mismatches in TestContainsAllInterface to not rely on string containment.

> Also if you could add another sentence to the class docstring using the word
> 'subset' somewhere that'd be great.

Done. Feel free to suggest something else for the docstring if the one I put doesn't suit you.

Raphaël Badin (rvb) wrote :

Hi Jonathan,

If you think this is worth it, could you please have another look at this? I think I've made all the improvements you requested.

R.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'testtools/matchers.py'
2--- testtools/matchers.py 2012-04-27 13:41:16 +0000
3+++ testtools/matchers.py 2012-04-30 14:41:18 +0000
4@@ -16,6 +16,7 @@
5 'AllMatch',
6 'Annotate',
7 'Contains',
8+ 'ContainsAll',
9 'DirExists',
10 'DocTestMatches',
11 'EndsWith',
12@@ -664,6 +665,16 @@
13 return None
14
15
16+def ContainsAll(items):
17+ """Make a matcher that checks whether a list of things is contained
18+ in another thing.
19+
20+ The matcher effectively checks that the provided sequence is a subset of
21+ the matchee.
22+ """
23+ return MatchesAll(*map(Contains, items), first_only=False)
24+
25+
26 class StartsWith(Matcher):
27 """Checks whether one string starts with another."""
28
29
30=== modified file 'testtools/tests/test_matchers.py'
31--- testtools/tests/test_matchers.py 2011-12-21 01:28:38 +0000
32+++ testtools/tests/test_matchers.py 2012-04-30 14:41:18 +0000
33@@ -12,7 +12,6 @@
34
35 from testtools import (
36 Matcher, # check that Matcher is exposed at the top level for docs.
37- skipIf,
38 TestCase,
39 )
40 from testtools.compat import (
41@@ -29,6 +28,7 @@
42 AnnotatedMismatch,
43 _BinaryMismatch,
44 Contains,
45+ ContainsAll,
46 DirContains,
47 DirExists,
48 DocTestMatches,
49@@ -409,6 +409,23 @@
50 describe_examples = [("1 not in 2", 2, Contains(1))]
51
52
53+class TestContainsAllInterface(TestCase, TestMatchersInterface):
54+
55+ matches_matcher = ContainsAll(['foo', 'bar'])
56+ matches_matches = [['foo', 'bar'], ['foo', 'z', 'bar'], ['bar', 'foo']]
57+ matches_mismatches = [['f', 'g'], ['foo', 'baz'], []]
58+
59+ str_examples = [(
60+ "MatchesAll(Contains('foo'), Contains('bar'))",
61+ ContainsAll(['foo', 'bar'])),
62+ ]
63+
64+ describe_examples = [("""Differences: [
65+'baz' not in 'foo'
66+]""",
67+ 'foo', ContainsAll(['foo', 'baz']))]
68+
69+
70 def make_error(type, *args, **kwargs):
71 try:
72 raise type(*args, **kwargs)

Subscribers

People subscribed via source and target branches