Skip to content

Commit 3d4c3d8

Browse files
committed
Increase truncation threshold with -vvv, disable with -vvvv
Fix pytest-dev#6682
1 parent cd783eb commit 3d4c3d8

File tree

7 files changed

+108
-10
lines changed

7 files changed

+108
-10
lines changed

changelog/6682.improvement.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
By default, pytest will truncate long strings in assert errors so they don't clutter the output too much,
2+
currently at ``240`` characters by default.
3+
4+
However, in some cases the longer output helps, or even is crucial, to diagnose the problem. Using ``-vvv`` will
5+
increase the truncation threshold to ``2400`` characters, and ``-vvvv`` or higher will disable truncation completely.

src/_pytest/_io/saferepr.py

+23-8
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,23 @@ def _ellipsize(s: str, maxsize: int) -> str:
3636

3737

3838
class SafeRepr(reprlib.Repr):
39-
"""repr.Repr that limits the resulting size of repr() and includes
40-
information on exceptions raised during the call."""
39+
"""
40+
repr.Repr that limits the resulting size of repr() and includes
41+
information on exceptions raised during the call.
42+
"""
4143

42-
def __init__(self, maxsize: int) -> None:
44+
def __init__(self, maxsize: Optional[int]) -> None:
45+
"""
46+
:param maxsize:
47+
If not None, will truncate the resulting repr to that specific size, using ellipsis
48+
somewhere in the middle to hide the extra text.
49+
If None, will not impose any size limits on the returning repr.
50+
"""
4351
super().__init__()
44-
self.maxstring = maxsize
52+
# ``maxsize`` is used by the superclass, and needs to be an int; using a
53+
# very large number in case maxsize is None, meaning we want to disable
54+
# truncation.
55+
self.maxstring = maxsize if maxsize is not None else 1_000_000_000
4556
self.maxsize = maxsize
4657

4758
def repr(self, x: object) -> str:
@@ -51,7 +62,9 @@ def repr(self, x: object) -> str:
5162
raise
5263
except BaseException as exc:
5364
s = _format_repr_exception(exc, x)
54-
return _ellipsize(s, self.maxsize)
65+
if self.maxsize is not None:
66+
s = _ellipsize(s, self.maxsize)
67+
return s
5568

5669
def repr_instance(self, x: object, level: int) -> str:
5770
try:
@@ -60,7 +73,9 @@ def repr_instance(self, x: object, level: int) -> str:
6073
raise
6174
except BaseException as exc:
6275
s = _format_repr_exception(exc, x)
63-
return _ellipsize(s, self.maxsize)
76+
if self.maxsize is not None:
77+
s = _ellipsize(s, self.maxsize)
78+
return s
6479

6580

6681
def safeformat(obj: object) -> str:
@@ -75,15 +90,15 @@ def safeformat(obj: object) -> str:
7590
return _format_repr_exception(exc, obj)
7691

7792

78-
def saferepr(obj: object, maxsize: int = 240) -> str:
93+
def saferepr(obj: object, maxsize: Optional[int] = 240) -> str:
7994
"""Return a size-limited safe repr-string for the given object.
8095
8196
Failing __repr__ functions of user instances will be represented
8297
with a short exception info and 'saferepr' generally takes
8398
care to never raise exceptions itself.
8499
85100
This function is a wrapper around the Repr/reprlib functionality of the
86-
standard 2.6 lib.
101+
stdlib.
87102
"""
88103
return SafeRepr(maxsize).repr(obj)
89104

src/_pytest/assertion/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ def callbinrepr(op, left: object, right: object) -> Optional[str]:
153153

154154
saved_assert_hooks = util._reprcompare, util._assertion_pass
155155
util._reprcompare = callbinrepr
156+
util._config = item.config
156157

157158
if ihook.pytest_assertion_pass.get_hookimpls():
158159

@@ -164,6 +165,7 @@ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
164165
yield
165166

166167
util._reprcompare, util._assertion_pass = saved_assert_hooks
168+
util._config = None
167169

168170

169171
def pytest_sessionfinish(session: "Session") -> None:
@@ -176,4 +178,5 @@ def pytest_sessionfinish(session: "Session") -> None:
176178
def pytest_assertrepr_compare(
177179
config: Config, op: str, left: Any, right: Any
178180
) -> Optional[List[str]]:
179-
return util.assertrepr_compare(config=config, op=op, left=left, right=right)
181+
x = util.assertrepr_compare(config=config, op=op, left=left, right=right)
182+
return x

