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__'
28 from pprint import pformat
31 class Matcher(object):
34 A Matcher must implement match and __str__ to be used by
35 testtools.TestCase.assertThat. Matcher.match(thing) returns None when
36 thing is completely matched, and a Mismatch object otherwise.
38 Matchers can be useful outside of test cases, as they are simply a
39 pattern matching language expressed as objects.
41 testtools.matchers is inspired by hamcrest, but is pythonic rather than
45 def match(self, something):
46 """Return None if this matcher matches something, a Mismatch otherwise.
48 raise NotImplementedError(self.match)
51 """Get a sensible human representation of the matcher.
53 This should include the parameters given to the matcher and any
54 state that would affect the matches operation.
56 raise NotImplementedError(self.__str__)
59 class Mismatch(object):
60 """An object describing a mismatch detected by a Matcher."""
62 def __init__(self, description=None, details=None):
63 """Construct a `Mismatch`.
65 :param description: A description to use. If not provided,
66 `Mismatch.describe` must be implemented.
67 :param details: Extra details about the mismatch. Defaults
71 self._description = description
74 self._details = details
77 """Describe the mismatch.
79 This should be either a human-readable string or castable to a string.
82 return self._description
83 except AttributeError:
84 raise NotImplementedError(self.describe)
86 def get_details(self):
87 """Get extra details about the mismatch.
89 This allows the mismatch to provide extra information beyond the basic
90 description, including large text or binary files, or debugging internals
91 without having to force it to fit in the output of 'describe'.
93 The testtools assertion assertThat will query get_details and attach
94 all its values to the test, permitting them to be reported in whatever
95 manner the test environment chooses.
97 :return: a dict mapping names to Content objects. name is a string to
98 name the detail, and the Content object is the detail to add
99 to the result. For more information see the API to which items from
100 this dict are passed testtools.TestCase.addDetail.
102 return getattr(self, '_details', {})
105 class DocTestMatches(object):
106 """See if a string matches a doctest example."""
108 def __init__(self, example, flags=0):
109 """Create a DocTestMatches to match example.
111 :param example: The example to match e.g. 'foo bar baz'
112 :param flags: doctest comparison flags to match on. e.g.
115 if not example.endswith('\n'):
117 self.want = example # required variable name by doctest.
119 self._checker = doctest.OutputChecker()
123 flagstr = ", flags=%d" % self.flags
126 return 'DocTestMatches(%r%s)' % (self.want, flagstr)
128 def _with_nl(self, actual):
130 if not result.endswith('\n'):
134 def match(self, actual):
135 with_nl = self._with_nl(actual)
136 if self._checker.check_output(self.want, with_nl, self.flags):
138 return DocTestMismatch(self, with_nl)
140 def _describe_difference(self, with_nl):
141 return self._checker.output_difference(self, with_nl, self.flags)
144 class DocTestMismatch(Mismatch):
145 """Mismatch object for DocTestMatches."""
147 def __init__(self, matcher, with_nl):
148 self.matcher = matcher
149 self.with_nl = with_nl
152 return self.matcher._describe_difference(self.with_nl)
155 class _BinaryComparison(object):
156 """Matcher that compares an object to another object."""
158 def __init__(self, expected):
159 self.expected = expected
162 return "%s(%r)" % (self.__class__.__name__, self.expected)
164 def match(self, other):
165 if self.comparator(other, self.expected):
167 return _BinaryMismatch(self.expected, self.mismatch_string, other)
169 def comparator(self, expected, other):
170 raise NotImplementedError(self.comparator)
173 class _BinaryMismatch(Mismatch):
174 """Two things did not match."""
176 def __init__(self, expected, mismatch_string, other):
177 self.expected = expected
178 self._mismatch_string = mismatch_string
182 left = repr(self.expected)
183 right = repr(self.other)
184 if len(left) + len(right) > 70:
185 return "%s:\nreference = %s\nactual = %s\n" % (
186 self._mismatch_string, pformat(self.expected),
189 return "%s %s %s" % (left, self._mismatch_string,right)
190 return "%r %s %r" % (self.expected, self._mismatch_string, self.other)
193 class Equals(_BinaryComparison):
194 """Matches if the items are equal."""
196 comparator = operator.eq
197 mismatch_string = '!='
200 class NotEquals(_BinaryComparison):
201 """Matches if the items are not equal.
203 In most cases, this is equivalent to `Not(Equals(foo))`. The difference
204 only matters when testing `__ne__` implementations.
207 comparator = operator.ne
208 mismatch_string = '=='
211 class Is(_BinaryComparison):
212 """Matches if the items are identical."""
214 comparator = operator.is_
215 mismatch_string = 'is not'
218 class LessThan(_BinaryComparison):
219 """Matches if the item is less than the matchers reference object."""
221 comparator = operator.__lt__
222 mismatch_string = 'is >='
225 class MatchesAny(object):
226 """Matches if any of the matchers it is created with match."""
228 def __init__(self, *matchers):
229 self.matchers = matchers
231 def match(self, matchee):
233 for matcher in self.matchers:
234 mismatch = matcher.match(matchee)
237 results.append(mismatch)
238 return MismatchesAll(results)
241 return "MatchesAny(%s)" % ', '.join([
242 str(matcher) for matcher in self.matchers])
245 class MatchesAll(object):
246 """Matches if all of the matchers it is created with match."""
248 def __init__(self, *matchers):
249 self.matchers = matchers
252 return 'MatchesAll(%s)' % ', '.join(map(str, self.matchers))
254 def match(self, matchee):
256 for matcher in self.matchers:
257 mismatch = matcher.match(matchee)
258 if mismatch is not None:
259 results.append(mismatch)
261 return MismatchesAll(results)
266 class MismatchesAll(Mismatch):
267 """A mismatch with many child mismatches."""
269 def __init__(self, mismatches):
270 self.mismatches = mismatches
273 descriptions = ["Differences: ["]
274 for mismatch in self.mismatches:
275 descriptions.append(mismatch.describe())
276 descriptions.append("]\n")
277 return '\n'.join(descriptions)
281 """Inverts a matcher."""
283 def __init__(self, matcher):
284 self.matcher = matcher
287 return 'Not(%s)' % (self.matcher,)
289 def match(self, other):
290 mismatch = self.matcher.match(other)
292 return MatchedUnexpectedly(self.matcher, other)
297 class MatchedUnexpectedly(Mismatch):
298 """A thing matched when it wasn't supposed to."""
300 def __init__(self, matcher, other):
301 self.matcher = matcher
305 return "%r matches %s" % (self.other, self.matcher)
308 class Annotate(object):
309 """Annotates a matcher with a descriptive string.
311 Mismatches are then described as '<mismatch>: <annotation>'.
314 def __init__(self, annotation, matcher):
315 self.annotation = annotation
316 self.matcher = matcher
319 return 'Annotate(%r, %s)' % (self.annotation, self.matcher)
321 def match(self, other):
322 mismatch = self.matcher.match(other)
323 if mismatch is not None:
324 return AnnotatedMismatch(self.annotation, mismatch)
327 class AnnotatedMismatch(Mismatch):
328 """A mismatch annotated with a descriptive string."""
330 def __init__(self, annotation, mismatch):
331 self.annotation = annotation
332 self.mismatch = mismatch
335 return '%s: %s' % (self.mismatch.describe(), self.annotation)