36e5ee022182c7725381e2572d5bb4102aff978c
[obnox/samba/samba-obnox.git] / lib / testtools / testtools / matchers / _impl.py
1 # Copyright (c) 2009-2012 testtools developers. 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 __all__ = [
14     'Matcher',
15     'Mismatch',
16     'MismatchDecorator',
17     'MismatchError',
18     ]
19
20 from testtools.compat import (
21     _isbytes,
22     istext,
23     str_is_unicode,
24     text_repr
25     )
26
27
28 class Matcher(object):
29     """A pattern matcher.
30
31     A Matcher must implement match and __str__ to be used by
32     testtools.TestCase.assertThat. Matcher.match(thing) returns None when
33     thing is completely matched, and a Mismatch object otherwise.
34
35     Matchers can be useful outside of test cases, as they are simply a
36     pattern matching language expressed as objects.
37
38     testtools.matchers is inspired by hamcrest, but is pythonic rather than
39     a Java transcription.
40     """
41
42     def match(self, something):
43         """Return None if this matcher matches something, a Mismatch otherwise.
44         """
45         raise NotImplementedError(self.match)
46
47     def __str__(self):
48         """Get a sensible human representation of the matcher.
49
50         This should include the parameters given to the matcher and any
51         state that would affect the matches operation.
52         """
53         raise NotImplementedError(self.__str__)
54
55
56 class Mismatch(object):
57     """An object describing a mismatch detected by a Matcher."""
58
59     def __init__(self, description=None, details=None):
60         """Construct a `Mismatch`.
61
62         :param description: A description to use.  If not provided,
63             `Mismatch.describe` must be implemented.
64         :param details: Extra details about the mismatch.  Defaults
65             to the empty dict.
66         """
67         if description:
68             self._description = description
69         if details is None:
70             details = {}
71         self._details = details
72
73     def describe(self):
74         """Describe the mismatch.
75
76         This should be either a human-readable string or castable to a string.
77         In particular, is should either be plain ascii or unicode on Python 2,
78         and care should be taken to escape control characters.
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     def __repr__(self):
104         return  "<testtools.matchers.Mismatch object at %x attributes=%r>" % (
105             id(self), self.__dict__)
106
107
108 class MismatchError(AssertionError):
109     """Raised when a mismatch occurs."""
110
111     # This class exists to work around
112     # <https://bugs.launchpad.net/testtools/+bug/804127>.  It provides a
113     # guaranteed way of getting a readable exception, no matter what crazy
114     # characters are in the matchee, matcher or mismatch.
115
116     def __init__(self, matchee, matcher, mismatch, verbose=False):
117         # Have to use old-style upcalling for Python 2.4 and 2.5
118         # compatibility.
119         AssertionError.__init__(self)
120         self.matchee = matchee
121         self.matcher = matcher
122         self.mismatch = mismatch
123         self.verbose = verbose
124
125     def __str__(self):
126         difference = self.mismatch.describe()
127         if self.verbose:
128             # GZ 2011-08-24: Smelly API? Better to take any object and special
129             #                case text inside?
130             if istext(self.matchee) or _isbytes(self.matchee):
131                 matchee = text_repr(self.matchee, multiline=False)
132             else:
133                 matchee = repr(self.matchee)
134             return (
135                 'Match failed. Matchee: %s\nMatcher: %s\nDifference: %s\n'
136                 % (matchee, self.matcher, difference))
137         else:
138             return difference
139
140     if not str_is_unicode:
141
142         __unicode__ = __str__
143
144         def __str__(self):
145             return self.__unicode__().encode("ascii", "backslashreplace")
146
147
148 class MismatchDecorator(object):
149     """Decorate a ``Mismatch``.
150
151     Forwards all messages to the original mismatch object.  Probably the best
152     way to use this is inherit from this class and then provide your own
153     custom decoration logic.
154     """
155
156     def __init__(self, original):
157         """Construct a `MismatchDecorator`.
158
159         :param original: A `Mismatch` object to decorate.
160         """
161         self.original = original
162
163     def __repr__(self):
164         return '<testtools.matchers.MismatchDecorator(%r)>' % (self.original,)
165
166     def describe(self):
167         return self.original.describe()
168
169     def get_details(self):
170         return self.original.get_details()
171
172
173 # Signal that this is part of the testing framework, and that code from this
174 # should not normally appear in tracebacks.
175 __unittest = True