subunit/testtools: Include newer version.
[ira/wip.git] / lib / subunit / python / testtools / matchers.py
1 # Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details.
2
3 """Matchers, a way to express complex assertions outside the testcase.
4
5 Inspired by 'hamcrest'.
6
7 Matcher provides the abstract API that all matchers need to implement.
8
9 Bundled matchers are listed in __all__: a list can be obtained by running
10 $ python -c 'import testtools.matchers; print testtools.matchers.__all__'
11 """
12
13 __metaclass__ = type
14 __all__ = [
15     'DocTestMatches',
16     'Equals',
17     'MatchesAll',
18     'MatchesAny',
19     'NotEquals',
20     'Not',
21     ]
22
23 import doctest
24
25
26 class Matcher:
27     """A pattern matcher.
28
29     A Matcher must implement match and __str__ to be used by
30     testtools.TestCase.assertThat. Matcher.match(thing) returns None when
31     thing is completely matched, and a Mismatch object otherwise.
32
33     Matchers can be useful outside of test cases, as they are simply a
34     pattern matching language expressed as objects.
35
36     testtools.matchers is inspired by hamcrest, but is pythonic rather than
37     a Java transcription.
38     """
39
40     def match(self, something):
41         """Return None if this matcher matches something, a Mismatch otherwise.
42         """
43         raise NotImplementedError(self.match)
44
45     def __str__(self):
46         """Get a sensible human representation of the matcher.
47
48         This should include the parameters given to the matcher and any
49         state that would affect the matches operation.
50         """
51         raise NotImplementedError(self.__str__)
52
53
54 class Mismatch:
55     """An object describing a mismatch detected by a Matcher."""
56
57     def describe(self):
58         """Describe the mismatch.
59
60         This should be either a human-readable string or castable to a string.
61         """
62         raise NotImplementedError(self.describe_difference)
63
64
65 class DocTestMatches:
66     """See if a string matches a doctest example."""
67
68     def __init__(self, example, flags=0):
69         """Create a DocTestMatches to match example.
70
71         :param example: The example to match e.g. 'foo bar baz'
72         :param flags: doctest comparison flags to match on. e.g.
73             doctest.ELLIPSIS.
74         """
75         if not example.endswith('\n'):
76             example += '\n'
77         self.want = example # required variable name by doctest.
78         self.flags = flags
79         self._checker = doctest.OutputChecker()
80
81     def __str__(self):
82         if self.flags:
83             flagstr = ", flags=%d" % self.flags
84         else:
85             flagstr = ""
86         return 'DocTestMatches(%r%s)' % (self.want, flagstr)
87
88     def _with_nl(self, actual):
89         result = str(actual)
90         if not result.endswith('\n'):
91             result += '\n'
92         return result
93
94     def match(self, actual):
95         with_nl = self._with_nl(actual)
96         if self._checker.check_output(self.want, with_nl, self.flags):
97             return None
98         return DocTestMismatch(self, with_nl)
99
100     def _describe_difference(self, with_nl):
101         return self._checker.output_difference(self, with_nl, self.flags)
102
103
104 class DocTestMismatch:
105     """Mismatch object for DocTestMatches."""
106
107     def __init__(self, matcher, with_nl):
108         self.matcher = matcher
109         self.with_nl = with_nl
110
111     def describe(self):
112         return self.matcher._describe_difference(self.with_nl)
113
114
115 class Equals:
116     """Matches if the items are equal."""
117
118     def __init__(self, expected):
119         self.expected = expected
120
121     def match(self, other):
122         if self.expected == other:
123             return None
124         return EqualsMismatch(self.expected, other)
125
126     def __str__(self):
127         return "Equals(%r)" % self.expected
128
129
130 class EqualsMismatch:
131     """Two things differed."""
132
133     def __init__(self, expected, other):
134         self.expected = expected
135         self.other = other
136
137     def describe(self):
138         return "%r != %r" % (self.expected, self.other)
139
140
141 class NotEquals:
142     """Matches if the items are not equal.
143
144     In most cases, this is equivalent to `Not(Equals(foo))`. The difference
145     only matters when testing `__ne__` implementations.
146     """
147
148     def __init__(self, expected):
149         self.expected = expected
150
151     def __str__(self):
152         return 'NotEquals(%r)' % (self.expected,)
153
154     def match(self, other):
155         if self.expected != other:
156             return None
157         return NotEqualsMismatch(self.expected, other)
158
159
160 class NotEqualsMismatch:
161     """Two things are the same."""
162
163     def __init__(self, expected, other):
164         self.expected = expected
165         self.other = other
166
167     def describe(self):
168         return '%r == %r' % (self.expected, self.other)
169
170
171 class MatchesAny:
172     """Matches if any of the matchers it is created with match."""
173
174     def __init__(self, *matchers):
175         self.matchers = matchers
176
177     def match(self, matchee):
178         results = []
179         for matcher in self.matchers:
180             mismatch = matcher.match(matchee)
181             if mismatch is None:
182                 return None
183             results.append(mismatch)
184         return MismatchesAll(results)
185
186     def __str__(self):
187         return "MatchesAny(%s)" % ', '.join([
188             str(matcher) for matcher in self.matchers])
189
190
191 class MatchesAll:
192     """Matches if all of the matchers it is created with match."""
193
194     def __init__(self, *matchers):
195         self.matchers = matchers
196
197     def __str__(self):
198         return 'MatchesAll(%s)' % ', '.join(map(str, self.matchers))
199
200     def match(self, matchee):
201         results = []
202         for matcher in self.matchers:
203             mismatch = matcher.match(matchee)
204             if mismatch is not None:
205                 results.append(mismatch)
206         if results:
207             return MismatchesAll(results)
208         else:
209             return None
210
211
212 class MismatchesAll:
213     """A mismatch with many child mismatches."""
214
215     def __init__(self, mismatches):
216         self.mismatches = mismatches
217
218     def describe(self):
219         descriptions = ["Differences: ["]
220         for mismatch in self.mismatches:
221             descriptions.append(mismatch.describe())
222         descriptions.append("]\n")
223         return '\n'.join(descriptions)
224
225
226 class Not:
227     """Inverts a matcher."""
228
229     def __init__(self, matcher):
230         self.matcher = matcher
231
232     def __str__(self):
233         return 'Not(%s)' % (self.matcher,)
234
235     def match(self, other):
236         mismatch = self.matcher.match(other)
237         if mismatch is None:
238             return MatchedUnexpectedly(self.matcher, other)
239         else:
240             return None
241
242
243 class MatchedUnexpectedly:
244     """A thing matched when it wasn't supposed to."""
245
246     def __init__(self, matcher, other):
247         self.matcher = matcher
248         self.other = other
249
250     def describe(self):
251         return "%r matches %s" % (self.other, self.matcher)