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/_unparse.py ADDED
@@ -0,0 +1,180 @@
1
+ """Expression source text for labels, identical on Python 2.7 and 3.x.
2
+
3
+ ``ast.unparse`` does not exist on 2.7 and its output drifts between 3.x
4
+ releases; labels end up in prebuilt RPython constants and in test
5
+ expectations, so they come from here on every version.
6
+ """
7
+
8
+ from __future__ import absolute_import, division, print_function
9
+
10
+ _BINOP = {
11
+ "Add": ("+", 11),
12
+ "Sub": ("-", 11),
13
+ "Mult": ("*", 12),
14
+ "MatMult": ("@", 12),
15
+ "Div": ("/", 12),
16
+ "FloorDiv": ("//", 12),
17
+ "Mod": ("%", 12),
18
+ "Pow": ("**", 14),
19
+ "LShift": ("<<", 10),
20
+ "RShift": (">>", 10),
21
+ "BitOr": ("|", 7),
22
+ "BitXor": ("^", 8),
23
+ "BitAnd": ("&", 9),
24
+ }
25
+ _UNARY = {"Not": ("not ", 4), "Invert": ("~", 13), "USub": ("-", 13), "UAdd": ("+", 13)}
26
+ _CMP = {
27
+ "Eq": "==",
28
+ "NotEq": "!=",
29
+ "Lt": "<",
30
+ "LtE": "<=",
31
+ "Gt": ">",
32
+ "GtE": ">=",
33
+ "Is": "is",
34
+ "IsNot": "is not",
35
+ "In": "in",
36
+ "NotIn": "not in",
37
+ }
38
+ _BOOL = {"And": ("and", 3), "Or": ("or", 2)}
39
+ _ATOM = 16
40
+
41
+
42
+ def cmpop(op):
43
+ return _CMP[type(op).__name__]
44
+
45
+
46
+ def binop(op):
47
+ return _BINOP[type(op).__name__][0]
48
+
49
+
50
+ def unaryop(op):
51
+ return _UNARY[type(op).__name__][0]
52
+
53
+
54
+ def unparse(node):
55
+ return _unparse(node)[0]
56
+
57
+
58
+ def _wrap(node, minimum):
59
+ text, prec = _unparse(node)
60
+ if prec < minimum:
61
+ return "(" + text + ")"
62
+ return text
63
+
64
+
65
+ def _unparse(node):
66
+ kind = type(node).__name__
67
+ if kind == "Name":
68
+ return node.id, _ATOM
69
+ if kind in ("Constant", "NameConstant"):
70
+ if node.value is Ellipsis:
71
+ return "...", _ATOM
72
+ return repr(node.value), _ATOM
73
+ if kind == "Num":
74
+ return repr(node.n), _ATOM
75
+ if kind in ("Str", "Bytes"):
76
+ return repr(node.s), _ATOM
77
+ if kind == "Attribute":
78
+ return _wrap(node.value, _ATOM) + "." + node.attr, _ATOM
79
+ if kind == "Call":
80
+ return _wrap(node.func, _ATOM) + "(" + ", ".join(_call_args(node)) + ")", _ATOM
81
+ if kind == "Subscript":
82
+ return _wrap(node.value, _ATOM) + "[" + _slice(node.slice) + "]", _ATOM
83
+ if kind == "BinOp":
84
+ op, prec = _BINOP[type(node.op).__name__]
85
+ # ** is right associative, everything else left
86
+ if op == "**":
87
+ left, right = _wrap(node.left, prec + 1), _wrap(node.right, prec)
88
+ else:
89
+ left, right = _wrap(node.left, prec), _wrap(node.right, prec + 1)
90
+ return left + " " + op + " " + right, prec
91
+ if kind == "UnaryOp":
92
+ op, prec = _UNARY[type(node.op).__name__]
93
+ return op + _wrap(node.operand, prec), prec
94
+ if kind == "Compare":
95
+ parts = [_wrap(node.left, 6)]
96
+ for op, comparator in zip(node.ops, node.comparators):
97
+ parts.append(cmpop(op))
98
+ parts.append(_wrap(comparator, 6))
99
+ return " ".join(parts), 5
100
+ if kind == "BoolOp":
101
+ op, prec = _BOOL[type(node.op).__name__]
102
+ return (" " + op + " ").join(_wrap(v, prec + 1) for v in node.values), prec
103
+ if kind == "IfExp":
104
+ return (
105
+ _wrap(node.body, 2)
106
+ + " if "
107
+ + _wrap(node.test, 2)
108
+ + " else "
109
+ + _wrap(node.orelse, 1)
110
+ ), 1
111
+ if kind == "Lambda":
112
+ return "lambda: ...", 0
113
+ if kind == "Tuple":
114
+ items = [unparse(e) for e in node.elts]
115
+ if len(items) == 1:
116
+ return "(" + items[0] + ",)", _ATOM
117
+ return "(" + ", ".join(items) + ")", _ATOM
118
+ if kind == "List":
119
+ return "[" + ", ".join(unparse(e) for e in node.elts) + "]", _ATOM
120
+ if kind == "Set":
121
+ return "{" + ", ".join(unparse(e) for e in node.elts) + "}", _ATOM
122
+ if kind == "Dict":
123
+ items = []
124
+ for k, v in zip(node.keys, node.values):
125
+ if k is None:
126
+ items.append("**" + _wrap(v, _ATOM))
127
+ else:
128
+ items.append(unparse(k) + ": " + unparse(v))
129
+ return "{" + ", ".join(items) + "}", _ATOM
130
+ if kind == "Starred":
131
+ return "*" + _wrap(node.value, _ATOM), _ATOM
132
+ if kind == "Repr":
133
+ return "`" + unparse(node.value) + "`", _ATOM
134
+ if kind in ("ListComp", "SetComp", "GeneratorExp", "DictComp"):
135
+ return {"ListComp": "[...]", "SetComp": "{...}", "DictComp": "{...}"}.get(
136
+ kind, "(...)"
137
+ ), _ATOM
138
+ if kind == "NamedExpr":
139
+ return unparse(node.target) + " := " + _wrap(node.value, 1), 0
140
+ return "<%s>" % kind, _ATOM
141
+
142
+
143
+ def _call_args(node):
144
+ args = [unparse(a) for a in node.args]
145
+ starargs = getattr(node, "starargs", None)
146
+ if starargs is not None:
147
+ args.append("*" + unparse(starargs))
148
+ for kw in node.keywords:
149
+ if kw.arg is None:
150
+ args.append("**" + unparse(kw.value))
151
+ else:
152
+ args.append(kw.arg + "=" + unparse(kw.value))
153
+ kwargs = getattr(node, "kwargs", None)
154
+ if kwargs is not None:
155
+ args.append("**" + unparse(kwargs))
156
+ return args
157
+
158
+
159
+ def _slice(node):
160
+ kind = type(node).__name__
161
+ if kind == "Index":
162
+ return _slice(node.value)
163
+ if kind == "Slice":
164
+ text = ""
165
+ if node.lower is not None:
166
+ text += unparse(node.lower)
167
+ text += ":"
168
+ if node.upper is not None:
169
+ text += unparse(node.upper)
170
+ if node.step is not None:
171
+ text += ":" + unparse(node.step)
172
+ return text
173
+ if kind == "ExtSlice":
174
+ return ", ".join(_slice(d) for d in node.dims)
175
+ if kind == "Tuple" and node.elts:
176
+ return ", ".join(_slice(e) for e in node.elts)
177
+ return unparse(node)
178
+
179
+
180
+ __all__ = ["binop", "cmpop", "unaryop", "unparse"]
cot_assert/_values.py ADDED
@@ -0,0 +1,30 @@
1
+ """Capturing values at the point an assert fails."""
2
+
3
+ from __future__ import absolute_import, division, print_function
4
+
5
+ from ._rpy import float_repr, specialize, we_are_translated
6
+
7
+
8
+ @specialize.argtype(0)
9
+ def value(obj):
10
+ """Host: the object itself. Translated: its RPython-level repr."""
11
+ if we_are_translated():
12
+ return rpy_repr(obj)
13
+ return obj
14
+
15
+
16
+ @specialize.argtype(0)
17
+ def rpy_repr(obj):
18
+ if obj is None:
19
+ return "None"
20
+ if isinstance(obj, bool):
21
+ if obj:
22
+ return "True"
23
+ return "False"
24
+ if isinstance(obj, int):
25
+ return str(obj)
26
+ if isinstance(obj, float):
27
+ return float_repr(obj)
28
+ if isinstance(obj, str):
29
+ return "'" + obj + "'"
30
+ return "<object>"
@@ -0,0 +1,140 @@
1
+ """pytest plugin: cot_assert in place of pytest's assertion rewriter.
2
+
3
+ Off unless enabled with ``--cot-assert`` or ``cot_assert = true`` in the ini
4
+ file. pytest keeps choosing which files to rewrite (test files, conftests,
5
+ ``register_assert_rewrite``) and keeps caching them; only the rewriting
6
+ itself is swapped, and the cache name changes so neither rewriter picks up
7
+ the other's bytecode. Works with pytest 4.6 (Python 2.7) and current pytest.
8
+ """
9
+
10
+ from __future__ import absolute_import, division, print_function
11
+
12
+ import pytest
13
+
14
+ from . import _hook, _render
15
+ from ._error import AnnotatedAssertion
16
+ from ._rewrite import rewrite_asserts
17
+
18
+ FINALIZE_MODES = ("message", "notes")
19
+
20
+
21
+ def pytest_addoption(parser):
22
+ group = parser.getgroup("cot_assert")
23
+ group.addoption(
24
+ "--cot-assert",
25
+ action="store_true",
26
+ default=None,
27
+ help="rewrite asserts with cot_assert instead of pytest's rewriter",
28
+ )
29
+ parser.addini(
30
+ "cot_assert",
31
+ type="bool",
32
+ default=False,
33
+ help="rewrite asserts with cot_assert instead of pytest's rewriter",
34
+ )
35
+ parser.addini(
36
+ "cot_assert_finalize",
37
+ default="message",
38
+ help="where a failed assert's explanation goes: message or notes",
39
+ )
40
+
41
+
42
+ def _enabled(config):
43
+ # during pytest_load_initial_conftests only the early parse has run
44
+ option = getattr(config.known_args_namespace, "cot_assert", None)
45
+ if option is not None:
46
+ return option
47
+ return config.getini("cot_assert")
48
+
49
+
50
+ @pytest.hookimpl(tryfirst=True)
51
+ def pytest_load_initial_conftests(early_config, parser, args):
52
+ # before pytest imports (and rewrites) the first conftest
53
+ if not _enabled(early_config):
54
+ return
55
+ mode = early_config.getini("cot_assert_finalize")
56
+ if mode not in FINALIZE_MODES:
57
+ raise pytest.UsageError(
58
+ "cot_assert_finalize must be one of %s, not %r"
59
+ % (", ".join(FINALIZE_MODES), mode)
60
+ )
61
+ patch = _PytestPatch(mode)
62
+ patch.apply()
63
+ early_config.pluginmanager.register(patch, "cot_assert_active")
64
+ early_config.add_cleanup(patch.undo)
65
+
66
+
67
+ class _PytestPatch(object):
68
+ def __init__(self, finalize_mode):
69
+ self.finalize_mode = finalize_mode
70
+ self.saved = []
71
+
72
+ def apply(self):
73
+ from _pytest.assertion import rewrite as pytest_rewrite
74
+
75
+ self._set(pytest_rewrite, "rewrite_asserts", _pytest_rewrite_asserts)
76
+ self._set(
77
+ pytest_rewrite,
78
+ "PYC_TAIL",
79
+ pytest_rewrite.PYC_TAIL.replace(
80
+ pytest_rewrite.PYC_EXT, "-" + _hook.CACHE_TAG + pytest_rewrite.PYC_EXT
81
+ ),
82
+ )
83
+ self.previous_formatter = _render.set_formatter(PytestFormatter())
84
+
85
+ def _set(self, obj, name, value):
86
+ self.saved.append((obj, name, getattr(obj, name)))
87
+ setattr(obj, name, value)
88
+
89
+ def undo(self):
90
+ while self.saved:
91
+ obj, name, value = self.saved.pop()
92
+ setattr(obj, name, value)
93
+ _render.set_formatter(self.previous_formatter)
94
+
95
+ @pytest.hookimpl(hookwrapper=True)
96
+ def pytest_runtest_makereport(self, item, call):
97
+ # render while pytest's comparison hook and config are still set,
98
+ # before the report turns the exception into text
99
+ excinfo = call.excinfo
100
+ if excinfo is not None and isinstance(excinfo.value, AnnotatedAssertion):
101
+ excinfo.value.finalize(self.finalize_mode)
102
+ yield
103
+
104
+ def pytest_report_header(self, config):
105
+ return "cot_assert: rewriting asserts (finalize into %s)" % (
106
+ self.finalize_mode,
107
+ )
108
+
109
+
110
+ def _pytest_rewrite_asserts(mod, *args, **kwargs):
111
+ # pytest 4.6: (mod, module_path, config); current: (mod, source, path, config)
112
+ rewrite_asserts(mod)
113
+
114
+
115
+ class PytestFormatter(_render.Formatter):
116
+ """Formatting through pytest's own helpers, as its rewriter does."""
117
+
118
+ def saferepr(self, obj):
119
+ from _pytest.assertion.rewrite import _saferepr
120
+
121
+ return _saferepr(obj)
122
+
123
+ def format_assertmsg(self, obj):
124
+ from _pytest.assertion.rewrite import _format_assertmsg
125
+
126
+ # pytest doubles % for the %-formatting its template goes through;
127
+ # nothing formats this text again
128
+ return _format_assertmsg(obj).replace("%%", "%")
129
+
130
+ def format_explanation(self, explanation):
131
+ from _pytest.assertion.util import format_explanation
132
+
133
+ return format_explanation(explanation)
134
+
135
+ def reprcompare(self, op, left, right):
136
+ from _pytest.assertion import util
137
+
138
+ if util._reprcompare is None:
139
+ return None
140
+ return util._reprcompare(op, left, right)
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.5
2
+ Name: cot-assert
3
+ Version: 0.1.0
4
+ Summary: assertion rewriting with a structured AssertionError, for Python 2.7, 3.x and RPython
5
+ Author-email: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
6
+ License-Expression: MPL-2.0
7
+ License-File: LICENSE
8
+ Keywords: assert,pytest,rpython
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Framework :: Pytest
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 2
13
+ Classifier: Programming Language :: Python :: 2.7
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,!=3.8.*,>=2.7
23
+ Description-Content-Type: text/markdown
24
+
25
+ # cot-assert
26
+
27
+ Assertion rewriting that raises a structured `AnnotatedAssertion`, for Python
28
+ 2.7, Python 3.9+ and RPython. Intended as a drop-in replacement for pytest's
29
+ assertion rewriting on current pytest and on pytest 4.6.
30
+
31
+ Pre-alpha.
32
+
33
+ ```
34
+ pip install cot-assert
35
+ ```
36
+
37
+ One universal wheel serves Python 2.7 (pip 20 and later, CPython or PyPy)
38
+ and Python 3.9 and later. It has no dependencies.
39
+
40
+ ## With pytest
41
+
42
+ Installing the package registers a pytest plugin that stays off until
43
+ enabled:
44
+
45
+ ```ini
46
+ [pytest]
47
+ cot_assert = true
48
+ # where a failed assert's explanation goes: message (default) or notes
49
+ cot_assert_finalize = message
50
+ ```
51
+
52
+ or `pytest --cot-assert`. On Python 2.7, where it runs from source, load it
53
+ with `-p cot_assert.pytest_plugin`.
54
+
55
+ pytest still decides which modules are rewritten (test files, conftests,
56
+ `pytest.register_assert_rewrite`) and caches them under a separate name;
57
+ cot_assert does the rewriting, and failures render through pytest's own
58
+ comparison hooks. Failures show as `AnnotatedAssertion` instead of
59
+ `AssertionError`. `--assert=plain` turns rewriting off for both.
60
+
61
+ ## Without pytest
62
+
63
+ ```python
64
+ import cot_assert
65
+
66
+ cot_assert.install(["mypackage", "test_*"])
67
+ ```
68
+
69
+ rewrites modules imported afterwards whose name matches.
@@ -0,0 +1,17 @@
1
+ cot_assert/__init__.py,sha256=cEb8TqnJ1XTtAFuAAxnDq7fpFX7MZ_cr16q0Sudbrik,582
2
+ cot_assert/_entry.py,sha256=x-a-3vo6uRDgK27X7EcPIKNyvt50viHuG1HNeE2G6xE,983
3
+ cot_assert/_error.py,sha256=L_Y52SFTPjWvwxPnIUZI-dKyD1725XPBWZ5M0YwDOkc,5425
4
+ cot_assert/_hook.py,sha256=JVA6A7_bHMQpQ_zUJoQyzmcXD6aota-9rmsc-1GJToc,7332
5
+ cot_assert/_llexc.py,sha256=VftW_Ktg-tXs5qXoR2SA04RVfGilRsQEwsbHqKRphFI,2582
6
+ cot_assert/_render.py,sha256=8C1wUUVlj9XRber8zxDIN3T1BivhQVr4WGZxnRIitks,9682
7
+ cot_assert/_rewrite.py,sha256=g1n0kWm6sbXJXldwp5jYi0YzypILBLBwDbst_NAQq6s,17613
8
+ cot_assert/_rpy.py,sha256=NC3d5VMqUgrg-nJTdPPPIPPaZquKPRUmcd8wWWB1VN8,957
9
+ cot_assert/_runtime.py,sha256=3_BimWc_RfkVZCwIUGTOc2ndfQawkUcWEFpUbekhE-o,334
10
+ cot_assert/_unparse.py,sha256=Yq1oGl07BQ3HcleCpx_920PU6vPoZP183iPUe29Dmgo,5541
11
+ cot_assert/_values.py,sha256=IMmaZUoE_ioyuFZQU_H4qo55QgAGGL2l1A_fU9ovzu8,742
12
+ cot_assert/pytest_plugin.py,sha256=v4xGIxgkjUHgR7SYIXlwHGGo9PJnx0WAeRrf8syutLM,4637
13
+ cot_assert-0.1.0.dist-info/METADATA,sha256=SSLQsZ0ZnZXoTZRn131SRuoo5A2M2kbRSw6dfw221CI,2312
14
+ cot_assert-0.1.0.dist-info/WHEEL,sha256=DnXx7cBEyVTTMvhePCuz2Now68IkDfyaQ4wNff8GnEk,105
15
+ cot_assert-0.1.0.dist-info/entry_points.txt,sha256=cyZBmXtGq7G82iJQy0Mhmqrq3gui0OkDetFeObZrXaM,49
16
+ cot_assert-0.1.0.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
17
+ cot_assert-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ cot_assert = cot_assert.pytest_plugin