From 350ab6fb2413951d3aceeb273af3712121ddf305 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 8 Mar 2022 16:12:03 +0000 Subject: [PATCH 1/4] improve -vv repr, fix #9742 --- AUTHORS | 1 + changelog/9742.improvement.rst | 1 + src/_pytest/_io/saferepr.py | 14 ++++++++++++++ src/_pytest/assertion/util.py | 6 +++--- testing/io/test_saferepr.py | 22 +++++++++++++++++++++- testing/test_assertion.py | 14 ++++++++++++++ 6 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 changelog/9742.improvement.rst diff --git a/AUTHORS b/AUTHORS index 2b6ca66de7a..1c82b006de1 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 00000000000..a1abfc27469 --- /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 e7ff5cab203..07c726aa6e1 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -107,6 +107,20 @@ 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. + """ + 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 03167ddd471..6d3a08bc10c 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -14,7 +14,7 @@ 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_unlimited from _pytest._io.saferepr import saferepr from _pytest.config import Config @@ -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 63d3af822b1..3bb5f3ece81 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -1,7 +1,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, saferepr_unlimited def test_simple_repr(): @@ -179,3 +179,23 @@ def __repr__(self): 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 d37ee72a2ec..0fcbca8e024 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1695,3 +1695,17 @@ def test(): "*= 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[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}" + ) From 2ae84c8494810d516df04b0c474898149aec4e44 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Mar 2022 16:18:03 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/assertion/util.py | 2 +- testing/io/test_saferepr.py | 13 +++++++------ testing/test_assertion.py | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 6d3a08bc10c..75026730df4 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -14,8 +14,8 @@ import _pytest._code from _pytest import outcomes from _pytest._io.saferepr import _pformat_dispatch -from _pytest._io.saferepr import saferepr_unlimited 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 diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 3bb5f3ece81..24746bc2235 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -1,7 +1,8 @@ import pytest from _pytest._io.saferepr import _pformat_dispatch from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE -from _pytest._io.saferepr import saferepr, saferepr_unlimited +from _pytest._io.saferepr import saferepr +from _pytest._io.saferepr import saferepr_unlimited def test_simple_repr(): @@ -182,13 +183,13 @@ def __repr__(self): def test_saferepr_unlimited(): - dict5 = {f'v{i}': i for i in range(5)} + 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)} + 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 + assert "..." not in r + assert "\n" not in r def test_saferepr_unlimited_exc(): @@ -197,5 +198,5 @@ def __repr__(self): raise ValueError(42) assert saferepr_unlimited(A()).startswith( - '<[ValueError(42) raised in repr()] A object at 0x' + "<[ValueError(42) raised in repr()] A object at 0x" ) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 0fcbca8e024..33b70372471 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1698,9 +1698,9 @@ def test(): def test_reprcompare_verbose_long() -> None: - a = {f'v{i}': i for i in range(11)} + a = {f"v{i}": i for i in range(11)} b = a.copy() - b['v2'] += 10 + b["v2"] += 10 lines = callop("==", a, b, verbose=2) assert lines[0] == ( "{'v0': 0, 'v1': 1, 'v2': 2, 'v3': 3, 'v4': 4, 'v5': 5, " From e4c1b55125bebc0f92371f4dc0bfe5b92cfdaff9 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 8 Mar 2022 18:20:45 +0000 Subject: [PATCH 3/4] fix mypy warning --- testing/test_assertion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 33b70372471..4825ede778b 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1702,6 +1702,7 @@ def test_reprcompare_verbose_long() -> None: 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}" From a9c54a1f8001adf6111f1c9eab52761f39bbc3fe Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 19 Mar 2022 08:37:29 -0300 Subject: [PATCH 4/4] Add note to saferepr_unlimited --- src/_pytest/_io/saferepr.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 07c726aa6e1..a27e8c2a6a5 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -114,6 +114,9 @@ def saferepr_unlimited(obj: object) -> str: 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)