From d6dcc6d6c7a79d33763b95eabfeb449662859a6c Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 5 Jan 2026 00:04:06 +0530 Subject: [PATCH 1/7] Add regression test and fixed dict insertion order in assertion diffs(#14079) --- src/_pytest/assertion/util.py | 47 +++++++++------ testing/test_assertion.py | 108 ++++++++++++++++++++++++++-------- 2 files changed, 114 insertions(+), 41 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index f35d83a6fe4..a4c2ac194e6 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -217,16 +217,20 @@ def assertrepr_compare( explanation = ["Both sets are equal"] elif op == ">=": if isset(left) and isset(right): - explanation = _compare_gte_set(left, right, highlighter, verbose) + explanation = _compare_gte_set( + left, right, highlighter, verbose) elif op == "<=": if isset(left) and isset(right): - explanation = _compare_lte_set(left, right, highlighter, verbose) + explanation = _compare_lte_set( + left, right, highlighter, verbose) elif op == ">": if isset(left) and isset(right): - explanation = _compare_gt_set(left, right, highlighter, verbose) + explanation = _compare_gt_set( + left, right, highlighter, verbose) elif op == "<": if isset(left) and isset(right): - explanation = _compare_lt_set(left, right, highlighter, verbose) + explanation = _compare_lt_set( + left, right, highlighter, verbose) except outcomes.Exit: raise @@ -269,7 +273,8 @@ def _compare_eq_any( # used in older code bases before dataclasses/attrs were available. explanation = _compare_eq_cls(left, right, highlighter, verbose) elif issequence(left) and issequence(right): - explanation = _compare_eq_sequence(left, right, highlighter, verbose) + explanation = _compare_eq_sequence( + left, right, highlighter, verbose) elif isset(left) and isset(right): explanation = _compare_eq_set(left, right, highlighter, verbose) elif isdict(left) and isdict(right): @@ -387,8 +392,8 @@ def _compare_eq_sequence( # 102 # >>> s[0:1] # b'f' - left_value = left[i : i + 1] - right_value = right[i : i + 1] + left_value = left[i: i + 1] + right_value = right[i: i + 1] else: left_value = left[i] right_value = right[i] @@ -511,6 +516,7 @@ def _compare_eq_dict( elif same: explanation += ["Common items:"] explanation += highlighter(pprint.pformat(same)).splitlines() + diff = {k for k in common if left[k] != right[k]} if diff: explanation += ["Differing items:"] @@ -520,24 +526,27 @@ def _compare_eq_dict( + " != " + highlighter(saferepr({k: right[k]})) ] + extra_left = set_left - set_right - len_extra_left = len(extra_left) - if len_extra_left: + if extra_left: explanation.append( - f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:" + f"Left contains {len(extra_left)} more item{'' if len(extra_left) == 1 else 's'}:" ) explanation.extend( - highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines() + highlighter(saferepr({k: left[k] + for k in extra_left})).splitlines() ) + extra_right = set_right - set_left - len_extra_right = len(extra_right) - if len_extra_right: + if extra_right: explanation.append( - f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:" + f"Right contains {len(extra_right)} more item{'' if len(extra_right) == 1 else 's'}:" ) explanation.extend( - highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines() + highlighter(saferepr({k: right[k] + for k in extra_right})).splitlines() ) + return explanation @@ -553,7 +562,8 @@ def _compare_eq_cls( fields_to_check = [info.name for info in all_fields if info.compare] elif isattrs(left): all_fields = left.__attrs_attrs__ - fields_to_check = [field.name for field in all_fields if getattr(field, "eq")] + fields_to_check = [ + field.name for field in all_fields if getattr(field, "eq")] elif isnamedtuple(left): fields_to_check = left._fields else: @@ -572,7 +582,8 @@ def _compare_eq_cls( if same or diff: explanation += [""] if same and verbose < 2: - explanation.append(f"Omitting {len(same)} identical items, use -vv to show") + explanation.append( + f"Omitting {len(same)} identical items, use -vv to show") elif same: explanation += ["Matching attributes:"] explanation += highlighter(pprint.pformat(same)).splitlines() @@ -599,7 +610,7 @@ def _compare_eq_cls( def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]: index = text.find(term) head = text[:index] - tail = text[index + len(term) :] + tail = text[index + len(term):] correct_text = head + tail diff = _diff_text(text, correct_text, dummy_highlighter, verbose) newdiff = [f"{saferepr(term, maxsize=42)} is contained here:"] diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 5179b13b0e9..9b173b3e5d4 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -209,8 +209,10 @@ def test_pytest_plugins_rewrite_module_names_correctly( "hamster.py": "", "test_foo.py": """\ def test_foo(pytestconfig): - assert pytestconfig.pluginmanager.rewrite_hook.find_spec('ham') is not None - assert pytestconfig.pluginmanager.rewrite_hook.find_spec('hamster') is None + assert pytestconfig.pluginmanager.rewrite_hook.find_spec( + 'ham') is not None + assert pytestconfig.pluginmanager.rewrite_hook.find_spec( + 'hamster') is None """, } pytester.makepyfile(**contents) @@ -372,7 +374,8 @@ def test_other(): def test_register_assert_rewrite_checks_types(self) -> None: with pytest.raises(TypeError): - pytest.register_assert_rewrite(["pytest_tests_internal_non_existing"]) # type: ignore + pytest.register_assert_rewrite( + ["pytest_tests_internal_non_existing"]) # type: ignore pytest.register_assert_rewrite( "pytest_tests_internal_non_existing", "pytest_tests_internal_non_existing2" ) @@ -417,6 +420,45 @@ class TestAssert_reprcompare: def test_different_types(self) -> None: assert callequal([0, 1], "foo") is None + def test_dict_preserves_insertion_order_regression(self) -> None: + # Regression test for issue #14079 + + long_a = "a" * 80 + sub = { + "long_a": long_a, + "sub1": { + "long_a": "substring that gets wrapped " * 3, + }, + } + + left = {"env": {"sub": sub}} + right = {"env": {"sub": sub}, "new": 1} + + diff = callequal(left, right, verbose=True) + assert diff is not None + + # extra key is reported + assert "{'new': 1}" in diff + + # inspect only structured diff + assert "Full diff:" in diff + start = diff.index("Full diff:") + 1 + diff_block = diff[start:] + + # 'new' must not appear as a structural key + assert all( + "'new': 1" not in line or line.lstrip().startswith("-") + for line in diff_block + ) + + # insertion order preserved inside left dict + env_index = next(i for i, l in enumerate( + diff_block) if "'env': {" in l) + closing_index = next(i for i, l in enumerate( + diff_block) if l.strip() == "}") + + assert env_index < closing_index + def test_summary(self) -> None: lines = callequal([0, 1], [0, 2]) assert lines is not None @@ -541,7 +583,8 @@ def test_iterable_full_diff(self, left, right, expected) -> None: assert expl[-1] == "Use -v to get more diff" verbose_expl = callequal(left, right, verbose=1) assert verbose_expl is not None - assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip()) + assert "\n".join(verbose_expl).endswith( + textwrap.dedent(expected).strip()) def test_iterable_quiet(self) -> None: expl = callequal([1, 2], [10, 2], verbose=-1) @@ -687,7 +730,8 @@ def test_dict_wrap(self) -> None: ] long_a = "a" * 80 - sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 3}} + sub = {"long_a": long_a, "sub1": { + "long_a": "substring that gets wrapped " * 3}} d1 = {"env": {"sub": sub}} d2 = {"env": {"sub": sub}, "new": 1} diff = callequal(d1, d2, verbose=True) @@ -987,7 +1031,8 @@ def test_dataclasses(self, pytester: Pytester) -> None: ) def test_recursive_dataclasses(self, pytester: Pytester) -> None: - p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") + p = pytester.copy_example( + "dataclasses/test_compare_recursive_dataclasses.py") result = pytester.runpytest(p) result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1005,7 +1050,8 @@ def test_recursive_dataclasses(self, pytester: Pytester) -> None: ) def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None: - p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") + p = pytester.copy_example( + "dataclasses/test_compare_recursive_dataclasses.py") result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1035,7 +1081,8 @@ def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None: ) def test_dataclasses_verbose(self, pytester: Pytester) -> None: - p = pytester.copy_example("dataclasses/test_compare_dataclasses_verbose.py") + p = pytester.copy_example( + "dataclasses/test_compare_dataclasses_verbose.py") result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1287,12 +1334,14 @@ def test_fmt_where(self) -> None: def test_fmt_and(self) -> None: expl = "\n".join(["assert 1", "{1 = foo", "} == 2", "{2 = bar", "}"]) - res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + and 2 = bar"]) + res = "\n".join( + ["assert 1 == 2", " + where 1 = foo", " + and 2 = bar"]) assert util.format_explanation(expl) == res def test_fmt_where_nested(self) -> None: expl = "\n".join(["assert 1", "{1 = foo", "{foo = bar", "}", "} == 2"]) - res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + where foo = bar"]) + res = "\n".join( + ["assert 1 == 2", " + where 1 = foo", " + where foo = bar"]) assert util.format_explanation(expl) == res def test_fmt_newline(self) -> None: @@ -1357,17 +1406,20 @@ class TestTruncateExplanation: def test_doesnt_truncate_when_input_is_empty_list(self) -> None: expl: list[str] = [] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=100) assert result == expl def test_doesnt_truncate_at_when_input_is_5_lines_and_LT_max_chars(self) -> None: expl = ["a" * 100 for x in range(5)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=8 * 80) assert result == expl def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: expl = ["" for x in range(50)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=100) assert len(result) != len(expl) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG @@ -1379,7 +1431,8 @@ def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: total_lines = 100 expl = ["a" for x in range(total_lines)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=8 * 80) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1390,7 +1443,8 @@ def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: def test_truncates_at_8_lines_when_there_is_one_line_to_remove(self) -> None: """The number of line in the result is 9, the same number as if we truncated.""" expl = ["a" for x in range(9)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=8 * 80) assert result == expl assert "truncated" not in result[-1] @@ -1399,7 +1453,8 @@ def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_ ) -> None: line = "a" * 10 expl = [line, line] - result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10) + result = truncate._truncate_explanation( + expl, max_lines=10, max_chars=10) assert result == [line, line] def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_lines( @@ -1407,12 +1462,14 @@ def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_ ) -> None: line = "a" * 10 expl = [line, line] - result = truncate._truncate_explanation(expl, max_lines=1, max_chars=100) + result = truncate._truncate_explanation( + expl, max_lines=1, max_chars=100) assert result == [line, line] def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: expl = [chr(97 + x) * 80 for x in range(16)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=8 * 80) assert result != expl assert len(result) == 16 - 8 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1422,7 +1479,8 @@ def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None: expl = ["a" * 250 for x in range(10)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=999) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=999) assert result != expl assert len(result) == 4 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1432,7 +1490,8 @@ def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None: def test_truncates_at_1_line_when_first_line_is_GT_max_chars(self) -> None: expl = ["a" * 250 for x in range(1000)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=100) assert result != expl assert len(result) == 1 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1674,12 +1733,14 @@ def test_hello(): def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None: pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"]) a = pytester.mkdir("a") - a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2", encoding="utf-8") + a.joinpath("test_a.py").write_text( + "def test_a(): assert 1 == 2", encoding="utf-8") a.joinpath("conftest.py").write_text( 'def pytest_assertrepr_compare(): return ["summary a"]', encoding="utf-8" ) b = pytester.mkdir("b") - b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2", encoding="utf-8") + b.joinpath("test_b.py").write_text( + "def test_b(): assert 1 == 2", encoding="utf-8") b.joinpath("conftest.py").write_text( 'def pytest_assertrepr_compare(): return ["summary b"]', encoding="utf-8" ) @@ -1971,7 +2032,8 @@ def test_raising_repr(): """ ) result = pytester.runpytest() - result.stdout.fnmatch_lines(["E AssertionError: "]) + result.stdout.fnmatch_lines( + ["E AssertionError: "]) def test_issue_1944(pytester: Pytester) -> None: From d2b37ea5722146d92d359cfab7b0c8ba37245efd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 18:42:35 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/assertion/util.py | 33 ++++++----------- testing/test_assertion.py | 69 ++++++++++++----------------------- 2 files changed, 35 insertions(+), 67 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index a4c2ac194e6..13a7f1e8c5a 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -217,20 +217,16 @@ def assertrepr_compare( explanation = ["Both sets are equal"] elif op == ">=": if isset(left) and isset(right): - explanation = _compare_gte_set( - left, right, highlighter, verbose) + explanation = _compare_gte_set(left, right, highlighter, verbose) elif op == "<=": if isset(left) and isset(right): - explanation = _compare_lte_set( - left, right, highlighter, verbose) + explanation = _compare_lte_set(left, right, highlighter, verbose) elif op == ">": if isset(left) and isset(right): - explanation = _compare_gt_set( - left, right, highlighter, verbose) + explanation = _compare_gt_set(left, right, highlighter, verbose) elif op == "<": if isset(left) and isset(right): - explanation = _compare_lt_set( - left, right, highlighter, verbose) + explanation = _compare_lt_set(left, right, highlighter, verbose) except outcomes.Exit: raise @@ -273,8 +269,7 @@ def _compare_eq_any( # used in older code bases before dataclasses/attrs were available. explanation = _compare_eq_cls(left, right, highlighter, verbose) elif issequence(left) and issequence(right): - explanation = _compare_eq_sequence( - left, right, highlighter, verbose) + explanation = _compare_eq_sequence(left, right, highlighter, verbose) elif isset(left) and isset(right): explanation = _compare_eq_set(left, right, highlighter, verbose) elif isdict(left) and isdict(right): @@ -392,8 +387,8 @@ def _compare_eq_sequence( # 102 # >>> s[0:1] # b'f' - left_value = left[i: i + 1] - right_value = right[i: i + 1] + left_value = left[i : i + 1] + right_value = right[i : i + 1] else: left_value = left[i] right_value = right[i] @@ -533,8 +528,7 @@ def _compare_eq_dict( f"Left contains {len(extra_left)} more item{'' if len(extra_left) == 1 else 's'}:" ) explanation.extend( - highlighter(saferepr({k: left[k] - for k in extra_left})).splitlines() + highlighter(saferepr({k: left[k] for k in extra_left})).splitlines() ) extra_right = set_right - set_left @@ -543,8 +537,7 @@ def _compare_eq_dict( f"Right contains {len(extra_right)} more item{'' if len(extra_right) == 1 else 's'}:" ) explanation.extend( - highlighter(saferepr({k: right[k] - for k in extra_right})).splitlines() + highlighter(saferepr({k: right[k] for k in extra_right})).splitlines() ) return explanation @@ -562,8 +555,7 @@ def _compare_eq_cls( fields_to_check = [info.name for info in all_fields if info.compare] elif isattrs(left): all_fields = left.__attrs_attrs__ - fields_to_check = [ - field.name for field in all_fields if getattr(field, "eq")] + fields_to_check = [field.name for field in all_fields if getattr(field, "eq")] elif isnamedtuple(left): fields_to_check = left._fields else: @@ -582,8 +574,7 @@ def _compare_eq_cls( if same or diff: explanation += [""] if same and verbose < 2: - explanation.append( - f"Omitting {len(same)} identical items, use -vv to show") + explanation.append(f"Omitting {len(same)} identical items, use -vv to show") elif same: explanation += ["Matching attributes:"] explanation += highlighter(pprint.pformat(same)).splitlines() @@ -610,7 +601,7 @@ def _compare_eq_cls( def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]: index = text.find(term) head = text[:index] - tail = text[index + len(term):] + tail = text[index + len(term) :] correct_text = head + tail diff = _diff_text(text, correct_text, dummy_highlighter, verbose) newdiff = [f"{saferepr(term, maxsize=42)} is contained here:"] diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 9b173b3e5d4..c9eb41d1654 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -374,8 +374,7 @@ def test_other(): def test_register_assert_rewrite_checks_types(self) -> None: with pytest.raises(TypeError): - pytest.register_assert_rewrite( - ["pytest_tests_internal_non_existing"]) # type: ignore + pytest.register_assert_rewrite(["pytest_tests_internal_non_existing"]) # type: ignore pytest.register_assert_rewrite( "pytest_tests_internal_non_existing", "pytest_tests_internal_non_existing2" ) @@ -452,10 +451,8 @@ def test_dict_preserves_insertion_order_regression(self) -> None: ) # insertion order preserved inside left dict - env_index = next(i for i, l in enumerate( - diff_block) if "'env': {" in l) - closing_index = next(i for i, l in enumerate( - diff_block) if l.strip() == "}") + env_index = next(i for i, l in enumerate(diff_block) if "'env': {" in l) + closing_index = next(i for i, l in enumerate(diff_block) if l.strip() == "}") assert env_index < closing_index @@ -583,8 +580,7 @@ def test_iterable_full_diff(self, left, right, expected) -> None: assert expl[-1] == "Use -v to get more diff" verbose_expl = callequal(left, right, verbose=1) assert verbose_expl is not None - assert "\n".join(verbose_expl).endswith( - textwrap.dedent(expected).strip()) + assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip()) def test_iterable_quiet(self) -> None: expl = callequal([1, 2], [10, 2], verbose=-1) @@ -730,8 +726,7 @@ def test_dict_wrap(self) -> None: ] long_a = "a" * 80 - sub = {"long_a": long_a, "sub1": { - "long_a": "substring that gets wrapped " * 3}} + sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 3}} d1 = {"env": {"sub": sub}} d2 = {"env": {"sub": sub}, "new": 1} diff = callequal(d1, d2, verbose=True) @@ -1031,8 +1026,7 @@ def test_dataclasses(self, pytester: Pytester) -> None: ) def test_recursive_dataclasses(self, pytester: Pytester) -> None: - p = pytester.copy_example( - "dataclasses/test_compare_recursive_dataclasses.py") + p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") result = pytester.runpytest(p) result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1050,8 +1044,7 @@ def test_recursive_dataclasses(self, pytester: Pytester) -> None: ) def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None: - p = pytester.copy_example( - "dataclasses/test_compare_recursive_dataclasses.py") + p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1081,8 +1074,7 @@ def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None: ) def test_dataclasses_verbose(self, pytester: Pytester) -> None: - p = pytester.copy_example( - "dataclasses/test_compare_dataclasses_verbose.py") + p = pytester.copy_example("dataclasses/test_compare_dataclasses_verbose.py") result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1334,14 +1326,12 @@ def test_fmt_where(self) -> None: def test_fmt_and(self) -> None: expl = "\n".join(["assert 1", "{1 = foo", "} == 2", "{2 = bar", "}"]) - res = "\n".join( - ["assert 1 == 2", " + where 1 = foo", " + and 2 = bar"]) + res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + and 2 = bar"]) assert util.format_explanation(expl) == res def test_fmt_where_nested(self) -> None: expl = "\n".join(["assert 1", "{1 = foo", "{foo = bar", "}", "} == 2"]) - res = "\n".join( - ["assert 1 == 2", " + where 1 = foo", " + where foo = bar"]) + res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + where foo = bar"]) assert util.format_explanation(expl) == res def test_fmt_newline(self) -> None: @@ -1406,20 +1396,17 @@ class TestTruncateExplanation: def test_doesnt_truncate_when_input_is_empty_list(self) -> None: expl: list[str] = [] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result == expl def test_doesnt_truncate_at_when_input_is_5_lines_and_LT_max_chars(self) -> None: expl = ["a" * 100 for x in range(5)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result == expl def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: expl = ["" for x in range(50)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert len(result) != len(expl) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG @@ -1431,8 +1418,7 @@ def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: total_lines = 100 expl = ["a" for x in range(total_lines)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1443,8 +1429,7 @@ def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: def test_truncates_at_8_lines_when_there_is_one_line_to_remove(self) -> None: """The number of line in the result is 9, the same number as if we truncated.""" expl = ["a" for x in range(9)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result == expl assert "truncated" not in result[-1] @@ -1453,8 +1438,7 @@ def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_ ) -> None: line = "a" * 10 expl = [line, line] - result = truncate._truncate_explanation( - expl, max_lines=10, max_chars=10) + result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10) assert result == [line, line] def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_lines( @@ -1462,14 +1446,12 @@ def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_ ) -> None: line = "a" * 10 expl = [line, line] - result = truncate._truncate_explanation( - expl, max_lines=1, max_chars=100) + result = truncate._truncate_explanation(expl, max_lines=1, max_chars=100) assert result == [line, line] def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: expl = [chr(97 + x) * 80 for x in range(16)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result != expl assert len(result) == 16 - 8 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1479,8 +1461,7 @@ def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None: expl = ["a" * 250 for x in range(10)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=999) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=999) assert result != expl assert len(result) == 4 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1490,8 +1471,7 @@ def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None: def test_truncates_at_1_line_when_first_line_is_GT_max_chars(self) -> None: expl = ["a" * 250 for x in range(1000)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result != expl assert len(result) == 1 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1733,14 +1713,12 @@ def test_hello(): def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None: pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"]) a = pytester.mkdir("a") - a.joinpath("test_a.py").write_text( - "def test_a(): assert 1 == 2", encoding="utf-8") + a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2", encoding="utf-8") a.joinpath("conftest.py").write_text( 'def pytest_assertrepr_compare(): return ["summary a"]', encoding="utf-8" ) b = pytester.mkdir("b") - b.joinpath("test_b.py").write_text( - "def test_b(): assert 1 == 2", encoding="utf-8") + b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2", encoding="utf-8") b.joinpath("conftest.py").write_text( 'def pytest_assertrepr_compare(): return ["summary b"]', encoding="utf-8" ) @@ -2032,8 +2010,7 @@ def test_raising_repr(): """ ) result = pytester.runpytest() - result.stdout.fnmatch_lines( - ["E AssertionError: "]) + result.stdout.fnmatch_lines(["E AssertionError: "]) def test_issue_1944(pytester: Pytester) -> None: From 7178056dc9cedda833e4699af7c5859f4b722446 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 5 Jan 2026 00:25:09 +0530 Subject: [PATCH 3/7] Add changelog entry for dict assertion order fix --- changelog/assert-dict-insertion-order.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/assert-dict-insertion-order.rst diff --git a/changelog/assert-dict-insertion-order.rst b/changelog/assert-dict-insertion-order.rst new file mode 100644 index 00000000000..cf65868238c --- /dev/null +++ b/changelog/assert-dict-insertion-order.rst @@ -0,0 +1,6 @@ +Fix assertion diff output to preserve dictionary insertion order. + +When comparing dictionaries with extra keys, pytest could incorrectly inject +those keys into the structured diff output, producing misleading results. +The assertion diff now correctly preserves insertion order and reports extra +keys separately. From dc40532118fe9551579c184d39537af70072147e Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 5 Jan 2026 00:27:47 +0530 Subject: [PATCH 4/7] Add changelog entry for dict assertion order fix --- changelog/14079.bugfix.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/14079.bugfix.rst diff --git a/changelog/14079.bugfix.rst b/changelog/14079.bugfix.rst new file mode 100644 index 00000000000..cf65868238c --- /dev/null +++ b/changelog/14079.bugfix.rst @@ -0,0 +1,6 @@ +Fix assertion diff output to preserve dictionary insertion order. + +When comparing dictionaries with extra keys, pytest could incorrectly inject +those keys into the structured diff output, producing misleading results. +The assertion diff now correctly preserves insertion order and reports extra +keys separately. From 54eff4358ff98f2846082455c6df8c4809e4bd95 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 5 Jan 2026 00:39:59 +0530 Subject: [PATCH 5/7] Fix ambiguous variable name in dict assertion regression test --- testing/test_assertion.py | 69 ++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index c9eb41d1654..ce5c8bb89a5 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -374,7 +374,8 @@ def test_other(): def test_register_assert_rewrite_checks_types(self) -> None: with pytest.raises(TypeError): - pytest.register_assert_rewrite(["pytest_tests_internal_non_existing"]) # type: ignore + pytest.register_assert_rewrite( + ["pytest_tests_internal_non_existing"]) # type: ignore pytest.register_assert_rewrite( "pytest_tests_internal_non_existing", "pytest_tests_internal_non_existing2" ) @@ -451,8 +452,10 @@ def test_dict_preserves_insertion_order_regression(self) -> None: ) # insertion order preserved inside left dict - env_index = next(i for i, l in enumerate(diff_block) if "'env': {" in l) - closing_index = next(i for i, l in enumerate(diff_block) if l.strip() == "}") + env_index = next(i for i, line in enumerate( + diff_block) if "'env': {" in line) + closing_index = next(i for i, line in enumerate( + diff_block) if line.strip() == "}") assert env_index < closing_index @@ -580,7 +583,8 @@ def test_iterable_full_diff(self, left, right, expected) -> None: assert expl[-1] == "Use -v to get more diff" verbose_expl = callequal(left, right, verbose=1) assert verbose_expl is not None - assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip()) + assert "\n".join(verbose_expl).endswith( + textwrap.dedent(expected).strip()) def test_iterable_quiet(self) -> None: expl = callequal([1, 2], [10, 2], verbose=-1) @@ -726,7 +730,8 @@ def test_dict_wrap(self) -> None: ] long_a = "a" * 80 - sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 3}} + sub = {"long_a": long_a, "sub1": { + "long_a": "substring that gets wrapped " * 3}} d1 = {"env": {"sub": sub}} d2 = {"env": {"sub": sub}, "new": 1} diff = callequal(d1, d2, verbose=True) @@ -1026,7 +1031,8 @@ def test_dataclasses(self, pytester: Pytester) -> None: ) def test_recursive_dataclasses(self, pytester: Pytester) -> None: - p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") + p = pytester.copy_example( + "dataclasses/test_compare_recursive_dataclasses.py") result = pytester.runpytest(p) result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1044,7 +1050,8 @@ def test_recursive_dataclasses(self, pytester: Pytester) -> None: ) def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None: - p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") + p = pytester.copy_example( + "dataclasses/test_compare_recursive_dataclasses.py") result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1074,7 +1081,8 @@ def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None: ) def test_dataclasses_verbose(self, pytester: Pytester) -> None: - p = pytester.copy_example("dataclasses/test_compare_dataclasses_verbose.py") + p = pytester.copy_example( + "dataclasses/test_compare_dataclasses_verbose.py") result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1326,12 +1334,14 @@ def test_fmt_where(self) -> None: def test_fmt_and(self) -> None: expl = "\n".join(["assert 1", "{1 = foo", "} == 2", "{2 = bar", "}"]) - res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + and 2 = bar"]) + res = "\n".join( + ["assert 1 == 2", " + where 1 = foo", " + and 2 = bar"]) assert util.format_explanation(expl) == res def test_fmt_where_nested(self) -> None: expl = "\n".join(["assert 1", "{1 = foo", "{foo = bar", "}", "} == 2"]) - res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + where foo = bar"]) + res = "\n".join( + ["assert 1 == 2", " + where 1 = foo", " + where foo = bar"]) assert util.format_explanation(expl) == res def test_fmt_newline(self) -> None: @@ -1396,17 +1406,20 @@ class TestTruncateExplanation: def test_doesnt_truncate_when_input_is_empty_list(self) -> None: expl: list[str] = [] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=100) assert result == expl def test_doesnt_truncate_at_when_input_is_5_lines_and_LT_max_chars(self) -> None: expl = ["a" * 100 for x in range(5)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=8 * 80) assert result == expl def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: expl = ["" for x in range(50)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=100) assert len(result) != len(expl) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG @@ -1418,7 +1431,8 @@ def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: total_lines = 100 expl = ["a" for x in range(total_lines)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=8 * 80) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1429,7 +1443,8 @@ def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: def test_truncates_at_8_lines_when_there_is_one_line_to_remove(self) -> None: """The number of line in the result is 9, the same number as if we truncated.""" expl = ["a" for x in range(9)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=8 * 80) assert result == expl assert "truncated" not in result[-1] @@ -1438,7 +1453,8 @@ def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_ ) -> None: line = "a" * 10 expl = [line, line] - result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10) + result = truncate._truncate_explanation( + expl, max_lines=10, max_chars=10) assert result == [line, line] def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_lines( @@ -1446,12 +1462,14 @@ def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_ ) -> None: line = "a" * 10 expl = [line, line] - result = truncate._truncate_explanation(expl, max_lines=1, max_chars=100) + result = truncate._truncate_explanation( + expl, max_lines=1, max_chars=100) assert result == [line, line] def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: expl = [chr(97 + x) * 80 for x in range(16)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=8 * 80) assert result != expl assert len(result) == 16 - 8 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1461,7 +1479,8 @@ def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None: expl = ["a" * 250 for x in range(10)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=999) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=999) assert result != expl assert len(result) == 4 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1471,7 +1490,8 @@ def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None: def test_truncates_at_1_line_when_first_line_is_GT_max_chars(self) -> None: expl = ["a" * 250 for x in range(1000)] - result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation( + expl, max_lines=8, max_chars=100) assert result != expl assert len(result) == 1 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1713,12 +1733,14 @@ def test_hello(): def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None: pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"]) a = pytester.mkdir("a") - a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2", encoding="utf-8") + a.joinpath("test_a.py").write_text( + "def test_a(): assert 1 == 2", encoding="utf-8") a.joinpath("conftest.py").write_text( 'def pytest_assertrepr_compare(): return ["summary a"]', encoding="utf-8" ) b = pytester.mkdir("b") - b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2", encoding="utf-8") + b.joinpath("test_b.py").write_text( + "def test_b(): assert 1 == 2", encoding="utf-8") b.joinpath("conftest.py").write_text( 'def pytest_assertrepr_compare(): return ["summary b"]', encoding="utf-8" ) @@ -2010,7 +2032,8 @@ def test_raising_repr(): """ ) result = pytester.runpytest() - result.stdout.fnmatch_lines(["E AssertionError: "]) + result.stdout.fnmatch_lines( + ["E AssertionError: "]) def test_issue_1944(pytester: Pytester) -> None: From 225585ad60c4cb4772ba690b45b5d7dc6c04957c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 19:10:19 +0000 Subject: [PATCH 6/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_assertion.py | 71 ++++++++++++++------------------------- 1 file changed, 25 insertions(+), 46 deletions(-) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index ce5c8bb89a5..2e30be655dd 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -374,8 +374,7 @@ def test_other(): def test_register_assert_rewrite_checks_types(self) -> None: with pytest.raises(TypeError): - pytest.register_assert_rewrite( - ["pytest_tests_internal_non_existing"]) # type: ignore + pytest.register_assert_rewrite(["pytest_tests_internal_non_existing"]) # type: ignore pytest.register_assert_rewrite( "pytest_tests_internal_non_existing", "pytest_tests_internal_non_existing2" ) @@ -452,10 +451,10 @@ def test_dict_preserves_insertion_order_regression(self) -> None: ) # insertion order preserved inside left dict - env_index = next(i for i, line in enumerate( - diff_block) if "'env': {" in line) - closing_index = next(i for i, line in enumerate( - diff_block) if line.strip() == "}") + env_index = next(i for i, line in enumerate(diff_block) if "'env': {" in line) + closing_index = next( + i for i, line in enumerate(diff_block) if line.strip() == "}" + ) assert env_index < closing_index @@ -583,8 +582,7 @@ def test_iterable_full_diff(self, left, right, expected) -> None: assert expl[-1] == "Use -v to get more diff" verbose_expl = callequal(left, right, verbose=1) assert verbose_expl is not None - assert "\n".join(verbose_expl).endswith( - textwrap.dedent(expected).strip()) + assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip()) def test_iterable_quiet(self) -> None: expl = callequal([1, 2], [10, 2], verbose=-1) @@ -730,8 +728,7 @@ def test_dict_wrap(self) -> None: ] long_a = "a" * 80 - sub = {"long_a": long_a, "sub1": { - "long_a": "substring that gets wrapped " * 3}} + sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 3}} d1 = {"env": {"sub": sub}} d2 = {"env": {"sub": sub}, "new": 1} diff = callequal(d1, d2, verbose=True) @@ -1031,8 +1028,7 @@ def test_dataclasses(self, pytester: Pytester) -> None: ) def test_recursive_dataclasses(self, pytester: Pytester) -> None: - p = pytester.copy_example( - "dataclasses/test_compare_recursive_dataclasses.py") + p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") result = pytester.runpytest(p) result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1050,8 +1046,7 @@ def test_recursive_dataclasses(self, pytester: Pytester) -> None: ) def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None: - p = pytester.copy_example( - "dataclasses/test_compare_recursive_dataclasses.py") + p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1081,8 +1076,7 @@ def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None: ) def test_dataclasses_verbose(self, pytester: Pytester) -> None: - p = pytester.copy_example( - "dataclasses/test_compare_dataclasses_verbose.py") + p = pytester.copy_example("dataclasses/test_compare_dataclasses_verbose.py") result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( @@ -1334,14 +1328,12 @@ def test_fmt_where(self) -> None: def test_fmt_and(self) -> None: expl = "\n".join(["assert 1", "{1 = foo", "} == 2", "{2 = bar", "}"]) - res = "\n".join( - ["assert 1 == 2", " + where 1 = foo", " + and 2 = bar"]) + res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + and 2 = bar"]) assert util.format_explanation(expl) == res def test_fmt_where_nested(self) -> None: expl = "\n".join(["assert 1", "{1 = foo", "{foo = bar", "}", "} == 2"]) - res = "\n".join( - ["assert 1 == 2", " + where 1 = foo", " + where foo = bar"]) + res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + where foo = bar"]) assert util.format_explanation(expl) == res def test_fmt_newline(self) -> None: @@ -1406,20 +1398,17 @@ class TestTruncateExplanation: def test_doesnt_truncate_when_input_is_empty_list(self) -> None: expl: list[str] = [] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result == expl def test_doesnt_truncate_at_when_input_is_5_lines_and_LT_max_chars(self) -> None: expl = ["a" * 100 for x in range(5)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result == expl def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: expl = ["" for x in range(50)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert len(result) != len(expl) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG @@ -1431,8 +1420,7 @@ def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: total_lines = 100 expl = ["a" for x in range(total_lines)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1443,8 +1431,7 @@ def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: def test_truncates_at_8_lines_when_there_is_one_line_to_remove(self) -> None: """The number of line in the result is 9, the same number as if we truncated.""" expl = ["a" for x in range(9)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result == expl assert "truncated" not in result[-1] @@ -1453,8 +1440,7 @@ def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_ ) -> None: line = "a" * 10 expl = [line, line] - result = truncate._truncate_explanation( - expl, max_lines=10, max_chars=10) + result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10) assert result == [line, line] def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_lines( @@ -1462,14 +1448,12 @@ def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_ ) -> None: line = "a" * 10 expl = [line, line] - result = truncate._truncate_explanation( - expl, max_lines=1, max_chars=100) + result = truncate._truncate_explanation(expl, max_lines=1, max_chars=100) assert result == [line, line] def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: expl = [chr(97 + x) * 80 for x in range(16)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=8 * 80) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result != expl assert len(result) == 16 - 8 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1479,8 +1463,7 @@ def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None: expl = ["a" * 250 for x in range(10)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=999) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=999) assert result != expl assert len(result) == 4 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1490,8 +1473,7 @@ def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None: def test_truncates_at_1_line_when_first_line_is_GT_max_chars(self) -> None: expl = ["a" * 250 for x in range(1000)] - result = truncate._truncate_explanation( - expl, max_lines=8, max_chars=100) + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result != expl assert len(result) == 1 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] @@ -1733,14 +1715,12 @@ def test_hello(): def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None: pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"]) a = pytester.mkdir("a") - a.joinpath("test_a.py").write_text( - "def test_a(): assert 1 == 2", encoding="utf-8") + a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2", encoding="utf-8") a.joinpath("conftest.py").write_text( 'def pytest_assertrepr_compare(): return ["summary a"]', encoding="utf-8" ) b = pytester.mkdir("b") - b.joinpath("test_b.py").write_text( - "def test_b(): assert 1 == 2", encoding="utf-8") + b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2", encoding="utf-8") b.joinpath("conftest.py").write_text( 'def pytest_assertrepr_compare(): return ["summary b"]', encoding="utf-8" ) @@ -2032,8 +2012,7 @@ def test_raising_repr(): """ ) result = pytester.runpytest() - result.stdout.fnmatch_lines( - ["E AssertionError: "]) + result.stdout.fnmatch_lines(["E AssertionError: "]) def test_issue_1944(pytester: Pytester) -> None: From dd874a04967e681a5a54d3aa8ba58bd6fd55321f Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 5 Jan 2026 00:44:33 +0530 Subject: [PATCH 7/7] Add changelog entry for dict assertion insertion order fix --- changelog/assert-dict-insertion-order.rst | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 changelog/assert-dict-insertion-order.rst diff --git a/changelog/assert-dict-insertion-order.rst b/changelog/assert-dict-insertion-order.rst deleted file mode 100644 index cf65868238c..00000000000 --- a/changelog/assert-dict-insertion-order.rst +++ /dev/null @@ -1,6 +0,0 @@ -Fix assertion diff output to preserve dictionary insertion order. - -When comparing dictionaries with extra keys, pytest could incorrectly inject -those keys into the structured diff output, producing misleading results. -The assertion diff now correctly preserves insertion order and reports extra -keys separately.