Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove newlines from left/right operands with '-vv' #9743

Merged
merged 4 commits into from Mar 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -289,6 +289,7 @@ Ruaridh Williamson
Russel Winder
Ryan Wooden
Saiprasad Kale
Samuel Colvin
Samuel Dion-Girardeau
Samuel Searles-Bryant
Samuele Pedroni
Expand Down
1 change: 1 addition & 0 deletions changelog/9742.improvement.rst
@@ -0,0 +1 @@
Display assertion message without escaped newline characters with ``-vv``.
17 changes: 17 additions & 0 deletions src/_pytest/_io/saferepr.py
Expand Up @@ -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)."""

Expand Down
6 changes: 3 additions & 3 deletions src/_pytest/assertion/util.py
Expand Up @@ -14,8 +14,8 @@
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
Expand Down Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed using safeformat for the left and right operands when verbose > 1 is not what is expected, because safeformat introduces newlines, and the result of assertrepr_compare is expected to be a list of lines; the string produced by safeformat (containing \n) is put as a single string in the result, which causes them to be escaped by the terminal later (I assume).

So the fix is entirely correct: use repr to produce the representation without \n, which is used to build the summary variable which is expected to be a single line.

right_repr = saferepr_unlimited(right)
else:
# XXX: "15 chars indentation" is wrong
# ("E AssertionError: assert "); should use term width.
Expand Down
21 changes: 21 additions & 0 deletions testing/io/test_saferepr.py
Expand Up @@ -2,6 +2,7 @@
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():
Expand Down Expand Up @@ -179,3 +180,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"
)
15 changes: 15 additions & 0 deletions testing/test_assertion.py
Expand Up @@ -1695,3 +1695,18 @@ 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 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}"
)