Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e720397
Add datetime/timedelta support to pytest.approx (#8395)
hamza-mobeen Apr 23, 2026
5a2a014
Add block text diffs
hamza-mobeen Apr 27, 2026
2a40e38
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 27, 2026
ef90179
Fix pre-commit lint and mypy errors
hamza-mobeen Apr 27, 2026
b9e20c4
Merge branch 'main' into codex/assertion-text-diff-blocks
hamza-mobeen Apr 27, 2026
499aaa9
Add test to cover getini KeyError in mock_config
hamza-mobeen Apr 27, 2026
e5f209e
Merge main
hamza-mobeen May 8, 2026
84dc2f4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 8, 2026
4529bf4
Merge branch 'main' into codex/assertion-text-diff-blocks
hamza-mobeen May 12, 2026
d093267
Merge branch 'main' into codex/assertion-text-diff-blocks
hamza-mobeen May 14, 2026
c1d79c6
Update doc/en/how-to/output.rst
hamza-mobeen May 18, 2026
588e1a4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 18, 2026
1fe4671
Update src/_pytest/assertion/util.py
hamza-mobeen May 18, 2026
68564ae
Update doc/en/reference/reference.rst
hamza-mobeen May 18, 2026
6ab0d7d
Update doc/en/reference/reference.rst
hamza-mobeen May 18, 2026
b9d11e8
Address assertion text diff style review
hamza-mobeen May 18, 2026
f9bf927
Merge branch 'main' into codex/assertion-text-diff-blocks
hamza-mobeen May 18, 2026
0eafb5f
Cover ndiff assertion text diff style
hamza-mobeen May 18, 2026
4e11a6b
Apply suggestions from code review
nicoddemus May 18, 2026
c051132
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 18, 2026
4cc27d7
Update util.py
nicoddemus May 18, 2026
8a69623
Apply suggestion from @nicoddemus
nicoddemus May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/6757.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added the :confval:`assertion_text_diff_style` configuration option, allowing
string equality failures to be rendered as separate ``Left:`` and ``Right:``
blocks instead of ``ndiff`` output.
7 changes: 7 additions & 0 deletions doc/en/how-to/output.rst
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,13 @@ This is done by setting a verbosity level in the configuration file for the spec
``pytest --no-header`` with a value of ``2`` would have the same output as the previous example, but each test inside
the file is shown by a single character in the output.

:confval:`assertion_text_diff_style`: Controls how pytest renders ``str == str`` failures.

* ``ndiff`` (the default) outputs the differences using inline diff markers.
* ``block`` prints string comparisons as separate ``Left:`` and ``Right:`` blocks, which can be easier to read when whitespace or indentation differences dominate.

Note that it is possible to set this option (as any other configuration option) directly in the command line using ``-o assertion_text_diff_style=block``.

:confval:`verbosity_test_cases`: Controls how verbose the test execution output should be when pytest is executed.
Running ``pytest --no-header`` with a value of ``2`` would have the same output as the first verbosity example, but each
test inside the file gets its own line in the output.
Expand Down
26 changes: 26 additions & 0 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2699,6 +2699,32 @@ passed multiple times. The expected format is ``name=value``. For example::
A special value of ``"auto"`` can be used to explicitly use the global verbosity level.


.. confval:: assertion_text_diff_style
:type: ``str``
:default: ``"ndiff"``

Set how pytest renders diffs for string equality assertions.

Supported values are:

* ``ndiff``: use the inline diff rendering markers.
* ``block``: render each string in separate ``Left:`` and ``Right:`` blocks.

.. tab:: toml

.. code-block:: toml

[pytest]
assertion_text_diff_style = "block"

.. tab:: ini

.. code-block:: ini

[pytest]
assertion_text_diff_style = block


.. confval:: verbosity_subtests
:type: ``str``
:default: ``"auto"``
Expand Down
14 changes: 14 additions & 0 deletions src/_pytest/assertion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ def pytest_addoption(parser: Parser) -> None:
default=None,
help=("Set threshold of CHARS after which truncation will take effect"),
)
parser.addini(
"assertion_text_diff_style",
default=util.ASSERTION_TEXT_DIFF_STYLE_NDIFF,
help=(
"Choose how pytest renders diffs for string equality assertions: "
f"{util.ASSERTION_TEXT_DIFF_STYLE_NDIFF} or "
f"{util.ASSERTION_TEXT_DIFF_STYLE_BLOCK}"
),
)

Config._add_verbosity_ini(
parser,
Expand All @@ -68,6 +77,10 @@ def pytest_addoption(parser: Parser) -> None:
)


def pytest_configure(config: Config) -> None:
util.validate_assertion_text_diff_style(config)


