diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 3b42b356d5b..6b8455596ba 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -110,8 +110,8 @@ def pytest_runtest_setup(item): comparison for the test. """ - def callbinrepr(op, left, right): - # type: (str, object, object) -> Optional[str] + def callbinrepr(op, left, right, expl): + # type: (str, object, object, str) -> Optional[str] """Call the pytest_assertrepr_compare hook and prepare the result This uses the first result from the hook and then ensures the @@ -127,10 +127,13 @@ def callbinrepr(op, left, right): pretty printing. """ hook_result = item.ihook.pytest_assertrepr_compare( - config=item.config, op=op, left=left, right=right + config=item.config, op=op, left=left, right=right, expl=expl ) for new_expl in hook_result: if new_expl: + if isinstance(new_expl, str): + return new_expl + new_expl = truncate.truncate_if_required(new_expl, item) new_expl = [line.replace("\n", "\\n") for line in new_expl] res = "\n~".join(new_expl) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index c225eff5fb5..4d10cf4ddfa 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -392,7 +392,7 @@ def _call_reprcompare(ops, results, expls, each_obj): if done: break if util._reprcompare is not None: - custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) + custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1], expl) if custom is not None: return custom return expl diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index c2a4e446ffb..2b6e460f78e 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -14,7 +14,9 @@ # interpretation code and assertion rewriter to detect this plugin was # loaded and in turn call the hooks defined here as part of the # DebugInterpreter. -_reprcompare = None # type: Optional[Callable[[str, object, object], Optional[str]]] +_reprcompare = ( + None +) # type: Optional[Callable[[str, object, object, str], Optional[str]]] # Works similarly as _reprcompare attribute. Is populated with the hook call # when pytest_runtest_setup is called. @@ -121,7 +123,7 @@ def isiterable(obj): return False -def assertrepr_compare(config, op, left, right): +def assertrepr_compare(config, op, left, right, expl): """Return specialised explanations for some operators/operands""" maxsize = (80 - 15 - len(op) - 2) // 2 # 15 chars indentation, 1 space around op left_repr = saferepr(left, maxsize=maxsize) @@ -146,7 +148,7 @@ def assertrepr_compare(config, op, left, right): type_fn = (isdatacls, isattrs) explanation = _compare_eq_cls(left, right, verbose, type_fn) elif verbose > 0: - explanation = _compare_eq_verbose(left, right) + return expl + "\n~" + "\n~".join(_compare_eq_verbose(left, right)) if isiterable(left) and isiterable(right): expl = _compare_eq_iterable(left, right, verbose) if explanation is not None: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 59fc569f4a5..9d9f264b1b0 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -472,7 +472,7 @@ def pytest_unconfigure(config): # ------------------------------------------------------------------------- -def pytest_assertrepr_compare(config, op, left, right): +def pytest_assertrepr_compare(config, op, left, right, expl): """return explanation for comparisons in failing assert expressions. Return None for no custom explanation, otherwise return a list