1 # Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details.
3 """Matchers, a way to express complex assertions outside the testcase.
5 Inspired by 'hamcrest'.
7 Matcher provides the abstract API that all matchers need to implement.
9 Bundled matchers are listed in __all__: a list can be obtained by running
10 $ python -c 'import testtools.matchers; print testtools.matchers.__all__'
30 class Matcher(object):
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.
37 Matchers can be useful outside of test cases, as they are simply a
38 pattern matching language expressed as objects.
40 testtools.matchers is inspired by hamcrest, but is pythonic rather than
44 def match(self, something):
45 """Return None if this matcher matches something, a Mismatch otherwise.
47 raise NotImplementedError(self.match)
50 """Get a sensible human representation of the matcher.
52 This should include the parameters given to the matcher and any
53 state that would affect the matches operation.
55 raise NotImplementedError(self.__str__)
58 class Mismatch(object):
59 """An object describing a mismatch detected by a Matcher."""
61 def __init__(self, description=None, details=None):
62 """Construct a `Mismatch`.
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
70 self._description = description
73 self._details = details
76 """Describe the mismatch.
78 This should be either a human-readable string or castable to a string.
81 return self._description
82 except AttributeError:
83 raise NotImplementedError(self.describe)
85 def get_details(self):
86 """Get extra details about the mismatch.
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'.
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.
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.
101 return getattr(self, '_details', {})
104 class DocTestMatches(object):
105 """See if a string matches a doctest example."""
107 def __init__(self, example, flags=0):
108 """Create a DocTestMatches to match example.
110 :param example: The example to match e.g. 'foo bar baz'
111 :param flags: doctest comparison flags to match on. e.g.
114 if not example.endswith('\n'):
116 self.want = example # required variable name by doctest.
118 self._checker = doctest.OutputChecker()
122 flagstr = ", flags=%d" % self.flags
125 return 'DocTestMatches(%r%s)' % (self.want, flagstr)
127 def _with_nl(self, actual):
129 if not result.endswith('\n'):
133 def match(self, actual):
134 with_nl = self._with_nl(actual)
135 if self._checker.check_output(self.want, with_nl, self.flags):
137 return DocTestMismatch(self, with_nl)
139 def _describe_difference(self, with_nl):
140 return self._checker.output_difference(self, with_nl, self.flags)
143 class DocTestMismatch(Mismatch):
144 """Mismatch object for DocTestMatches."""
146 def __init__(self, matcher, with_nl):
147 self.matcher = matcher
148 self.with_nl = with_nl
151 return self.matcher._describe_difference(self.with_nl)
154 class _BinaryComparison(object):
155 """Matcher that compares an object to another object."""
157 def __init__(self, expected):
158 self.expected = expected
161 return "%s(%r)" % (self.__class__.__name__, self.expected)
163 def match(self, other):
164 if self.comparator(other, self.expected):
166 return _BinaryMismatch(self.expected, self.mismatch_string, other)
168 def comparator(self, expected, other):
169 raise NotImplementedError(self.comparator)
172 class _BinaryMismatch(Mismatch):
173 """Two things did not match."""
175 def __init__(self, expected, mismatch_string, other):
176 self.expected = expected
177 self._mismatch_string = mismatch_string
181 return "%r %s %r" % (self.expected, self._mismatch_string, self.other)
184 class Equals(_BinaryComparison):
185 """Matches if the items are equal."""
187 comparator = operator.eq
188 mismatch_string = '!='
191 class NotEquals(_BinaryComparison):
192 """Matches if the items are not equal.
194 In most cases, this is equivalent to `Not(Equals(foo))`. The difference
195 only matters when testing `__ne__` implementations.
198 comparator = operator.ne
199 mismatch_string = '=='
202 class Is(_BinaryComparison):
203 """Matches if the items are identical."""
205 comparator = operator.is_
206 mismatch_string = 'is not'
209 class LessThan(_BinaryComparison):
210 """Matches if the item is less than the matchers reference object."""
212 comparator = operator.__lt__
213 mismatch_string = 'is >='
216 class MatchesAny(object):
217 """Matches if any of the matchers it is created with match."""
219 def __init__(self, *matchers):
220 self.matchers = matchers
222 def match(self, matchee):
224 for matcher in self.matchers:
225 mismatch = matcher.match(matchee)
228 results.append(mismatch)
229 return MismatchesAll(results)
232 return "MatchesAny(%s)" % ', '.join([
233 str(matcher) for matcher in self.matchers])
236 class MatchesAll(object):
237 """Matches if all of the matchers it is created with match."""
239 def __init__(self, *matchers):
240 self.matchers = matchers
243 return 'MatchesAll(%s)' % ', '.join(map(str, self.matchers))
245 def match(self, matchee):
247 for matcher in self.matchers:
248 mismatch = matcher.match(matchee)
249 if mismatch is not None:
250 results.append(mismatch)
252 return MismatchesAll(results)
257 class MismatchesAll(Mismatch):
258 """A mismatch with many child mismatches."""
260 def __init__(self, mismatches):
261 self.mismatches = mismatches
264 descriptions = ["Differences: ["]
265 for mismatch in self.mismatches:
266 descriptions.append(mismatch.describe())
267 descriptions.append("]\n")
268 return '\n'.join(descriptions)
272 """Inverts a matcher."""
274 def __init__(self, matcher):
275 self.matcher = matcher
278 return 'Not(%s)' % (self.matcher,)
280 def match(self, other):
281 mismatch = self.matcher.match(other)
283 return MatchedUnexpectedly(self.matcher, other)
288 class MatchedUnexpectedly(Mismatch):
289 """A thing matched when it wasn't supposed to."""
291 def __init__(self, matcher, other):
292 self.matcher = matcher
296 return "%r matches %s" % (self.other, self.matcher)
299 class Annotate(object):
300 """Annotates a matcher with a descriptive string.
302 Mismatches are then described as '<mismatch>: <annotation>'.
305 def __init__(self, annotation, matcher):
306 self.annotation = annotation
307 self.matcher = matcher
310 return 'Annotate(%r, %s)' % (self.annotation, self.matcher)
312 def match(self, other):
313 mismatch = self.matcher.match(other)
314 if mismatch is not None:
315 return AnnotatedMismatch(self.annotation, mismatch)
318 class AnnotatedMismatch(Mismatch):
319 """A mismatch annotated with a descriptive string."""
321 def __init__(self, annotation, mismatch):
322 self.annotation = annotation
323 self.mismatch = mismatch
326 return '%s: %s' % (self.mismatch.describe(), self.annotation)