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/__init__.py +22 -0
- cot_assert/_entry.py +29 -0
- cot_assert/_error.py +155 -0
- cot_assert/_hook.py +224 -0
- cot_assert/_llexc.py +75 -0
- cot_assert/_render.py +311 -0
- cot_assert/_rewrite.py +495 -0
- cot_assert/_rpy.py +32 -0
- cot_assert/_runtime.py +12 -0
- cot_assert/_unparse.py +180 -0
- cot_assert/_values.py +30 -0
- cot_assert/pytest_plugin.py +140 -0
- cot_assert-0.1.0.dist-info/METADATA +69 -0
- cot_assert-0.1.0.dist-info/RECORD +17 -0
- cot_assert-0.1.0.dist-info/WHEEL +5 -0
- cot_assert-0.1.0.dist-info/entry_points.txt +2 -0
- cot_assert-0.1.0.dist-info/licenses/LICENSE +373 -0
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))
|