diff --git a/py/__init__.py b/py/__init__.py index e62e1a72a..814161cea 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -93,6 +93,7 @@ py.apipkg.initpkg(__name__, dict( '_AssertionError' : '._code.assertion:AssertionError', '_reinterpret_old' : '._code.assertion:reinterpret_old', '_reinterpret' : '._code.assertion:reinterpret', + '_binrepr' : '._code.assertion:_binrepr', }, # backports and additions of builtins diff --git a/py/_code/_assertionnew.py b/py/_code/_assertionnew.py index 2c40a93dc..5fdc1088b 100644 --- a/py/_code/_assertionnew.py +++ b/py/_code/_assertionnew.py @@ -108,16 +108,10 @@ unary_map = { class DebugInterpreter(ast.NodeVisitor): - """Interpret AST nodes to gleam useful debugging information. - - The _pytesthook attribute is used to detect if the py.test - pytest_assertion plugin is loaded and if so call it's hooks. - """ + """Interpret AST nodes to gleam useful debugging information. """ def __init__(self, frame): self.frame = frame - self._pytesthook = getattr(py.builtin.builtins.AssertionError, - "_pytesthook") def generic_visit(self, node): # Fallback when we don't have a special implementation. @@ -183,14 +177,12 @@ class DebugInterpreter(ast.NodeVisitor): if not result: break left_explanation, left_result = next_explanation, next_result - if self._pytesthook: - hook_result = self._pytesthook.pytest_assert_binrepr( - op=op_symbol, left=left_result, right=next_result) - if hook_result: - for new_expl in hook_result: - if new_expl: - explanation = '\n~'.join(new_expl) - break + + binrepr = py.code._binrepr + if binrepr: + res = binrepr(op_symbol, left_result, next_result) + if res: + explanation = res return explanation, result def visit_BoolOp(self, boolop): diff --git a/py/_code/assertion.py b/py/_code/assertion.py index adbbce7c5..675643e57 100644 --- a/py/_code/assertion.py +++ b/py/_code/assertion.py @@ -3,6 +3,7 @@ import py BuiltinAssertionError = py.builtin.builtins.AssertionError +_binrepr = None # if set, will be called by assert reinterp for comparison ops def _format_explanation(explanation): """This formats an explanation @@ -49,7 +50,6 @@ def _format_explanation(explanation): class AssertionError(BuiltinAssertionError): - def __init__(self, *args): BuiltinAssertionError.__init__(self, *args) if args: diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index 315da074a..2281d149c 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -151,7 +151,7 @@ def pytest_sessionfinish(session, exitstatus): # hooks for customising the assert methods # ------------------------------------------------------------------------- -def pytest_assert_binrepr(op, left, right): +def pytest_assert_binrepr(config, op, left, right): """Customise explanation for binary operators Return None or an empty list for no custom explanation, otherwise diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index b58b91985..bdfd0e819 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -15,13 +15,22 @@ def pytest_configure(config): if not config.getvalue("noassert") and not config.getvalue("nomagic"): warn_about_missing_assertion() config._oldassertion = py.builtin.builtins.AssertionError + config._oldbinrepr = py.code._binrepr py.builtin.builtins.AssertionError = py.code._AssertionError - py.builtin.builtins.AssertionError._pytesthook = config.hook + def callbinrepr(op, left, right): + hook_result = config.hook.pytest_assert_binrepr( + config=config, op=op, left=left, right=right) + for new_expl in hook_result: + if new_expl: + return '\n~'.join(new_expl) + py.code._binrepr = callbinrepr def pytest_unconfigure(config): if hasattr(config, '_oldassertion'): py.builtin.builtins.AssertionError = config._oldassertion + py.code._binrepr = config._oldbinrepr del config._oldassertion + del config._oldbinrepr def warn_about_missing_assertion(): try: diff --git a/testing/code/test_assertion.py b/testing/code/test_assertion.py index d417b0f75..07e9fa754 100644 --- a/testing/code/test_assertion.py +++ b/testing/code/test_assertion.py @@ -217,3 +217,13 @@ def test_underscore_api(): py.code._AssertionError py.code._reinterpret_old # used by pypy py.code._reinterpret + +@py.test.mark.skipif("sys.version_info < (2,6)") +def test_assert_customizable_binrepr(monkeypatch): + monkeypatch.setattr(py.code, '_binrepr', lambda *args: 'hello') + try: + assert 3 == 4 + except AssertionError: + e = exvalue() + s = str(e) + assert "hello" in s diff --git a/testing/plugin/test_pytest_assertion.py b/testing/plugin/test_pytest_assertion.py index 0dd64f473..8740b733a 100644 --- a/testing/plugin/test_pytest_assertion.py +++ b/testing/plugin/test_pytest_assertion.py @@ -3,29 +3,104 @@ import sys import py import py._plugin.pytest_assertion as plugin +needsnewassert = py.test.mark.skipif("sys.version_info < (2,6)") -def getframe(): - """Return the frame of the caller as a py.code.Frame object""" - return py.code.Frame(sys._getframe(1)) +def interpret(expr): + return py.code._reinterpret(expr, py.code.Frame(sys._getframe(1))) -def interpret(expr, frame): - anew = py.test.importorskip('py._code._assertionnew') - return anew.interpret(expr, frame) +class TestBinReprIntegration: + pytestmark = needsnewassert -def pytest_funcarg__hook(request): - class MockHook(object): - def __init__(self): - self.called = False - self.args = tuple() - self.kwargs = dict() + def pytest_funcarg__hook(self, request): + class MockHook(object): + def __init__(self): + self.called = False + self.args = tuple() + self.kwargs = dict() - def __call__(self, op, left, right): - self.called = True - self.op = op - self.left = left - self.right = right - return MockHook() + def __call__(self, op, left, right): + self.called = True + self.op = op + self.left = left + self.right = right + mockhook = MockHook() + monkeypatch = request.getfuncargvalue("monkeypatch") + monkeypatch.setattr(py.code, '_binrepr', mockhook) + return mockhook + def test_pytest_assert_binrepr_called(self, hook): + interpret('assert 0 == 1') + assert hook.called + + + def test_pytest_assert_binrepr_args(self, hook): + interpret('assert [0, 1] == [0, 2]') + assert hook.op == '==' + assert hook.left == [0, 1] + assert hook.right == [0, 2] + + def test_configure_unconfigure(self, testdir, hook): + assert hook == py.code._binrepr + config = testdir.parseconfig() + plugin.pytest_configure(config) + assert hook != py.code._binrepr + plugin.pytest_unconfigure(config) + assert hook == py.code._binrepr + +class TestAssert_binrepr: + def test_different_types(self): + assert plugin.pytest_assert_binrepr('==', [0, 1], 'foo') is None + + def test_summary(self): + summary = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2])[0] + assert len(summary) < 65 + + def test_text_diff(self): + diff = plugin.pytest_assert_binrepr('==', 'spam', 'eggs')[1:] + assert '- spam' in diff + assert '+ eggs' in diff + + def test_multiline_text_diff(self): + left = 'foo\nspam\nbar' + right = 'foo\neggs\nbar' + diff = plugin.pytest_assert_binrepr('==', left, right) + assert '- spam' in diff + assert '+ eggs' in diff + + def test_list(self): + expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2]) + assert len(expl) > 1 + + def test_list_different_lenghts(self): + expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 1, 2]) + assert len(expl) > 1 + expl = plugin.pytest_assert_binrepr('==', [0, 1, 2], [0, 1]) + assert len(expl) > 1 + + def test_dict(self): + expl = plugin.pytest_assert_binrepr('==', {'a': 0}, {'a': 1}) + assert len(expl) > 1 + + def test_set(self): + expl = plugin.pytest_assert_binrepr('==', set([0, 1]), set([0, 2])) + assert len(expl) > 1 + +@needsnewassert +def test_pytest_assert_binrepr_integration(testdir): + testdir.makepyfile(""" + def test_hello(): + x = set(range(100)) + y = x.copy() + y.remove(50) + assert x == y + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*def test_hello():*", + "*assert x == y*", + "*E*Extra items*left*", + "*E*50*", + ]) def test_functional(testdir): testdir.makepyfile(""" @@ -78,57 +153,3 @@ def test_traceback_failure(testdir): "*test_traceback_failure.py:4: AssertionError" ]) - -def test_pytest_assert_binrepr_called(monkeypatch, hook): - monkeypatch.setattr(py._plugin.pytest_assertion, - 'pytest_assert_binrepr', hook) - interpret('assert 0 == 1', getframe()) - assert hook.called - - -def test_pytest_assert_binrepr_args(monkeypatch, hook): - monkeypatch.setattr(py._plugin.pytest_assertion, - 'pytest_assert_binrepr', hook) - interpret('assert [0, 1] == [0, 2]', getframe()) - assert hook.op == '==' - assert hook.left == [0, 1] - assert hook.right == [0, 2] - - -class TestAssertCompare: - def test_different_types(self): - assert plugin.pytest_assert_binrepr('==', [0, 1], 'foo') is None - - def test_summary(self): - summary = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2])[0] - assert len(summary) < 65 - - def test_text_diff(self): - diff = plugin.pytest_assert_binrepr('==', 'spam', 'eggs')[1:] - assert '- spam' in diff - assert '+ eggs' in diff - - def test_multiline_text_diff(self): - left = 'foo\nspam\nbar' - right = 'foo\neggs\nbar' - diff = plugin.pytest_assert_binrepr('==', left, right) - assert '- spam' in diff - assert '+ eggs' in diff - - def test_list(self): - expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2]) - assert len(expl) > 1 - - def test_list_different_lenghts(self): - expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 1, 2]) - assert len(expl) > 1 - expl = plugin.pytest_assert_binrepr('==', [0, 1, 2], [0, 1]) - assert len(expl) > 1 - - def test_dict(self): - expl = plugin.pytest_assert_binrepr('==', {'a': 0}, {'a': 1}) - assert len(expl) > 1 - - def test_set(self): - expl = plugin.pytest_assert_binrepr('==', set([0, 1]), set([0, 2])) - assert len(expl) > 1