cot-assert 0.1.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cot_assert/_render.py ADDED
@@ -0,0 +1,311 @@
1
+ """Host-only rendering of an AnnotatedAssertion, in pytest's format.
2
+
3
+ The explanation mirrors what pytest's rewriter builds with %-templates at
4
+ rewrite time; here it is built from the site's shape and the values of the
5
+ failure path when it is needed. The formatting helpers follow
6
+ ``_pytest.assertion.rewrite`` and ``_pytest.assertion.util`` (MIT licensed).
7
+ """
8
+
9
+ from __future__ import absolute_import, division, print_function
10
+
11
+ import ast
12
+ import sys
13
+ import types
14
+
15
+ try:
16
+ import reprlib
17
+ except ImportError: # Python 2
18
+ import repr as reprlib
19
+
20
+ PY2 = sys.version_info[0] == 2
21
+ if PY2:
22
+ _string_types = (str, unicode) # noqa: F821
23
+ else:
24
+ _string_types = (str,)
25
+
26
+ DEFAULT_REPR_MAX_SIZE = 240
27
+
28
+
29
+ def _try_repr_or_str(obj):
30
+ try:
31
+ return repr(obj)
32
+ except (KeyboardInterrupt, SystemExit):
33
+ raise
34
+ except BaseException:
35
+ return '%s("%s")' % (type(obj).__name__, obj)
36
+
37
+
38
+ def _format_repr_exception(exc, obj):
39
+ try:
40
+ exc_info = _try_repr_or_str(exc)
41
+ except (KeyboardInterrupt, SystemExit):
42
+ raise
43
+ except BaseException as inner_exc:
44
+ exc_info = "unpresentable exception (%s)" % (_try_repr_or_str(inner_exc),)
45
+ return "<[%s raised in repr()] %s object at 0x%x>" % (
46
+ exc_info,
47
+ type(obj).__name__,
48
+ id(obj),
49
+ )
50
+
51
+
52
+ def _ellipsize(s, maxsize):
53
+ if len(s) > maxsize:
54
+ i = max(0, (maxsize - 3) // 2)
55
+ j = max(0, maxsize - 3 - i)
56
+ return s[:i] + "..." + s[len(s) - j :]
57
+ return s
58
+
59
+
60
+ class SafeRepr(reprlib.Repr):
61
+ def __init__(self, maxsize):
62
+ reprlib.Repr.__init__(self)
63
+ self.maxstring = maxsize if maxsize is not None else 1000000000
64
+ self.maxsize = maxsize
65
+
66
+ def repr(self, x):
67
+ try:
68
+ s = reprlib.Repr.repr(self, x)
69
+ except (KeyboardInterrupt, SystemExit):
70
+ raise
71
+ except BaseException as exc:
72
+ s = _format_repr_exception(exc, x)
73
+ if self.maxsize is not None:
74
+ s = _ellipsize(s, self.maxsize)
75
+ return s
76
+
77
+ def repr_instance(self, x, level):
78
+ try:
79
+ s = repr(x)
80
+ except (KeyboardInterrupt, SystemExit):
81
+ raise
82
+ except BaseException as exc:
83
+ s = _format_repr_exception(exc, x)
84
+ if self.maxsize is not None:
85
+ s = _ellipsize(s, self.maxsize)
86
+ return s
87
+
88
+ def repr_dict(self, x, level):
89
+ # insertion order, where the stdlib sorts
90
+ n = len(x)
91
+ if n == 0:
92
+ return "{}"
93
+ if level <= 0:
94
+ return "{...}"
95
+ pieces = []
96
+ for i, key in enumerate(x):
97
+ if i >= self.maxdict:
98
+ pieces.append("...")
99
+ break
100
+ pieces.append(
101
+ "%s: %s" % (self.repr1(key, level - 1), self.repr1(x[key], level - 1))
102
+ )
103
+ return "{" + ", ".join(pieces) + "}"
104
+
105
+
106
+ class Formatter(object):
107
+ """The pieces of rendering a host may replace.
108
+
109
+ The pytest plugin substitutes pytest's own comparison explanations and
110
+ verbosity-dependent repr size.
111
+ """
112
+
113
+ maxsize = DEFAULT_REPR_MAX_SIZE
114
+
115
+ def saferepr(self, obj):
116
+ if isinstance(obj, types.MethodType):
117
+ # for bound methods, skip redundant <bound method ...> information
118
+ return obj.__name__
119
+ if self.maxsize:
120
+ text = SafeRepr(self.maxsize).repr(obj)
121
+ else:
122
+ text = _try_repr_or_str(obj)
123
+ return text.replace("\n", "\\n")
124
+
125
+ def reprcompare(self, op, left, right):
126
+ """A multi-line explanation for ``left op right``, or None."""
127
+ return None
128
+
129
+ def format_assertmsg(self, obj):
130
+ replaces = [("\n", "\n~")]
131
+ if not isinstance(obj, _string_types):
132
+ obj = self.saferepr(obj)
133
+ replaces.append(("\\n", "\n~"))
134
+ for old, new in replaces:
135
+ obj = obj.replace(old, new)
136
+ return obj
137
+
138
+ def format_explanation(self, explanation):
139
+ return "\n".join(_format_lines(_split_explanation(explanation)))
140
+
141
+
142
+ def _split_explanation(explanation):
143
+ raw_lines = (explanation or "").split("\n")
144
+ lines = [raw_lines[0]]
145
+ for values in raw_lines[1:]:
146
+ if values and values[0] in ["{", "}", "~", ">"]:
147
+ lines.append(values)
148
+ else:
149
+ lines[-1] += "\\n" + values
150
+ return lines
151
+
152
+
153
+ def _format_lines(lines):
154
+ result = list(lines[:1])
155
+ stack = [0]
156
+ stackcnt = [0]
157
+ for line in lines[1:]:
158
+ if line.startswith("{"):
159
+ if stackcnt[-1]:
160
+ s = "and "
161
+ else:
162
+ s = "where "
163
+ stack.append(len(result))
164
+ stackcnt[-1] += 1
165
+ stackcnt.append(0)
166
+ result.append(" +" + " " * (len(stack) - 1) + s + line[1:])
167
+ elif line.startswith("}"):
168
+ stack.pop()
169
+ stackcnt.pop()
170
+ result[stack[-1]] += line[1:]
171
+ else:
172
+ stack[-1] += 1
173
+ indent = len(stack) if line.startswith("~") else len(stack) - 1
174
+ result.append(" " * indent + line[1:])
175
+ return result
176
+
177
+
178
+ _formatter = Formatter()
179
+
180
+
181
+ def set_formatter(formatter):
182
+ """Install the formatter ``render`` uses by default; returns the old one."""
183
+ global _formatter
184
+ previous, _formatter = _formatter, formatter
185
+ return previous
186
+
187
+
188
+ def render(exc, formatter=None):
189
+ formatter = formatter or _formatter
190
+ site = exc.site
191
+ values = exc.values
192
+ parts = []
193
+ if site is None or site.shape is None:
194
+ if exc.msg is not None:
195
+ parts.append(formatter.format_assertmsg(exc.msg))
196
+ if site is not None:
197
+ parts.append("assert " + site.source)
198
+ extra = list(zip(exc.all_labels(), values))
199
+ else:
200
+ slots, marks = site.paths[exc.path]
201
+ explained = _Explainer(formatter, dict(zip(slots, values)), set(marks))
202
+ explanation = explained.expl(site.shape)
203
+ if exc.msg is not None:
204
+ parts.append(formatter.format_assertmsg(exc.msg))
205
+ parts.append(">assert " + explanation)
206
+ else:
207
+ parts.append("assert " + explanation)
208
+ extra = list(zip(exc.labels, values[len(slots) :]))
209
+ for label, obj in extra:
210
+ parts.append("~%s = %s" % (label, formatter.saferepr(obj)))
211
+ text = "\n".join(parts)
212
+ if text.startswith(("~", ">")):
213
+ text = text[1:]
214
+ return formatter.format_explanation(text)
215
+
216
+
217
+ _UNKNOWN = object()
218
+
219
+
220
+ class _Explainer(object):
221
+ def __init__(self, formatter, values, marks):
222
+ self.formatter = formatter
223
+ self.values = values
224
+ self.marks = marks
225
+
226
+ def repr(self, slot):
227
+ return self.formatter.saferepr(self.values[slot])
228
+
229
+ def obj(self, shape):
230
+ """The value behind an operand shape, or _UNKNOWN."""
231
+ kind = shape[0]
232
+ if kind == "const":
233
+ try:
234
+ return ast.literal_eval(shape[1])
235
+ except ValueError:
236
+ return _UNKNOWN
237
+ if kind in ("name", "repr", "attr", "call"):
238
+ return self.values.get(shape[1], _UNKNOWN)
239
+ if kind == "compare" and shape[4] is not None:
240
+ return self.values.get(shape[4], _UNKNOWN)
241
+ return _UNKNOWN
242
+
243
+ def expl(self, shape):
244
+ return getattr(self, "expl_" + shape[0])(*shape[1:])
245
+
246
+ def expl_const(self, source):
247
+ obj = self.obj(("const", source))
248
+ if obj is _UNKNOWN:
249
+ return source
250
+ return self.formatter.saferepr(obj)
251
+
252
+ def expl_text(self, text):
253
+ return text
254
+
255
+ def expl_name(self, slot, name):
256
+ if slot in self.values:
257
+ return self.repr(slot)
258
+ return name
259
+
260
+ def expl_repr(self, slot):
261
+ return self.repr(slot)
262
+
263
+ def expl_attr(self, slot, value, attr):
264
+ res = self.repr(slot)
265
+ return "%s\n{%s = %s.%s\n}" % (res, res, self.expl(value), attr)
266
+
267
+ def expl_method(self, obj, attr):
268
+ return "%s.%s" % (self.expl(obj), attr)
269
+
270
+ def expl_binop(self, sym, left, right):
271
+ return "(%s %s %s)" % (self.expl(left), sym, self.expl(right))
272
+
273
+ def expl_unary(self, pattern, operand):
274
+ return pattern % (self.expl(operand),)
275
+
276
+ def expl_call(self, slot, func, args):
277
+ res = self.repr(slot)
278
+ arglist = ", ".join(prefix + self.expl(arg) for prefix, arg in args)
279
+ return "%s\n{%s = %s(%s)\n}" % (res, res, self.expl(func), arglist)
280
+
281
+ def expl_boolop(self, node, is_or, operands):
282
+ expls = [
283
+ self.expl(shape)
284
+ for i, shape in enumerate(operands)
285
+ if ("bool", node, i) in self.marks
286
+ ]
287
+ return "(" + (" or " if is_or else " and ").join(expls) + ")"
288
+
289
+ def expl_compare(self, node, syms, operands, result):
290
+ # the first comparison known to fail, else the last one evaluated
291
+ # (that is where pytest's _call_reprcompare stops)
292
+ index = None
293
+ for i in range(len(syms)):
294
+ if ("cmp", node, i) in self.marks:
295
+ index = i
296
+ break
297
+ if index is None and result is not None and result in self.values:
298
+ if not self.values[result]:
299
+ index = 0
300
+ if index is None:
301
+ index = len(syms) - 1
302
+ while index > 0 and self.obj(operands[index + 1]) is _UNKNOWN:
303
+ index -= 1
304
+ left, right = operands[index], operands[index + 1]
305
+ sym = syms[index]
306
+ left_obj, right_obj = self.obj(left), self.obj(right)
307
+ if left_obj is not _UNKNOWN and right_obj is not _UNKNOWN:
308
+ custom = self.formatter.reprcompare(sym, left_obj, right_obj)
309
+ if custom is not None:
310
+ return custom
311
+ return "%s %s %s" % (self.expl(left), sym, self.expl(right))