From b75cbee290ffd5a5e7fb3a247220d812d7f0fe71 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 19 Mar 2022 11:55:39 +0000 Subject: [PATCH] Remove newlines from left/right operands with '-vv' (#9743) The left/right operands produced when `verbose > 1` should not contain newlines, because they are used to build the `summary` string. The `assertrepr_compare` function returns a list of lines, and the summary is one of those lines and should not contain newlines itself. Fix #9742 Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bruno Oliveira --- AUTHORS | 1 + changelog/9742.improvement.rst | 1 + src/_pytest/_io/saferepr.py | 17 +++++++++++++++++ src/_pytest/assertion/util.py | 6 +++--- testing/io/test_saferepr.py | 21 +++++++++++++++++++++ testing/test_assertion.py | 15 +++++++++++++++ 6 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 changelog/9742.improvement.rst diff --git a/AUTHORS b/AUTHORS index 2b6ca66de..1c82b006d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -289,6 +289,7 @@ Ruaridh Williamson Russel Winder Ryan Wooden Saiprasad Kale +Samuel Colvin Samuel Dion-Girardeau Samuel Searles-Bryant Samuele Pedroni diff --git a/changelog/9742.improvement.rst b/changelog/9742.improvement.rst new file mode 100644 index 000000000..a1abfc274 --- /dev/null +++ b/changelog/9742.improvement.rst @@ -0,0 +1 @@ +Display assertion message without escaped newline characters with ``-vv``. diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index e7ff5cab2..a27e8c2a6 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -107,6 +107,23 @@ def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str return SafeRepr(maxsize).repr(obj) +def saferepr_unlimited(obj: object) -> str: + """Return an unlimited-size safe repr-string for the given object. + + As with saferepr, failing __repr__ functions of user instances + will be represented with a short exception info. + + This function is a wrapper around simple repr. + + Note: a cleaner solution would be to alter ``saferepr``this way + when maxsize=None, but that might affect some other code. + """ + try: + return repr(obj) + except Exception as exc: + return _format_repr_exception(exc, obj) + + class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter): """PrettyPrinter that always dispatches (regardless of width).""" diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 03167ddd4..75026730d 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -14,8 +14,8 @@ from typing import Sequence import _pytest._code from _pytest import outcomes from _pytest._io.saferepr import _pformat_dispatch -from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr +from _pytest._io.saferepr import saferepr_unlimited from _pytest.config import Config # The _reprcompare attribute on the util module is used by the new assertion @@ -160,8 +160,8 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[ """Return specialised explanations for some operators/operands.""" verbose = config.getoption("verbose") if verbose > 1: - left_repr = safeformat(left) - right_repr = safeformat(right) + left_repr = saferepr_unlimited(left) + right_repr = saferepr_unlimited(right) else: # XXX: "15 chars indentation" is wrong # ("E AssertionError: assert "); should use term width. diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 63d3af822..24746bc22 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -2,6 +2,7 @@ import pytest from _pytest._io.saferepr import _pformat_dispatch from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest._io.saferepr import saferepr +from _pytest._io.saferepr import saferepr_unlimited def test_simple_repr(): @@ -179,3 +180,23 @@ def test_broken_getattribute(): assert saferepr(SomeClass()).startswith( "<[RuntimeError() raised in repr()] SomeClass object at 0x" ) + + +def test_saferepr_unlimited(): + dict5 = {f"v{i}": i for i in range(5)} + assert saferepr_unlimited(dict5) == "{'v0': 0, 'v1': 1, 'v2': 2, 'v3': 3, 'v4': 4}" + + dict_long = {f"v{i}": i for i in range(1_000)} + r = saferepr_unlimited(dict_long) + assert "..." not in r + assert "\n" not in r + + +def test_saferepr_unlimited_exc(): + class A: + def __repr__(self): + raise ValueError(42) + + assert saferepr_unlimited(A()).startswith( + "<[ValueError(42) raised in repr()] A object at 0x" + ) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index d37ee72a2..4825ede77 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1695,3 +1695,18 @@ def test_assertion_location_with_coverage(pytester: Pytester) -> None: "*= 1 failed in*", ] ) + + +def test_reprcompare_verbose_long() -> None: + a = {f"v{i}": i for i in range(11)} + b = a.copy() + b["v2"] += 10 + lines = callop("==", a, b, verbose=2) + assert lines is not None + assert lines[0] == ( + "{'v0': 0, 'v1': 1, 'v2': 2, 'v3': 3, 'v4': 4, 'v5': 5, " + "'v6': 6, 'v7': 7, 'v8': 8, 'v9': 9, 'v10': 10}" + " == " + "{'v0': 0, 'v1': 1, 'v2': 12, 'v3': 3, 'v4': 4, 'v5': 5, " + "'v6': 6, 'v7': 7, 'v8': 8, 'v9': 9, 'v10': 10}" + )