From 6762f5a1cd813a4c0147bf93cd27418f6bfe2d3b Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 11 Jan 2026 23:41:52 +0100 Subject: [PATCH 1/7] [truncate explanation] Fix edge case when we truncate due to max_chars Previousely the added test case would output: "...Full output truncated (0 lines hidden), use '-vv' to show" --- src/_pytest/assertion/truncate.py | 7 +++---- testing/test_assertion.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index 5820e6e8a80..ba08ef2dfa1 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -101,16 +101,15 @@ def _truncate_explanation( # No truncation happened, so we do not need to add any explanations return truncated_explanation - truncated_line_count = len(input_lines) - len(truncated_explanation) if truncated_explanation[-1]: # Add ellipsis and take into account part-truncated final line truncated_explanation[-1] = truncated_explanation[-1] + "..." - if truncated_char: - # It's possible that we did not remove any char from this line - truncated_line_count += 1 else: # Add proper ellipsis when we were able to fit a full line exactly truncated_explanation[-1] = "..." + truncated_line_count = ( + len(input_lines) - len(truncated_explanation) + int(truncated_char) + ) return [ *truncated_explanation, "", diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 5179b13b0e9..9c9881cf8ed 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1394,6 +1394,17 @@ def test_truncates_at_8_lines_when_there_is_one_line_to_remove(self) -> None: assert result == expl assert "truncated" not in result[-1] + def test_truncates_full_line_because_of_max_chars(self) -> None: + """A line is fully truncated because of the max_chars value.""" + expl = ["a" * 10, "b" * 71] + result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10) + assert result == [ + "a" * 10, + "...", + "", + "...Full output truncated (1 line hidden), use '-vv' to show", + ] + def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_chars( self, ) -> None: From f27a339073d63847a12437a40bc7129650439d4c Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 12 Jan 2026 15:09:56 +0100 Subject: [PATCH 2/7] [doc] Better documentation in _truncate_explanation --- src/_pytest/assertion/truncate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index ba08ef2dfa1..c4d8a5c40cd 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -59,6 +59,12 @@ def _truncate_explanation( Truncates to either max_lines, or max_chars - whichever the input reaches first, taking the truncation explanation into account. The remaining lines will be replaced by a usage message. + + If max_chars=0, no truncation by character count is performed. + If max_lines=0, no truncation by line count is performed. + + When this function is launched we know max_lines > 0 or max_chars > 0 + because _get_truncation_parameters was called first. """ # Check if truncation required input_char_count = len("".join(input_lines)) From 48087fd9d61f1eb0828d03f2b8bb46b9912ccedc Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 12 Jan 2026 15:10:37 +0100 Subject: [PATCH 3/7] [truncate explanation] Simplification of the '...' adding mechanism --- src/_pytest/assertion/truncate.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index c4d8a5c40cd..fe006c6325c 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -107,12 +107,8 @@ def _truncate_explanation( # No truncation happened, so we do not need to add any explanations return truncated_explanation - if truncated_explanation[-1]: - # Add ellipsis and take into account part-truncated final line - truncated_explanation[-1] = truncated_explanation[-1] + "..." - else: - # Add proper ellipsis when we were able to fit a full line exactly - truncated_explanation[-1] = "..." + # Something was truncated, adding '...' at the end to show that + truncated_explanation[-1] += "..." truncated_line_count = ( len(input_lines) - len(truncated_explanation) + int(truncated_char) ) From 4e543e816bb52cc58e76cce4b4a67af1b3a7e0cc Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 12 Jan 2026 15:11:54 +0100 Subject: [PATCH 4/7] [truncate explanation] Refactor the logic to simplify the code --- src/_pytest/assertion/truncate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index fe006c6325c..8a58d806f97 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -94,14 +94,14 @@ def _truncate_explanation( truncated_explanation = input_lines[:max_lines] else: truncated_explanation = input_lines - truncated_char = True # We reevaluate the need to truncate chars following removal of some lines - if len("".join(truncated_explanation)) > tolerable_max_chars and max_chars > 0: + need_to_truncate_char = ( + max_chars > 0 and len("".join(truncated_explanation)) > tolerable_max_chars + ) + if need_to_truncate_char: truncated_explanation = _truncate_by_char_count( truncated_explanation, max_chars ) - else: - truncated_char = False if truncated_explanation == input_lines: # No truncation happened, so we do not need to add any explanations @@ -110,7 +110,7 @@ def _truncate_explanation( # Something was truncated, adding '...' at the end to show that truncated_explanation[-1] += "..." truncated_line_count = ( - len(input_lines) - len(truncated_explanation) + int(truncated_char) + len(input_lines) - len(truncated_explanation) + int(need_to_truncate_char) ) return [ *truncated_explanation, From 49bb9878a275bf8388c7972d425a10f018bd37eb Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 12 Jan 2026 15:43:26 +0100 Subject: [PATCH 5/7] [truncate explanation] Optimize the string length calculation sum(len(x for s in strings) is consistentely faster than len(''.join(strings)), see https://claude.ai/public/artifacts/6a4c33e7-9ad5-4078-8ee7-e343984ce087 --- src/_pytest/assertion/truncate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index 8a58d806f97..dbf55fd2594 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -67,7 +67,7 @@ def _truncate_explanation( because _get_truncation_parameters was called first. """ # Check if truncation required - input_char_count = len("".join(input_lines)) + input_char_count = sum(len(line) for line in input_lines) # The length of the truncation explanation depends on the number of lines # removed but is at least 68 characters: # The real value is @@ -96,7 +96,8 @@ def _truncate_explanation( truncated_explanation = input_lines # We reevaluate the need to truncate chars following removal of some lines need_to_truncate_char = ( - max_chars > 0 and len("".join(truncated_explanation)) > tolerable_max_chars + max_chars > 0 + and sum(len(e) for e in truncated_explanation) > tolerable_max_chars ) if need_to_truncate_char: truncated_explanation = _truncate_by_char_count( From b86c6f4893c9ca48faf8d4a0eb3717360f9790fb Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 12 Jan 2026 16:22:02 +0100 Subject: [PATCH 6/7] [truncate_explanation] Cut short by checking if we're going to truncate Better check the theory and the len of the string to know if we're going to truncate rather than checking the full length of the result after the fact. --- src/_pytest/assertion/truncate.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index dbf55fd2594..cb08ddfb490 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -66,8 +66,6 @@ def _truncate_explanation( When this function is launched we know max_lines > 0 or max_chars > 0 because _get_truncation_parameters was called first. """ - # Check if truncation required - input_char_count = sum(len(line) for line in input_lines) # The length of the truncation explanation depends on the number of lines # removed but is at least 68 characters: # The real value is @@ -82,11 +80,11 @@ def _truncate_explanation( tolerable_max_chars = ( max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...' ) - # The truncation explanation add two lines to the output - tolerable_max_lines = max_lines + 2 if ( - len(input_lines) <= tolerable_max_lines - and input_char_count <= tolerable_max_chars + # The truncation explanation add two lines to the output + max_lines == 0 or len(input_lines) <= max_lines + 2 + ) and ( + max_chars == 0 or sum(len(line) for line in input_lines) <= tolerable_max_chars ): return input_lines # Truncate first to max_lines, and then truncate to max_chars if necessary @@ -103,11 +101,6 @@ def _truncate_explanation( truncated_explanation = _truncate_by_char_count( truncated_explanation, max_chars ) - - if truncated_explanation == input_lines: - # No truncation happened, so we do not need to add any explanations - return truncated_explanation - # Something was truncated, adding '...' at the end to show that truncated_explanation[-1] += "..." truncated_line_count = ( From c454d2fb0b8f701e7e31684ef8bd52d55c805fc9 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 13 Jan 2026 14:13:09 +0100 Subject: [PATCH 7/7] [truncate explanation] Remove some redundant checks by grouping them together --- src/_pytest/assertion/truncate.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index cb08ddfb490..d62ca33cc4b 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -80,18 +80,14 @@ def _truncate_explanation( tolerable_max_chars = ( max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...' ) - if ( - # The truncation explanation add two lines to the output - max_lines == 0 or len(input_lines) <= max_lines + 2 - ) and ( - max_chars == 0 or sum(len(line) for line in input_lines) <= tolerable_max_chars - ): - return input_lines - # Truncate first to max_lines, and then truncate to max_chars if necessary - if max_lines > 0: - truncated_explanation = input_lines[:max_lines] - else: + # The truncation explanation add two lines to the output + if max_lines == 0 or len(input_lines) <= max_lines + 2: + if max_chars == 0 or sum(len(s) for s in input_lines) <= tolerable_max_chars: + return input_lines truncated_explanation = input_lines + else: + # Truncate first to max_lines, and then truncate to max_chars if necessary + truncated_explanation = input_lines[:max_lines] # We reevaluate the need to truncate chars following removal of some lines need_to_truncate_char = ( max_chars > 0