refactor explanation formatting things into their own module
This commit is contained in:
parent
f423ce9c01
commit
250160b4b0
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue