testtools: Import latest upstream.
[metze/samba/wip.git] / lib / testtools / 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     'Annotate',
16     'DocTestMatches',
17     'Equals',
18     'Is',
19     'LessThan',
20     'MatchesAll',
21     'MatchesAny',
22     'NotEquals',
23     'Not',
24     ]
25
26 import doctest
27 import operator
28
29
30 class Matcher(object):
31     """A pattern matcher.
32
33     A Matcher must implement match and __str__ to be used by
34     testtools.TestCase.assertThat. Matcher.match(thing) returns None when
35     thing is completely matched, and a Mismatch object otherwise.
36
37     Matchers can be useful outside of test cases, as they are simply a
38     pattern matching language expressed as objects.
39
40     testtools.matchers is inspired by hamcrest, but is pythonic rather than
41     a Java transcription.
42     """
43
44     def match(self, something):
45         """Return None if this matcher matches something, a Mismatch otherwise.
46         """
47         raise NotImplementedError(self.match)
48
49     def __str__(self):
50         """Get a sensible human representation of the matcher.
51
52         This should include the parameters given to the matcher and any
53         state that would affect the matches operation.
54         """
55         raise NotImplementedError(self.__str__)
56
57
58 class Mismatch(object):
59     """An object describing a mismatch detected by a Matcher."""
60
61     def __init__(self, description=None, details=None):
62         """Construct a `Mismatch`.
63
64         :param description: A description to use.  If not provided,
65             `Mismatch.describe` must be implemented.
66         :param details: Extra details about the mismatch.  Defaults
67             to the empty dict.
68         """
69         if description:
70             self._description = description
71         if details is None:
72             details = {}
73         self._details = details
74
75     def describe(self):
76         """Describe the mismatch.
77
78         This should be either a human-readable string or castable to a string.
79         """
80         try:
81             return self._description
82         except AttributeError:
83             raise NotImplementedError(self.describe)
84
85     def get_details(self):
86         """Get extra details about the mismatch.
87
88         This allows the mismatch to provide extra information beyond the basic
89         description, including large text or binary files, or debugging internals
90         without having to force it to fit in the output of 'describe'.
91
92         The testtools assertion assertThat will query get_details and attach
93         all its values to the test, permitting them to be reported in whatever
94         manner the test environment chooses.
95
96         :return: a dict mapping names to Content objects. name is a string to
97             name the detail, and the Content object is the detail to add
98             to the result. For more information see the API to which items from
99             this dict are passed testtools.TestCase.addDetail.
100         """
101         return getattr(self, '_details', {})
102
103
104 class DocTestMatches(object):
105     """See if a string matches a doctest example."""
106
107     def __init__(self, example, flags=0):
108         """Create a DocTestMatches to match example.
109
110         :param example: The example to match e.g. 'foo bar baz'
111         :param flags: doctest comparison flags to match on. e.g.
112             doctest.ELLIPSIS.
113         """
114         if not example.endswith('\n'):
115             example += '\n'
116         self.want = example # required variable name by doctest.
117         self.flags = flags
118         self._checker = doctest.OutputChecker()
119
120     def __str__(self):
121         if self.flags:
122             flagstr = ", flags=%d" % self.flags
123         else:
124             flagstr = ""
125         return 'DocTestMatches(%r%s)' % (self.want, flagstr)
126
127     def _with_nl(self, actual):
128         result = str(actual)
129         if not result.endswith('\n'):
130             result += '\n'
131         return result
132
133     def match(self, actual):
134         with_nl = self._with_nl(actual)
135         if self._checker.check_output(self.want, with_nl, self.flags):
136             return None
137         return DocTestMismatch(self, with_nl)
138
139     def _describe_difference(self, with_nl):
140         return self._checker.output_difference(self, with_nl, self.flags)
141
142
143 class DocTestMismatch(Mismatch):
144     """Mismatch object for DocTestMatches."""
145
146     def __init__(self, matcher, with_nl):
147         self.matcher = matcher
148         self.with_nl = with_nl
149
150     def describe(self):
151         return self.matcher._describe_difference(self.with_nl)
152
153
154 class _BinaryComparison(object):
155     """Matcher that compares an object to another object."""
156
157     def __init__(self, expected):
158         self.expected = expected
159
160     def __str__(self):
161         return "%s(%r)" % (self.__class__.__name__, self.expected)
162
163     def match(self, other):
164         if self.comparator(other, self.expected):
165             return None
166         return _BinaryMismatch(self.expected, self.mismatch_string, other)
167
168     def comparator(self, expected, other):
169         raise NotImplementedError(self.comparator)
170
171
172 class _BinaryMismatch(Mismatch):
173     """Two things did not match."""
174
175     def __init__(self, expected, mismatch_string, other):
176         self.expected = expected
177         self._mismatch_string = mismatch_string
178         self.other = other
179
180     def describe(self):
181         return "%r %s %r" % (self.expected, self._mismatch_string, self.other)
182
183
184 class Equals(_BinaryComparison):
185     """Matches if the items are equal."""
186
187     comparator = operator.eq
188     mismatch_string = '!='
189
190
191 class NotEquals(_BinaryComparison):
192     """Matches if the items are not equal.
193
194     In most cases, this is equivalent to `Not(Equals(foo))`. The difference
195     only matters when testing `__ne__` implementations.
196     """
197
198     comparator = operator.ne
199     mismatch_string = '=='
200
201
202 class Is(_BinaryComparison):
203     """Matches if the items are identical."""
204
205     comparator = operator.is_
206     mismatch_string = 'is not'
207
208
209 class LessThan(_BinaryComparison):
210     """Matches if the item is less than the matchers reference object."""
211
212     comparator = operator.__lt__
213     mismatch_string = 'is >='
214
215
216 class MatchesAny(object):
217     """Matches if any of the matchers it is created with match."""
218
219     def __init__(self, *matchers):
220         self.matchers = matchers
221
222     def match(self, matchee):
223         results = []
224         for matcher in self.matchers:
225             mismatch = matcher.match(matchee)
226             if mismatch is None:
227                 return None
228             results.append(mismatch)
229         return MismatchesAll(results)
230
231     def __str__(self):
232         return "MatchesAny(%s)" % ', '.join([
233             str(matcher) for matcher in self.matchers])
234
235
236 class MatchesAll(object):
237     """Matches if all of the matchers it is created with match."""
238
239     def __init__(self, *matchers):
240         self.matchers = matchers
241
242     def __str__(self):
243         return 'MatchesAll(%s)' % ', '.join(map(str, self.matchers))
244
245     def match(self, matchee):
246         results = []
247         for matcher in self.matchers:
248             mismatch = matcher.match(matchee)
249             if mismatch is not None:
250                 results.append(mismatch)
251         if results:
252             return MismatchesAll(results)
253         else:
254             return None
255
256
257 class MismatchesAll(Mismatch):
258     """A mismatch with many child mismatches."""
259
260     def __init__(self, mismatches):
261         self.mismatches = mismatches
262
263     def describe(self):
264         descriptions = ["Differences: ["]
265         for mismatch in self.mismatches:
266             descriptions.append(mismatch.describe())
267         descriptions.append("]\n")
268         return '\n'.join(descriptions)
269
270
271 class Not(object):
272     """Inverts a matcher."""
273
274     def __init__(self, matcher):
275         self.matcher = matcher
276
277     def __str__(self):
278         return 'Not(%s)' % (self.matcher,)
279
280     def match(self, other):
281         mismatch = self.matcher.match(other)
282         if mismatch is None:
283             return MatchedUnexpectedly(self.matcher, other)
284         else:
285             return None
286
287
288 class MatchedUnexpectedly(Mismatch):
289     """A thing matched when it wasn't supposed to."""
290
291     def __init__(self, matcher, other):
292         self.matcher = matcher
293         self.other = other
294
295     def describe(self):
296         return "%r matches %s" % (self.other, self.matcher)
297
298
299 class Annotate(object):
300     """Annotates a matcher with a descriptive string.
301
302     Mismatches are then described as '<mismatch>: <annotation>'.
303     """
304
305     def __init__(self, annotation, matcher):
306         self.annotation = annotation
307         self.matcher = matcher
308
309     def __str__(self):
310         return 'Annotate(%r, %s)' % (self.annotation, self.matcher)
311
312     def match(self, other):
313         mismatch = self.matcher.match(other)
314         if mismatch is not None:
315             return AnnotatedMismatch(self.annotation, mismatch)
316
317
318 class AnnotatedMismatch(Mismatch):
319     """A mismatch annotated with a descriptive string."""
320
321     def __init__(self, annotation, mismatch):
322         self.annotation = annotation
323         self.mismatch = mismatch
324
325     def describe(self):
326         return '%s: %s' % (self.mismatch.describe(), self.annotation)