def register_assert_rewrite(*names: str) -> None:
"""Register one or more module names to be rewritten on import.

Expand Down Expand Up @@ -216,4 +229,5 @@ def pytest_assertrepr_compare(
right=right,
verbose=config.get_verbosity(Config.VERBOSITY_ASSERTIONS),
highlighter=highlighter,
assertion_text_diff_style=util.get_assertion_text_diff_style(config),
)
99 changes: 93 additions & 6 deletions src/_pytest/assertion/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from _pytest._io.saferepr import saferepr_unlimited
from _pytest.compat import running_on_ci
from _pytest.config import Config
from _pytest.config import UsageError


# The _reprcompare attribute on the util module is used by the new assertion
Expand All @@ -38,6 +39,15 @@
# Config object which is assigned during pytest_runtest_protocol.
_config: Config | None = None

ASSERTION_TEXT_DIFF_STYLE_INI = "assertion_text_diff_style"
_AssertionTextDiffStyle = Literal["ndiff", "block"]
ASSERTION_TEXT_DIFF_STYLE_NDIFF: Literal["ndiff"] = "ndiff"
ASSERTION_TEXT_DIFF_STYLE_BLOCK: Literal["block"] = "block"
ASSERTION_TEXT_DIFF_STYLE_CHOICES = (
ASSERTION_TEXT_DIFF_STYLE_NDIFF,
ASSERTION_TEXT_DIFF_STYLE_BLOCK,
)


class _HighlightFunc(Protocol):
def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str:
Expand All @@ -52,6 +62,24 @@ def dummy_highlighter(source: str, lexer: Literal["diff", "python"] = "python")
return source


def get_assertion_text_diff_style(config: Config) -> _AssertionTextDiffStyle:
style = str(config.getini(ASSERTION_TEXT_DIFF_STYLE_INI))
match style:
case "ndiff" | "block":
return style
case _:
choices = ", ".join(
repr(choice) for choice in ASSERTION_TEXT_DIFF_STYLE_CHOICES
)
raise UsageError(
f"{ASSERTION_TEXT_DIFF_STYLE_INI} must be one of {choices}; got {style!r}"
)


def validate_assertion_text_diff_style(config: Config) -> None:
get_assertion_text_diff_style(config)


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

Expand Down Expand Up @@ -180,6 +208,7 @@ def assertrepr_compare(
*,
verbose: int,
highlighter: _HighlightFunc,
assertion_text_diff_style: _AssertionTextDiffStyle,
) -> list[str] | None:
"""Return specialised explanations for some operators/operands."""
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
Expand Down Expand Up @@ -208,7 +237,13 @@ def assertrepr_compare(
explanation = None
try:
if op == "==":
explanation = _compare_eq_any(left, right, highlighter, verbose)
explanation = _compare_eq_any(
left,
right,
highlighter,
verbose,
assertion_text_diff_style,
)
elif op == "not in":
if istext(left) and istext(right):
explanation = _notin_text(left, right, verbose)
Expand Down Expand Up @@ -246,11 +281,21 @@ def assertrepr_compare(


def _compare_eq_any(
left: object, right: object, highlighter: _HighlightFunc, verbose: int = 0
left: object,
right: object,
highlighter: _HighlightFunc,
verbose: int,
assertion_text_diff_style: _AssertionTextDiffStyle,
) -> list[str]:
explanation = []
if istext(left) and istext(right):
explanation = _diff_text(left, right, highlighter, verbose)
explanation = _compare_eq_text(
left,
right,
highlighter,
verbose,
assertion_text_diff_style,
)
else:
from _pytest.python_api import ApproxBase

Expand All @@ -266,7 +311,13 @@ def _compare_eq_any(
# field values, not the type or field names. But this branch
# intentionally only handles the same-type case, which was often
# used in older code bases before dataclasses/attrs were available.
explanation = _compare_eq_cls(left, right, highlighter, verbose)
explanation = _compare_eq_cls(
left,
right,
highlighter,
verbose,
assertion_text_diff_style,
)
elif issequence(left) and issequence(right):
explanation = _compare_eq_sequence(left, right, highlighter, verbose)
elif isset(left) and isset(right):
Expand All @@ -281,6 +332,34 @@ def _compare_eq_any(
return explanation


def _compare_eq_text(
left: str,
right: str,
highlighter: _HighlightFunc,
verbose: int,
assertion_text_diff_style: _AssertionTextDiffStyle,
) -> list[str]:
match assertion_text_diff_style:
case "block":
return _diff_text_block(left, right)
case "ndiff":
return _diff_text(left, right, highlighter, verbose)
Comment thread
nicoddemus marked this conversation as resolved.


def _diff_text_block(left: str, right: str) -> list[str]:
return [
"Left:",
*_format_text_block_lines(left),
"",
"Right:",
*_format_text_block_lines(right),
]


def _format_text_block_lines(text: str) -> list[str]:
return [f" {line}" for line in text.split("\n")]
Comment thread
nicoddemus marked this conversation as resolved.


def _diff_text(
left: str, right: str, highlighter: _HighlightFunc, verbose: int = 0
) -> list[str]:
Expand Down Expand Up @@ -541,7 +620,11 @@ def _compare_eq_dict(


def _compare_eq_cls(
left: object, right: object, highlighter: _HighlightFunc, verbose: int
left: object,
right: object,
highlighter: _HighlightFunc,
verbose: int,
assertion_text_diff_style: _AssertionTextDiffStyle,
) -> list[str]:
if not has_default_eq(left):
return []
Expand Down Expand Up @@ -587,7 +670,11 @@ def _compare_eq_cls(
explanation += [
indent + line
for line in _compare_eq_any(
field_left, field_right, highlighter, verbose
field_left,
field_right,
highlighter,
verbose,
assertion_text_diff_style,
)
]
return explanation
Expand Down
Loading
Loading