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/_rewrite.py ADDED
@@ -0,0 +1,495 @@
1
+ """Rewrite assert statements to raise AnnotatedAssertion.
2
+
3
+ The emitted code is kept inside what the RPython flow space accepts and to
4
+ plain bytecode: every sub-expression lands in its own temporary (one type
5
+ each), short-circuits become nested ``if``s, temporaries are never reset to
6
+ None, and the failure branch is a single call per exit that gets the values
7
+ evaluated on that path.
8
+ """
9
+
10
+ from __future__ import absolute_import, division, print_function
11
+
12
+ import ast
13
+ import copy
14
+ import sys
15
+
16
+ from ._unparse import binop, cmpop, unaryop, unparse
17
+
18
+ PY2 = sys.version_info[0] == 2
19
+
20
+ RUNTIME = "@cot_rt"
21
+ DONT_REWRITE = ("COT_DONT_REWRITE", "PYTEST_DONT_REWRITE")
22
+
23
+ if PY2:
24
+ _NAME_CONSTANTS = ("True", "False", "None")
25
+ _CONST_NODES = (ast.Num, ast.Str)
26
+ else:
27
+ _NAME_CONSTANTS = ()
28
+ _CONST_NODES = (ast.Constant,)
29
+
30
+
31
+ def rewrite_source(source, filename="<rewritten>"):
32
+ """Parse, rewrite and compile module source; returns a code object."""
33
+ tree = compile(source, filename, "exec", ast.PyCF_ONLY_AST, True)
34
+ rewrite_asserts(tree)
35
+ # dont_inherit: this module's __future__ flags must not leak into the
36
+ # rewritten one
37
+ return compile(tree, filename, "exec", 0, True)
38
+
39
+
40
+ def rewrite_asserts(module):
41
+ """Rewrite the asserts of an ``ast.Module`` in place."""
42
+ if _is_disabled(module):
43
+ return
44
+ ModuleRewriter().run(module)
45
+
46
+
47
+ def _is_disabled(module):
48
+ doc = _docstring(module)
49
+ return doc is not None and any(marker in doc for marker in DONT_REWRITE)
50
+
51
+
52
+ def _docstring(module):
53
+ if not module.body:
54
+ return None
55
+ first = module.body[0]
56
+ if not isinstance(first, ast.Expr):
57
+ return None
58
+ value = first.value
59
+ if PY2:
60
+ return value.s if isinstance(value, ast.Str) else None
61
+ if isinstance(value, ast.Constant) and isinstance(value.value, str):
62
+ return value.value
63
+ return None
64
+
65
+
66
+ # -- node construction that works on both AST flavours ----------------------
67
+
68
+
69
+ def _const(value):
70
+ if not PY2:
71
+ return ast.Constant(value)
72
+ if value is None or isinstance(value, bool):
73
+ return ast.Name(repr(value), ast.Load())
74
+ if isinstance(value, (int, float)):
75
+ return ast.Num(value)
76
+ return ast.Str(value)
77
+
78
+
79
+ def _literal(value):
80
+ """AST for nested tuples and lists of str, int, bool and None."""
81
+ return ast.parse(repr(value), mode="eval").body
82
+
83
+
84
+ def _name(id_, store=False):
85
+ return ast.Name(id_, ast.Store() if store else ast.Load())
86
+
87
+
88
+ def _attr(value, attr):
89
+ return ast.Attribute(value, attr, ast.Load())
90
+
91
+
92
+ def _call(func, args):
93
+ if PY2:
94
+ return ast.Call(func, args, [], None, None)
95
+ return ast.Call(func, args, [])
96
+
97
+
98
+ def _raise(exc):
99
+ if PY2:
100
+ return ast.Raise(exc, None, None)
101
+ return ast.Raise(exc, None)
102
+
103
+
104
+ def _not(expr):
105
+ return ast.UnaryOp(ast.Not(), expr)
106
+
107
+
108
+ def _is_constant(node):
109
+ if isinstance(node, _CONST_NODES):
110
+ return True
111
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd)):
112
+ return _is_constant(node.operand)
113
+ return isinstance(node, ast.Name) and node.id in _NAME_CONSTANTS
114
+
115
+
116
+ # -- scopes -------------------------------------------------------------------
117
+
118
+
119
+ def _function_locals(func):
120
+ """Names local to ``func``: its parameters and everything it binds."""
121
+ names = set()
122
+ args = func.args
123
+ if PY2:
124
+ for arg in args.args:
125
+ names.update(_target_names(arg))
126
+ else:
127
+ for arg in getattr(args, "posonlyargs", []) + args.args + args.kwonlyargs:
128
+ names.add(arg.arg)
129
+ for star in (args.vararg, args.kwarg):
130
+ if star is not None:
131
+ names.add(star if PY2 else star.arg)
132
+ declared_global = set()
133
+ for node in _walk_scope(func.body):
134
+ if isinstance(node, ast.Name) and not isinstance(node.ctx, ast.Load):
135
+ names.add(node.id)
136
+ elif isinstance(node, (ast.Global,) + _NONLOCAL):
137
+ declared_global.update(node.names)
138
+ elif isinstance(node, (ast.Import, ast.ImportFrom)):
139
+ for alias in node.names:
140
+ names.add((alias.asname or alias.name).split(".")[0])
141
+ elif isinstance(node, _SCOPES):
142
+ names.add(node.name)
143
+ elif isinstance(node, ast.ExceptHandler) and isinstance(node.name, str):
144
+ names.add(node.name)
145
+ return names - declared_global
146
+
147
+
148
+ _NONLOCAL = () if PY2 else (ast.Nonlocal,)
149
+ _SCOPES = (ast.FunctionDef, ast.ClassDef) + (() if PY2 else (ast.AsyncFunctionDef,))
150
+
151
+
152
+ def _target_names(node):
153
+ return [n.id for n in ast.walk(node) if isinstance(n, ast.Name)]
154
+
155
+
156
+ def _walk_scope(body):
157
+ """Walk statements without descending into nested scopes."""
158
+ todo = list(body)
159
+ while todo:
160
+ node = todo.pop()
161
+ yield node
162
+ if isinstance(node, _SCOPES + (ast.Lambda,)):
163
+ continue
164
+ todo.extend(ast.iter_child_nodes(node))
165
+
166
+
167
+ # -- the rewriter ------------------------------------------------------------
168
+
169
+
170
+ class ModuleRewriter(object):
171
+ def __init__(self):
172
+ self.sites = []
173
+ self.counter = 0
174
+
175
+ def run(self, module):
176
+ self.rewrite_body(module.body, None)
177
+ if self.sites:
178
+ module.body[_insert_position(module.body) : 0] = self.preamble()
179
+ ast.fix_missing_locations(module)
180
+
181
+ def preamble(self):
182
+ stmts = [
183
+ ast.ImportFrom("cot_assert", [ast.alias("_runtime", RUNTIME)], 0),
184
+ ]
185
+ for name, source, path_labels, shape, paths in self.sites:
186
+ site = _call(
187
+ _attr(_name(RUNTIME), "AssertSite"),
188
+ [
189
+ _const(source),
190
+ _literal(path_labels),
191
+ _literal(shape),
192
+ _literal(paths),
193
+ ],
194
+ )
195
+ stmts.append(ast.Assign([_name(name, store=True)], site))
196
+ for stmt in stmts:
197
+ stmt.lineno = 1
198
+ stmt.col_offset = 0
199
+ return stmts
200
+
201
+ def fresh(self, prefix):
202
+ self.counter += 1
203
+ return "@%s%d" % (prefix, self.counter)
204
+
205
+ def rewrite_body(self, body, local_names):
206
+ """Rewrite asserts in a statement list; ``local_names`` None = module."""
207
+ i = 0
208
+ while i < len(body):
209
+ stmt = body[i]
210
+ if isinstance(stmt, ast.Assert):
211
+ new = AssertRewriter(self, local_names).rewrite(stmt)
212
+ body[i : i + 1] = new
213
+ i += len(new)
214
+ continue
215
+ self.rewrite_children(stmt, local_names)
216
+ i += 1
217
+
218
+ def rewrite_children(self, stmt, local_names):
219
+ if isinstance(stmt, ast.FunctionDef) or (
220
+ not PY2 and isinstance(stmt, ast.AsyncFunctionDef)
221
+ ):
222
+ self.rewrite_body(stmt.body, _function_locals(stmt))
223
+ return
224
+ if isinstance(stmt, ast.ClassDef):
225
+ self.rewrite_body(stmt.body, None)
226
+ return
227
+ for field in ("body", "orelse", "finalbody"):
228
+ block = getattr(stmt, field, None)
229
+ if isinstance(block, list):
230
+ self.rewrite_body(block, local_names)
231
+ # except handlers and match cases hold their own statement lists
232
+ for child in getattr(stmt, "handlers", []) + getattr(stmt, "cases", []):
233
+ self.rewrite_body(child.body, local_names)
234
+
235
+
236
+ def _insert_position(body):
237
+ pos = 0
238
+ if _docstring_node(body):
239
+ pos = 1
240
+ while (
241
+ pos < len(body)
242
+ and isinstance(body[pos], ast.ImportFrom)
243
+ and body[pos].module == "__future__"
244
+ ):
245
+ pos += 1
246
+ return pos
247
+
248
+
249
+ def _docstring_node(body):
250
+ if not body or not isinstance(body[0], ast.Expr):
251
+ return False
252
+ value = body[0].value
253
+ if PY2:
254
+ return isinstance(value, ast.Str)
255
+ return isinstance(value, ast.Constant) and isinstance(value.value, str)
256
+
257
+
258
+ class AssertRewriter(object):
259
+ """Rewrites a single assert statement.
260
+
261
+ Besides the code, it records what the failure explanation needs:
262
+
263
+ - slots: every tracked value gets a slot number and a label;
264
+ - paths: per failure exit, the slots evaluated on the way there (in the
265
+ order their values are passed) and the marks that tell which boolean
266
+ operands ran and which comparisons are known to have failed;
267
+ - shape: a tree of tuples mirroring the expression, which the host
268
+ renders the way pytest's rewriter formats its explanations.
269
+ """
270
+
271
+ def __init__(self, module, local_names):
272
+ self.module = module
273
+ self.local_names = local_names
274
+ self.site_name = module.fresh("cot_site")
275
+ self.labels = []
276
+ self.paths = []
277
+ self.node_ids = 0
278
+
279
+ def rewrite(self, stmt):
280
+ self.msg = stmt.msg
281
+ block = []
282
+ shape = self.check(stmt.test, block, _Path())
283
+ path_labels = [[self.labels[slot] for slot in slots] for slots, _ in self.paths]
284
+ self.module.sites.append(
285
+ (self.site_name, unparse(stmt.test), path_labels, shape, self.paths)
286
+ )
287
+ for new in block:
288
+ ast.copy_location(new, stmt)
289
+ return block
290
+
291
+ def new_id(self):
292
+ self.node_ids += 1
293
+ return self.node_ids
294
+
295
+ # -- failure exits --
296
+
297
+ def fail(self, path):
298
+ number = len(self.paths)
299
+ self.paths.append((list(path.slots), sorted(path.marks)))
300
+ values = ast.List(
301
+ [_call(_attr(_name(RUNTIME), "v"), [expr]) for expr in path.exprs],
302
+ ast.Load(),
303
+ )
304
+ msg = copy.deepcopy(self.msg) if self.msg is not None else _const(None)
305
+ return _raise(
306
+ _call(
307
+ _attr(_name(RUNTIME), "fail"),
308
+ [_name(self.site_name), _const(number), values, msg],
309
+ )
310
+ )
311
+
312
+ def fail_unless(self, test, block, path):
313
+ block.append(ast.If(_not(test), [self.fail(path)], []))
314
+
315
+ # -- statement level: fall through when true, raise when false --
316
+
317
+ def check(self, expr, block, path):
318
+ """Emit the test for ``expr`` into ``block``; return its shape."""
319
+ if isinstance(expr, ast.BoolOp):
320
+ node = self.new_id()
321
+ is_or = isinstance(expr.op, ast.Or)
322
+ shapes = []
323
+ last = len(expr.values) - 1
324
+ for i, value in enumerate(expr.values):
325
+ path.marks.add(("bool", node, i))
326
+ if is_or and i < last:
327
+ # a false operand of ``or`` moves on to the next one
328
+ test, shape = self.cond(value, block, path)
329
+ inner = []
330
+ block.append(ast.If(_not(test), inner, []))
331
+ block = inner
332
+ shapes.append(shape)
333
+ self.mark_false(shape, path)
334
+ else:
335
+ shapes.append(self.check(value, block, path))
336
+ return ("boolop", node, is_or, shapes)
337
+ if isinstance(expr, ast.Compare) and len(expr.ops) > 1:
338
+ node = self.new_id()
339
+ left, left_shape = self.track(expr.left, block, path)
340
+ operands = [left_shape]
341
+ for i, (op, comparator) in enumerate(zip(expr.ops, expr.comparators)):
342
+ right, right_shape = self.track(comparator, block, path)
343
+ operands.append(right_shape)
344
+ test = ast.Compare(left, [op], [right])
345
+ path.marks.add(("cmp", node, i))
346
+ block.append(ast.If(_not(test), [self.fail(path)], []))
347
+ path.marks.discard(("cmp", node, i))
348
+ left = right
349
+ return ("compare", node, [cmpop(op) for op in expr.ops], operands, None)
350
+ test, shape = self.cond(expr, block, path)
351
+ self.mark_false(shape, path)
352
+ self.fail_unless(test, block, path)
353
+ self.unmark_false(shape, path)
354
+ return shape
355
+
356
+ def mark_false(self, shape, path):
357
+ if shape[0] == "compare" and shape[4] is None:
358
+ path.marks.add(("cmp", shape[1], 0))
359
+
360
+ def unmark_false(self, shape, path):
361
+ if shape[0] == "compare" and shape[4] is None:
362
+ path.marks.discard(("cmp", shape[1], 0))
363
+
364
+ def cond(self, expr, block, path):
365
+ """An expression to test, its operands already evaluated into block."""
366
+ if isinstance(expr, ast.Compare) and len(expr.ops) == 1:
367
+ left, left_shape = self.track(expr.left, block, path)
368
+ right, right_shape = self.track(expr.comparators[0], block, path)
369
+ shape = (
370
+ "compare",
371
+ self.new_id(),
372
+ [cmpop(expr.ops[0])],
373
+ [left_shape, right_shape],
374
+ None,
375
+ )
376
+ return ast.Compare(left, expr.ops, [right]), shape
377
+ return self.track(expr, block, path)
378
+
379
+ # -- expression level: evaluate once, remember the value --
380
+
381
+ def slot(self, expr, label, path):
382
+ slot = len(self.labels)
383
+ self.labels.append(label)
384
+ path.slots.append(slot)
385
+ path.exprs.append(expr)
386
+ return slot
387
+
388
+ def track(self, expr, block, path):
389
+ """Evaluate ``expr`` into ``block``; return (value expression, shape)."""
390
+ if _is_constant(expr):
391
+ return expr, ("const", unparse(expr))
392
+ label = unparse(expr)
393
+ if isinstance(expr, ast.Name):
394
+ if self.local_names is None or expr.id in self.local_names:
395
+ return expr, ("name", self.slot(_name(expr.id), label, path), expr.id)
396
+ return expr, ("text", expr.id)
397
+ new, build = self.rebuild(expr, block, path)
398
+ temp = self.module.fresh("cot")
399
+ block.append(ast.Assign([_name(temp, store=True)], new))
400
+ slot = self.slot(_name(temp), label, path)
401
+ return _name(temp), build(slot)
402
+
403
+ def rebuild(self, expr, block, path):
404
+ """``expr`` over tracked sub-values, and a shape builder taking its slot."""
405
+ if isinstance(expr, ast.Attribute) and isinstance(expr.ctx, ast.Load):
406
+ value, value_shape = self.track(expr.value, block, path)
407
+ attr = expr.attr
408
+ return _attr(value, attr), lambda slot: ("attr", slot, value_shape, attr)
409
+ if isinstance(expr, ast.BinOp):
410
+ left, left_shape = self.track(expr.left, block, path)
411
+ right, right_shape = self.track(expr.right, block, path)
412
+ sym = binop(expr.op)
413
+ return (
414
+ ast.BinOp(left, expr.op, right),
415
+ lambda slot: ("binop", sym, left_shape, right_shape),
416
+ )
417
+ if isinstance(expr, ast.UnaryOp):
418
+ operand, operand_shape = self.track(expr.operand, block, path)
419
+ pattern = unaryop(expr.op) + "%s"
420
+ return (
421
+ ast.UnaryOp(expr.op, operand),
422
+ lambda slot: ("unary", pattern, operand_shape),
423
+ )
424
+ if isinstance(expr, ast.Compare) and len(expr.ops) == 1:
425
+ test, shape = self.cond(expr, block, path)
426
+ return test, lambda slot: shape[:4] + (slot,)
427
+ if isinstance(expr, ast.Call):
428
+ return self.rebuild_call(expr, block, path)
429
+ if isinstance(expr, ast.Subscript) and isinstance(expr.ctx, ast.Load):
430
+ value, _ = self.track(expr.value, block, path)
431
+ return ast.Subscript(value, expr.slice, ast.Load()), _repr_shape
432
+ return expr, _repr_shape
433
+
434
+ def rebuild_call(self, call, block, path):
435
+ func = call.func
436
+ if isinstance(func, ast.Attribute):
437
+ # keep the method call shape: a bound method in a temporary is
438
+ # something RPython would have to annotate as a value
439
+ obj, obj_shape = self.track(func.value, block, path)
440
+ func_shape = ("method", obj_shape, func.attr)
441
+ func = _attr(obj, func.attr)
442
+ else:
443
+ func, func_shape = self.track(func, block, path)
444
+ args = []
445
+ arg_shapes = []
446
+ for arg in call.args:
447
+ if not PY2 and isinstance(arg, ast.Starred):
448
+ value, shape = self.track(arg.value, block, path)
449
+ args.append(ast.Starred(value, ast.Load()))
450
+ arg_shapes.append(("*", shape))
451
+ else:
452
+ value, shape = self.track(arg, block, path)
453
+ args.append(value)
454
+ arg_shapes.append(("", shape))
455
+ starargs = kwargs = None
456
+ if PY2 and call.starargs is not None:
457
+ starargs, shape = self.track(call.starargs, block, path)
458
+ arg_shapes.append(("*", shape))
459
+ keywords = []
460
+ for kw in call.keywords:
461
+ value, shape = self.track(kw.value, block, path)
462
+ keywords.append(ast.keyword(kw.arg, value))
463
+ arg_shapes.append(("**" if kw.arg is None else kw.arg + "=", shape))
464
+ if PY2 and call.kwargs is not None:
465
+ kwargs, shape = self.track(call.kwargs, block, path)
466
+ arg_shapes.append(("**", shape))
467
+ if PY2:
468
+ new = ast.Call(func, args, keywords, starargs, kwargs)
469
+ else:
470
+ new = ast.Call(func, args, keywords)
471
+ return new, lambda slot: ("call", slot, func_shape, arg_shapes)
472
+
473
+
474
+ def _repr_shape(slot):
475
+ return ("repr", slot)
476
+
477
+
478
+ class _Path(object):
479
+ """What has been evaluated along the code path being emitted.
480
+
481
+ Failure exits only ever come after the code of everything they report,
482
+ and short-circuits only nest, so one growing record per assert is enough.
483
+ """
484
+
485
+ def __init__(self):
486
+ self.slots = []
487
+ self.exprs = []
488
+ self.marks = set()
489
+
490
+
491
+ def load_source(source, name="rewritten"):
492
+ """Rewrite and execute module source; return its namespace."""
493
+ namespace = {"__name__": name}
494
+ exec(rewrite_source(source, "<%s>" % name), namespace)
495
+ return namespace
cot_assert/_rpy.py ADDED
@@ -0,0 +1,32 @@
1
+ """RPython primitives, or inert stand-ins when rpython is not importable.
2
+
3
+ Code in the RPython subset imports these from here, never from rpython
4
+ directly, so the package works on hosts without an rpython checkout.
5
+ """
6
+
7
+ from __future__ import absolute_import, division, print_function
8
+
9
+ try:
10
+ from rpython.rlib.objectmodel import specialize, we_are_translated
11
+ from rpython.rlib.rfloat import DTSF_ADD_DOT_0, formatd
12
+
13
+ def float_repr(x):
14
+ # str() of an RPython float is "%f"; this matches the host's repr
15
+ return formatd(x, "r", 0, DTSF_ADD_DOT_0)
16
+
17
+ except ImportError:
18
+ float_repr = repr
19
+
20
+ def we_are_translated():
21
+ return False
22
+
23
+ class _Specialize(object):
24
+ def __getattr__(self, name):
25
+ def decorator_factory(*args, **kwargs):
26
+ return lambda func: func
27
+
28
+ return decorator_factory
29
+
30
+ specialize = _Specialize()
31
+
32
+ __all__ = ["float_repr", "specialize", "we_are_translated"]
cot_assert/_runtime.py ADDED
@@ -0,0 +1,12 @@
1
+ """Names the rewritten code calls; imported into rewritten modules."""
2
+
3
+ from __future__ import absolute_import, division, print_function
4
+
5
+ from ._error import AssertSite, annotated
6
+ from ._values import value as v
7
+
8
+ __all__ = ["AssertSite", "fail", "v"]
9
+
10
+
11
+ def fail(site, path, values, msg):
12
+ return annotated(msg, site, path, values)