src/_pytest/assertion/rewrite.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,22 @@ def _saferepr(obj: object) -> str:
427427
sequences, especially '\n{' and '\n}' are likely to be present in
428428
JSON reprs.
429429
"""
430-
return saferepr(obj).replace("\n", "\\n")
430+
maxsize = _get_maxsize_for_saferepr(util._config)
431+
return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
432+
433+
434+
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
435+
"""Get `maxsize` configuration for saferepr based on the given config object."""
436+
verbosity = config.getoption("verbose") if config is not None else 0
437+
if verbosity >= 4:
438+
return None
439+
if verbosity >= 3:
440+
return _DEFAULT_REPR_MAX_SIZE * 10
441+
return _DEFAULT_REPR_MAX_SIZE
442+
443+
444+
# Maximum size of overall repr of objects to display during assertion errors.
445+
_DEFAULT_REPR_MAX_SIZE = 240
431446

432447

433448
def _format_assertmsg(obj: object) -> str:

src/_pytest/assertion/util.py

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from _pytest._io.saferepr import _pformat_dispatch
1616
from _pytest._io.saferepr import safeformat
1717
from _pytest._io.saferepr import saferepr
18+
from _pytest.config import Config
19+
1820

1921
# The _reprcompare attribute on the util module is used by the new assertion
2022
# interpretation code and assertion rewriter to detect this plugin was
@@ -26,6 +28,9 @@
2628
# when pytest_runtest_setup is called.
2729
_assertion_pass: Optional[Callable[[int, str, str], None]] = None
2830

31+
# Config object which is assigned
32+
_config: Optional[Config] = None
33+
2934

3035
def format_explanation(explanation: str) -> str:
3136
r"""Format an explanation.

testing/io/test_saferepr.py

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ def test_maxsize():
1515
assert s == expected
1616

1717

18+
def test_no_maxsize():
19+
text = "x" * 1000
20+
s = saferepr(text, maxsize=None)
21+
expected = repr(text)
22+
assert s == expected
23+
24+
1825
def test_maxsize_error_on_instance():
1926
class A:
2027
def __repr__(self):

testing/test_assertrewrite.py

+48
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import zipfile
1212
from functools import partial
1313
from pathlib import Path
14+
from typing import cast
1415
from typing import Dict
1516
from typing import List
1617
from typing import Mapping
@@ -20,12 +21,15 @@
2021
import _pytest._code
2122
import pytest
2223
from _pytest.assertion import util
24+
from _pytest.assertion.rewrite import _DEFAULT_REPR_MAX_SIZE
2325
from _pytest.assertion.rewrite import _get_assertion_exprs
26+
from _pytest.assertion.rewrite import _get_maxsize_for_saferepr
2427
from _pytest.assertion.rewrite import AssertionRewritingHook
2528
from _pytest.assertion.rewrite import get_cache_dir
2629
from _pytest.assertion.rewrite import PYC_TAIL
2730
from _pytest.assertion.rewrite import PYTEST_TAG
2831
from _pytest.assertion.rewrite import rewrite_asserts
32+
from _pytest.config import Config
2933
from _pytest.config import ExitCode
3034
from _pytest.pathlib import make_numbered_dir
3135
from _pytest.pytester import Pytester
@@ -1708,3 +1712,47 @@ def test_foo():
17081712
cache_tag=sys.implementation.cache_tag
17091713
)
17101714
assert bar_init_pyc.is_file()
1715+
1716+
1717+
class TestReprSizeVerbosity:
1718+
"""
1719+
Check that verbosity also controls the string length threshold to shorten it using
1720+
ellipsis.
1721+
"""
1722+
1723+
@pytest.mark.parametrize(
1724+
"verbose, expected_size", [(0, 240), (1, 240), (2, 240), (3, 2400), (4, None)]
1725+
)
1726+
def test_get_maxsize_for_saferepr(self, verbose: int, expected_size) -> None:
1727+
class FakeConfig:
1728+
def getoption(self, name: str) -> int:
1729+
assert name == "verbose"
1730+
return verbose
1731+
1732+
config = FakeConfig()
1733+
assert _get_maxsize_for_saferepr(cast(Config, config)) == expected_size
1734+
1735+
def create_test_file(self, pytester: Pytester, size: int) -> None:
1736+
pytester.makepyfile(
1737+
f"""
1738+
def test_very_long_string():
1739+
text = "x" * {size}
1740+
assert "hello world" in text
1741+
"""
1742+
)
1743+
1744+
@pytest.mark.parametrize("verbose_arg", ["", "-v", "-vv"])
1745+
def test_default_verbosity(self, pytester: Pytester, verbose_arg: str) -> None:
1746+
self.create_test_file(pytester, _DEFAULT_REPR_MAX_SIZE)
1747+
result = pytester.runpytest(verbose_arg)
1748+
result.stdout.fnmatch_lines(["*xxx...xxx*"])
1749+
1750+
def test_increased_verbosity(self, pytester: Pytester) -> None:
1751+
self.create_test_file(pytester, _DEFAULT_REPR_MAX_SIZE)
1752+
result = pytester.runpytest("-vvv")
1753+
result.stdout.no_fnmatch_line("*xxx...xxx*")
1754+
1755+
def test_max_increased_verbosity(self, pytester: Pytester) -> None:
1756+
self.create_test_file(pytester, _DEFAULT_REPR_MAX_SIZE * 10)
1757+
result = pytester.runpytest("-vvvv")
1758+
result.stdout.no_fnmatch_line("*xxx...xxx*")

0 commit comments

Comments
 (0)