2222from _pytest ._io .saferepr import saferepr_unlimited
2323from _pytest .compat import running_on_ci
2424from _pytest .config import Config
25+ from _pytest .config import UsageError
2526
2627
2728# The _reprcompare attribute on the util module is used by the new assertion
3738# Config object which is assigned during pytest_runtest_protocol.
3839_config : Config | None = None
3940
41+ ASSERTION_TEXT_DIFF_STYLE_INI = "assertion_text_diff_style"
42+ ASSERTION_TEXT_DIFF_STYLE_NDIFF = "ndiff"
43+ ASSERTION_TEXT_DIFF_STYLE_BLOCK = "block"
44+ ASSERTION_TEXT_DIFF_STYLE_CHOICES = (
45+ ASSERTION_TEXT_DIFF_STYLE_NDIFF ,
46+ ASSERTION_TEXT_DIFF_STYLE_BLOCK ,
47+ )
48+
4049
4150class _HighlightFunc (Protocol ):
4251 def __call__ (self , source : str , lexer : Literal ["diff" , "python" ] = "python" ) -> str :
@@ -51,6 +60,20 @@ def dummy_highlighter(source: str, lexer: Literal["diff", "python"] = "python")
5160 return source
5261
5362
63+ def get_assertion_text_diff_style (config : Config ) -> str :
64+ style = config .getini (ASSERTION_TEXT_DIFF_STYLE_INI )
65+ if style not in ASSERTION_TEXT_DIFF_STYLE_CHOICES :
66+ choices = ", " .join (repr (choice ) for choice in ASSERTION_TEXT_DIFF_STYLE_CHOICES )
67+ raise UsageError (
68+ f"{ ASSERTION_TEXT_DIFF_STYLE_INI } must be one of { choices } ; got { style !r} "
69+ )
70+ return style
71+
72+
73+ def validate_assertion_text_diff_style (config : Config ) -> None :
74+ get_assertion_text_diff_style (config )
75+
76+
5477def format_explanation (explanation : str ) -> str :
5578 r"""Format an explanation.
5679
@@ -180,6 +203,7 @@ def assertrepr_compare(
180203) -> list [str ] | None :
181204 """Return specialised explanations for some operators/operands."""
182205 verbose = config .get_verbosity (Config .VERBOSITY_ASSERTIONS )
206+ assertion_text_diff_style = get_assertion_text_diff_style (config )
183207
184208 # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
185209 # See issue #3246.
@@ -208,7 +232,13 @@ def assertrepr_compare(
208232 explanation = None
209233 try :
210234 if op == "==" :
211- explanation = _compare_eq_any (left , right , highlighter , verbose )
235+ explanation = _compare_eq_any (
236+ left ,
237+ right ,
238+ highlighter ,
239+ verbose ,
240+ assertion_text_diff_style ,
241+ )
212242 elif op == "not in" :
213243 if istext (left ) and istext (right ):
214244 explanation = _notin_text (left , right , verbose )
@@ -246,11 +276,21 @@ def assertrepr_compare(
246276
247277
248278def _compare_eq_any (
249- left : Any , right : Any , highlighter : _HighlightFunc , verbose : int = 0
279+ left : Any ,
280+ right : Any ,
281+ highlighter : _HighlightFunc ,
282+ verbose : int = 0 ,
283+ assertion_text_diff_style : str = ASSERTION_TEXT_DIFF_STYLE_NDIFF ,
250284) -> list [str ]:
251285 explanation = []
252286 if istext (left ) and istext (right ):
253- explanation = _diff_text (left , right , highlighter , verbose )
287+ explanation = _compare_eq_text (
288+ left ,
289+ right ,
290+ highlighter ,
291+ verbose ,
292+ assertion_text_diff_style ,
293+ )
254294 else :
255295 from _pytest .python_api import ApproxBase
256296
@@ -282,6 +322,40 @@ def _compare_eq_any(
282322 return explanation
283323
284324
325+ def _compare_eq_text (
326+ left : str ,
327+ right : str ,
328+ highlighter : _HighlightFunc ,
329+ verbose : int ,
330+ assertion_text_diff_style : str ,
331+ ) -> list [str ]:
332+ if (
333+ assertion_text_diff_style == ASSERTION_TEXT_DIFF_STYLE_BLOCK
334+ and _is_multiline_text (left , right )
335+ and not (left .isspace () or right .isspace ())
336+ ):
337+ return _diff_text_block (left , right )
338+ return _diff_text (left , right , highlighter , verbose )
339+
340+
341+ def _is_multiline_text (* texts : str ) -> bool :
342+ return any ("\n " in text or "\r " in text for text in texts )
343+
344+
345+ def _diff_text_block (left : str , right : str ) -> list [str ]:
346+ return [
347+ "Left:" ,
348+ * _format_text_block_lines (left ),
349+ "" ,
350+ "Right:" ,
351+ * _format_text_block_lines (right ),
352+ ]
353+
354+
355+ def _format_text_block_lines (text : str ) -> list [str ]:
356+ return [f" { line } " for line in text .split ("\n " )]
357+
358+
285359def _diff_text (
286360 left : str , right : str , highlighter : _HighlightFunc , verbose : int = 0
287361) -> list [str ]:
0 commit comments