From 250160b4b061e98d77db6e03c673c8c2ddac2722 Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Thu, 26 May 2011 12:01:34 -0500 Subject: [PATCH] refactor explanation formatting things into their own module --- _pytest/assertion/__init__.py | 188 +----------------------------- _pytest/assertion/newinterpret.py | 9 +- _pytest/assertion/oldinterpret.py | 4 +- _pytest/assertion/rewrite.py | 20 ++-- testing/test_assertinterpret.py | 4 +- testing/test_assertion.py | 10 +- testing/test_assertrewrite.py | 22 ++-- 7 files changed, 37 insertions(+), 220 deletions(-) diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index 9f89d17fc..c1c8f3be1 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -7,6 +7,7 @@ import marshal import struct import sys from _pytest.monkeypatch import monkeypatch +from _pytest.assertion import reinterpret, util try: from _pytest.assertion.rewrite import rewrite_asserts @@ -31,7 +32,6 @@ def pytest_configure(config): config._cleanup.append(m.undo) warn_about_missing_assertion() if not config.getvalue("noassert") and not config.getvalue("nomagic"): - from _pytest.assertion import reinterpret def callbinrepr(op, left, right): hook_result = config.hook.pytest_assertrepr_compare( config=config, op=op, left=left, right=right) @@ -40,7 +40,7 @@ def pytest_configure(config): return '\n~'.join(new_expl) m.setattr(py.builtin.builtins, 'AssertionError', reinterpret.AssertionError) - m.setattr(sys.modules[__name__], '_reprcompare', callbinrepr) + m.setattr(util, '_reprcompare', callbinrepr) else: rewrite_asserts = None @@ -99,186 +99,4 @@ def warn_about_missing_assertion(): sys.stderr.write("WARNING: failing tests may report as passing because " "assertions are turned off! (are you using python -O?)\n") -# if set, will be called by assert reinterp for comparison ops -_reprcompare = None - -def _format_explanation(explanation): - """This formats an explanation - - Normally all embedded newlines are escaped, however there are - three exceptions: \n{, \n} and \n~. The first two are intended - cover nested explanations, see function and attribute explanations - for examples (.visit_Call(), visit_Attribute()). The last one is - for when one explanation needs to span multiple lines, e.g. when - displaying diffs. - """ - raw_lines = (explanation or '').split('\n') - # escape newlines not followed by {, } and ~ - lines = [raw_lines[0]] - for l in raw_lines[1:]: - if l.startswith('{') or l.startswith('}') or l.startswith('~'): - lines.append(l) - else: - lines[-1] += '\\n' + l - - result = lines[:1] - stack = [0] - stackcnt = [0] - for line in lines[1:]: - if line.startswith('{'): - if stackcnt[-1]: - s = 'and ' - else: - s = 'where ' - stack.append(len(result)) - stackcnt[-1] += 1 - stackcnt.append(0) - result.append(' +' + ' '*(len(stack)-1) + s + line[1:]) - elif line.startswith('}'): - assert line.startswith('}') - stack.pop() - stackcnt.pop() - result[stack[-1]] += line[1:] - else: - assert line.startswith('~') - result.append(' '*len(stack) + line[1:]) - assert len(stack) == 1 - return '\n'.join(result) - - -# Provide basestring in python3 -try: - basestring = basestring -except NameError: - basestring = str - - -def pytest_assertrepr_compare(op, left, right): - """return specialised explanations for some operators/operands""" - width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op - left_repr = py.io.saferepr(left, maxsize=int(width/2)) - right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) - summary = '%s %s %s' % (left_repr, op, right_repr) - - issequence = lambda x: isinstance(x, (list, tuple)) - istext = lambda x: isinstance(x, basestring) - isdict = lambda x: isinstance(x, dict) - isset = lambda x: isinstance(x, set) - - explanation = None - try: - if op == '==': - if istext(left) and istext(right): - explanation = _diff_text(left, right) - elif issequence(left) and issequence(right): - explanation = _compare_eq_sequence(left, right) - elif isset(left) and isset(right): - explanation = _compare_eq_set(left, right) - elif isdict(left) and isdict(right): - explanation = _diff_text(py.std.pprint.pformat(left), - py.std.pprint.pformat(right)) - elif op == 'not in': - if istext(left) and istext(right): - explanation = _notin_text(left, right) - except py.builtin._sysex: - raise - except: - excinfo = py.code.ExceptionInfo() - explanation = ['(pytest_assertion plugin: representation of ' - 'details failed. Probably an object has a faulty __repr__.)', - str(excinfo) - ] - - - if not explanation: - return None - - # Don't include pageloads of data, should be configurable - if len(''.join(explanation)) > 80*8: - explanation = ['Detailed information too verbose, truncated'] - - return [summary] + explanation - - -def _diff_text(left, right): - """Return the explanation for the diff between text - - This will skip leading and trailing characters which are - identical to keep the diff minimal. - """ - explanation = [] - i = 0 # just in case left or right has zero length - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - break - if i > 42: - i -= 10 # Provide some context - explanation = ['Skipping %s identical ' - 'leading characters in diff' % i] - left = left[i:] - right = right[i:] - if len(left) == len(right): - for i in range(len(left)): - if left[-i] != right[-i]: - break - if i > 42: - i -= 10 # Provide some context - explanation += ['Skipping %s identical ' - 'trailing characters in diff' % i] - left = left[:-i] - right = right[:-i] - explanation += [line.strip('\n') - for line in py.std.difflib.ndiff(left.splitlines(), - right.splitlines())] - return explanation - - -def _compare_eq_sequence(left, right): - explanation = [] - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - explanation += ['At index %s diff: %r != %r' % - (i, left[i], right[i])] - break - if len(left) > len(right): - explanation += ['Left contains more items, ' - 'first extra item: %s' % py.io.saferepr(left[len(right)],)] - elif len(left) < len(right): - explanation += ['Right contains more items, ' - 'first extra item: %s' % py.io.saferepr(right[len(left)],)] - return explanation # + _diff_text(py.std.pprint.pformat(left), - # py.std.pprint.pformat(right)) - - -def _compare_eq_set(left, right): - explanation = [] - diff_left = left - right - diff_right = right - left - if diff_left: - explanation.append('Extra items in the left set:') - for item in diff_left: - explanation.append(py.io.saferepr(item)) - if diff_right: - explanation.append('Extra items in the right set:') - for item in diff_right: - explanation.append(py.io.saferepr(item)) - return explanation - - -def _notin_text(term, text): - index = text.find(term) - head = text[:index] - tail = text[index+len(term):] - correct_text = head + tail - diff = _diff_text(correct_text, text) - newdiff = ['%s is contained here:' % py.io.saferepr(term, maxsize=42)] - for line in diff: - if line.startswith('Skipping'): - continue - if line.startswith('- '): - continue - if line.startswith('+ '): - newdiff.append(' ' + line[2:]) - else: - newdiff.append(line) - return newdiff +pytest_assertrepr_compare = util.assertrepr_compare diff --git a/_pytest/assertion/newinterpret.py b/_pytest/assertion/newinterpret.py index 1d061aa46..c6e2dea17 100644 --- a/_pytest/assertion/newinterpret.py +++ b/_pytest/assertion/newinterpret.py @@ -7,8 +7,7 @@ import sys import ast import py -from _pytest import assertion -from _pytest.assertion import _format_explanation +from _pytest.assertion import util from _pytest.assertion.reinterpret import BuiltinAssertionError @@ -62,7 +61,7 @@ def run(offending_line, frame=None): return interpret(offending_line, frame) def getfailure(failure): - explanation = _format_explanation(failure.explanation) + explanation = util.format_explanation(failure.explanation) value = failure.cause[1] if str(value): lines = explanation.splitlines() @@ -185,8 +184,8 @@ class DebugInterpreter(ast.NodeVisitor): break left_explanation, left_result = next_explanation, next_result - if assertion._reprcompare is not None: - res = assertion._reprcompare(op_symbol, left_result, next_result) + if util._reprcompare is not None: + res = util._reprcompare(op_symbol, left_result, next_result) if res: explanation = res return explanation, result diff --git a/_pytest/assertion/oldinterpret.py b/_pytest/assertion/oldinterpret.py index 3e8f1c0b3..0c91558a1 100644 --- a/_pytest/assertion/oldinterpret.py +++ b/_pytest/assertion/oldinterpret.py @@ -1,7 +1,7 @@ import py import sys, inspect from compiler import parse, ast, pycodegen -from _pytest.assertion import _format_explanation +from _pytest.assertion.util import format_explanation from _pytest.assertion.reinterpret import BuiltinAssertionError passthroughex = py.builtin._sysex @@ -132,7 +132,7 @@ class Interpretable(View): raise Failure(self) def nice_explanation(self): - return _format_explanation(self.explanation) + return format_explanation(self.explanation) class Name(Interpretable): diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 186d2425e..7e18f2c30 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -5,6 +5,7 @@ import collections import itertools import py +from _pytest.assertion import util def rewrite_asserts(mod): @@ -13,6 +14,7 @@ def rewrite_asserts(mod): _saferepr = py.io.saferepr +from _pytest.assertion.util import format_explanation as _format_explanation def _format_boolop(operands, explanations, is_or): show_explanations = [] @@ -30,9 +32,8 @@ def _call_reprcompare(ops, results, expls, each_obj): done = True if done: break - from _pytest.assertion import _reprcompare - if _reprcompare is not None: - custom = _reprcompare(ops[i], each_obj[i], each_obj[i + 1]) + if util._reprcompare is not None: + custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) if custom is not None: return custom return expl @@ -94,7 +95,6 @@ class AssertionRewriter(ast.NodeVisitor): # Insert some special imports at the top of the module but after any # docstrings and __future__ imports. aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), - ast.alias("_pytest.assertion", "@pytest_a"), ast.alias("_pytest.assertion.rewrite", "@pytest_ar")] expect_docstring = True pos = 0 @@ -153,11 +153,11 @@ class AssertionRewriter(ast.NodeVisitor): def display(self, expr): """Call py.io.saferepr on the expression.""" - return self.helper("ar", "saferepr", expr) + return self.helper("saferepr", expr) - def helper(self, mod, name, *args): + def helper(self, name, *args): """Call a helper in this module.""" - py_name = ast.Name("@pytest_" + mod, ast.Load()) + py_name = ast.Name("@pytest_ar", ast.Load()) attr = ast.Attribute(py_name, "_" + name, ast.Load()) return ast.Call(attr, list(args), [], None, None) @@ -211,7 +211,7 @@ class AssertionRewriter(ast.NodeVisitor): explanation = "assert " + explanation template = ast.Str(explanation) msg = self.pop_format_context(template) - fmt = self.helper("a", "format_explanation", msg) + fmt = self.helper("format_explanation", msg) body.append(ast.Assert(top_condition, fmt)) # Delete temporary variables. names = [ast.Name(name, ast.Del()) for name in self.variables] @@ -242,7 +242,7 @@ class AssertionRewriter(ast.NodeVisitor): explanations.append(explanation) expls = ast.Tuple([ast.Str(expl) for expl in explanations], ast.Load()) is_or = ast.Num(isinstance(boolop.op, ast.Or)) - expl_template = self.helper("ar", "format_boolop", + expl_template = self.helper("format_boolop", ast.Tuple(operands, ast.Load()), expls, is_or) expl = self.pop_format_context(expl_template) @@ -321,7 +321,7 @@ class AssertionRewriter(ast.NodeVisitor): self.statements.append(ast.Assign([store_names[i]], res_expr)) left_res, left_expl = next_res, next_expl # Use py.code._reprcompare if that's available. - expl_call = self.helper("ar", "call_reprcompare", + expl_call = self.helper("call_reprcompare", ast.Tuple(syms, ast.Load()), ast.Tuple(load_names, ast.Load()), ast.Tuple(expls, ast.Load()), diff --git a/testing/test_assertinterpret.py b/testing/test_assertinterpret.py index 318516eae..316cf49d4 100644 --- a/testing/test_assertinterpret.py +++ b/testing/test_assertinterpret.py @@ -1,7 +1,7 @@ "PYTEST_DONT_REWRITE" import pytest, py -from _pytest import assertion +from _pytest.assertion import util def exvalue(): return py.std.sys.exc_info()[1] @@ -249,7 +249,7 @@ class TestView: @py.test.mark.skipif("sys.version_info < (2,6)") def test_assert_customizable_reprcompare(monkeypatch): - monkeypatch.setattr(assertion, '_reprcompare', lambda *args: 'hello') + monkeypatch.setattr(util, '_reprcompare', lambda *args: 'hello') try: assert 3 == 4 except AssertionError: diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 5470f6416..24b665066 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -2,7 +2,7 @@ import sys import py, pytest import _pytest.assertion as plugin -from _pytest.assertion import reinterpret +from _pytest.assertion import reinterpret, util needsnewassert = pytest.mark.skipif("sys.version_info < (2,6)") @@ -26,7 +26,7 @@ class TestBinReprIntegration: self.right = right mockhook = MockHook() monkeypatch = request.getfuncargvalue("monkeypatch") - monkeypatch.setattr(plugin, '_reprcompare', mockhook) + monkeypatch.setattr(util, '_reprcompare', mockhook) return mockhook def test_pytest_assertrepr_compare_called(self, hook): @@ -41,13 +41,13 @@ class TestBinReprIntegration: assert hook.right == [0, 2] def test_configure_unconfigure(self, testdir, hook): - assert hook == plugin._reprcompare + assert hook == util._reprcompare config = testdir.parseconfig() plugin.pytest_configure(config) - assert hook != plugin._reprcompare + assert hook != util._reprcompare from _pytest.config import pytest_unconfigure pytest_unconfigure(config) - assert hook == plugin._reprcompare + assert hook == util._reprcompare def callequal(left, right): return plugin.pytest_assertrepr_compare('==', left, right) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index d713b6e25..ffb544fdd 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -4,16 +4,16 @@ import pytest ast = pytest.importorskip("ast") -from _pytest import assertion +from _pytest.assertion import util from _pytest.assertion.rewrite import rewrite_asserts def setup_module(mod): - mod._old_reprcompare = assertion._reprcompare + mod._old_reprcompare = util._reprcompare py.code._reprcompare = None def teardown_module(mod): - assertion._reprcompare = mod._old_reprcompare + util._reprcompare = mod._old_reprcompare del mod._old_reprcompare @@ -53,29 +53,29 @@ class TestAssertionRewrite: m = rewrite(s) assert isinstance(m.body[0], ast.Expr) assert isinstance(m.body[0].value, ast.Str) - for imp in m.body[1:4]: + for imp in m.body[1:3]: assert isinstance(imp, ast.Import) assert imp.lineno == 2 assert imp.col_offset == 0 - assert isinstance(m.body[4], ast.Assign) + assert isinstance(m.body[3], ast.Assign) s = """from __future__ import with_statement\nother_stuff""" m = rewrite(s) assert isinstance(m.body[0], ast.ImportFrom) - for imp in m.body[1:4]: + for imp in m.body[1:3]: assert isinstance(imp, ast.Import) assert imp.lineno == 2 assert imp.col_offset == 0 - assert isinstance(m.body[4], ast.Expr) + assert isinstance(m.body[3], ast.Expr) s = """'doc string'\nfrom __future__ import with_statement\nother""" m = rewrite(s) assert isinstance(m.body[0], ast.Expr) assert isinstance(m.body[0].value, ast.Str) assert isinstance(m.body[1], ast.ImportFrom) - for imp in m.body[2:5]: + for imp in m.body[2:4]: assert isinstance(imp, ast.Import) assert imp.lineno == 3 assert imp.col_offset == 0 - assert isinstance(m.body[5], ast.Expr) + assert isinstance(m.body[4], ast.Expr) def test_dont_rewrite(self): s = """'PYTEST_DONT_REWRITE'\nassert 14""" @@ -230,13 +230,13 @@ class TestAssertionRewrite: def test_custom_reprcompare(self, monkeypatch): def my_reprcompare(op, left, right): return "42" - monkeypatch.setattr(assertion, "_reprcompare", my_reprcompare) + monkeypatch.setattr(util, "_reprcompare", my_reprcompare) def f(): assert 42 < 3 assert getmsg(f) == "assert 42" def my_reprcompare(op, left, right): return "%s %s %s" % (left, op, right) - monkeypatch.setattr(assertion, "_reprcompare", my_reprcompare) + monkeypatch.setattr(util, "_reprcompare", my_reprcompare) def f(): assert 1 < 3 < 5 <= 4 < 7 assert getmsg(f) == "assert 5 <= 4"