refactor explanation formatting things into their own module

This commit is contained in:
Benjamin Peterson 2011-05-26 12:01:34 -05:00
parent f423ce9c01
commit 250160b4b0
7 changed files with 37 additions and 220 deletions

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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()),

View File

@ -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:

View File

@ -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)

View File

@ -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"