Skip to content

Commit 88ae27d

Browse files
Add syntactic highlights to the error explanations (#11661)
* Put a 'reset' color in front of the highlighting When doing the highlighting, some lexers will not set the initial color explicitly, which may lead to the red from the errors being propagated to the start of the expression * Add syntactic highlighting to the error explanations This updates the various error reporting to highlight python code when displayed, to increase readability and make it easier to understand
1 parent e06a3d0 commit 88ae27d

File tree

6 files changed

+96
-46
lines changed

6 files changed

+96
-46
lines changed

changelog/11520.improvement.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
Improved very verbose diff output to color it as a diff instead of only red.
22

33
Improved the error reporting to better separate each section.
4+
5+
Improved the error reporting to syntax-highlight Python code when Pygments is available.

src/_pytest/_io/terminalwriter.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,15 @@ def _highlight(
223223
style=os.getenv("PYTEST_THEME"),
224224
),
225225
)
226-
return highlighted
226+
# pygments terminal formatter may add a newline when there wasn't one.
227+
# We don't want this, remove.
228+
if highlighted[-1] == "\n" and source[-1] != "\n":
229+
highlighted = highlighted[:-1]
230+
231+
# Some lexers will not set the initial color explicitly
232+
# which may lead to the previous color being propagated to the
233+
# start of the expression, so reset first.
234+
return "\x1b[0m" + highlighted
227235
except pygments.util.ClassNotFound:
228236
raise UsageError(
229237
"PYTEST_THEME environment variable had an invalid value: '{}'. "

src/_pytest/assertion/util.py

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,12 @@ def assertrepr_compare(
192192
right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
193193

194194
summary = f"{left_repr} {op} {right_repr}"
195+
highlighter = config.get_terminal_writer()._highlight
195196

196197
explanation = None
197198
try:
198199
if op == "==":
199-
writer = config.get_terminal_writer()
200-
explanation = _compare_eq_any(left, right, writer._highlight, verbose)
200+
explanation = _compare_eq_any(left, right, highlighter, verbose)
201201
elif op == "not in":
202202
if istext(left) and istext(right):
203203
explanation = _notin_text(left, right, verbose)
@@ -206,16 +206,16 @@ def assertrepr_compare(
206206
explanation = ["Both sets are equal"]
207207
elif op == ">=":
208208
if isset(left) and isset(right):
209-
explanation = _compare_gte_set(left, right, verbose)
209+
explanation = _compare_gte_set(left, right, highlighter, verbose)
210210
elif op == "<=":
211211
if isset(left) and isset(right):
212-
explanation = _compare_lte_set(left, right, verbose)
212+
explanation = _compare_lte_set(left, right, highlighter, verbose)
213213
elif op == ">":
214214
if isset(left) and isset(right):
215-
explanation = _compare_gt_set(left, right, verbose)
215+
explanation = _compare_gt_set(left, right, highlighter, verbose)
216216
elif op == "<":
217217
if isset(left) and isset(right):
218-
explanation = _compare_lt_set(left, right, verbose)
218+
explanation = _compare_lt_set(left, right, highlighter, verbose)
219219

220220
except outcomes.Exit:
221221
raise
@@ -259,11 +259,11 @@ def _compare_eq_any(
259259
# used in older code bases before dataclasses/attrs were available.
260260
explanation = _compare_eq_cls(left, right, highlighter, verbose)
261261
elif issequence(left) and issequence(right):
262-
explanation = _compare_eq_sequence(left, right, verbose)
262+
explanation = _compare_eq_sequence(left, right, highlighter, verbose)
263263
elif isset(left) and isset(right):
264-
explanation = _compare_eq_set(left, right, verbose)
264+
explanation = _compare_eq_set(left, right, highlighter, verbose)
265265
elif isdict(left) and isdict(right):
266-
explanation = _compare_eq_dict(left, right, verbose)
266+
explanation = _compare_eq_dict(left, right, highlighter, verbose)
267267

268268
if isiterable(left) and isiterable(right):
269269
expl = _compare_eq_iterable(left, right, highlighter, verbose)
@@ -350,7 +350,10 @@ def _compare_eq_iterable(
350350

351351

352352
def _compare_eq_sequence(
353-
left: Sequence[Any], right: Sequence[Any], verbose: int = 0
353+
left: Sequence[Any],
354+
right: Sequence[Any],
355+
highlighter: _HighlightFunc,
356+
verbose: int = 0,
354357
) -> List[str]:
355358
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
356359
explanation: List[str] = []
@@ -373,7 +376,10 @@ def _compare_eq_sequence(
373376
left_value = left[i]
374377
right_value = right[i]
375378

376-
explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"]
379+
explanation.append(
380+
f"At index {i} diff:"
381+
f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}"
382+
)
377383
break
378384

379385
if comparing_bytes:
@@ -393,68 +399,91 @@ def _compare_eq_sequence(
393399
extra = saferepr(right[len_left])
394400

395401
if len_diff == 1:
396-
explanation += [f"{dir_with_more} contains one more item: {extra}"]
402+
explanation += [
403+
f"{dir_with_more} contains one more item: {highlighter(extra)}"
404+
]
397405
else:
398406
explanation += [
399407
"%s contains %d more items, first extra item: %s"
400-
% (dir_with_more, len_diff, extra)
408+
% (dir_with_more, len_diff, highlighter(extra))
401409
]
402410
return explanation
403411

404412

405413
def _compare_eq_set(
406-
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
414+
left: AbstractSet[Any],
415+
right: AbstractSet[Any],
416+
highlighter: _HighlightFunc,
417+
verbose: int = 0,
407418
) -> List[str]:
408419
explanation = []
409-
explanation.extend(_set_one_sided_diff("left", left, right))
410-
explanation.extend(_set_one_sided_diff("right", right, left))
420+
explanation.extend(_set_one_sided_diff("left", left, right, highlighter))
421+
explanation.extend(_set_one_sided_diff("right", right, left, highlighter))
411422
return explanation
412423

413424

414425
def _compare_gt_set(
415-
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
426+
left: AbstractSet[Any],
427+
right: AbstractSet[Any],
428+
highlighter: _HighlightFunc,
429+
verbose: int = 0,
416430
) -> List[str]:
417-
explanation = _compare_gte_set(left, right, verbose)
431+
explanation = _compare_gte_set(left, right, highlighter)
418432
if not explanation:
419433
return ["Both sets are equal"]
420434
return explanation
421435

422436

423437
def _compare_lt_set(
424-
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
438+
left: AbstractSet[Any],
439+
right: AbstractSet[Any],
440+
highlighter: _HighlightFunc,
441+
verbose: int = 0,
425442
) -> List[str]:
426-
explanation = _compare_lte_set(left, right, verbose)
443+
explanation = _compare_lte_set(left, right, highlighter)
427444
if not explanation:
428445
return ["Both sets are equal"]
429446
return explanation
430447

431448

432449
def _compare_gte_set(
433-
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
450+
left: AbstractSet[Any],
451+
right: AbstractSet[Any],
452+
highlighter: _HighlightFunc,
453+
verbose: int = 0,
434454
) -> List[str]:
435-
return _set_one_sided_diff("right", right, left)
455+
return _set_one_sided_diff("right", right, left, highlighter)
436456

437457

438458
def _compare_lte_set(
439-
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
459+
left: AbstractSet[Any],
460+
right: AbstractSet[Any],
461+
highlighter: _HighlightFunc,
462+
verbose: int = 0,
440463
) -> List[str]:
441-
return _set_one_sided_diff("left", left, right)
464+
return _set_one_sided_diff("left", left, right, highlighter)
442465

443466

444467
def _set_one_sided_diff(
445-
posn: str, set1: AbstractSet[Any], set2: AbstractSet[Any]
468+
posn: str,
469+
set1: AbstractSet[Any],
470+
set2: AbstractSet[Any],
471+
highlighter: _HighlightFunc,
446472
) -> List[str]:
447473
explanation = []
448474
diff = set1 - set2
449475
if diff:
450476
explanation.append(f"Extra items in the {posn} set:")
451477
for item in diff:
452-
explanation.append(saferepr(item))
478+
explanation.append(highlighter(saferepr(item)))
453479
return explanation
454480

455481

456482
def _compare_eq_dict(
457-
left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0
483+
left: Mapping[Any, Any],
484+
right: Mapping[Any, Any],
485+
highlighter: _HighlightFunc,
486+
verbose: int = 0,
458487
) -> List[str]:
459488
explanation: List[str] = []
460489
set_left = set(left)
@@ -465,12 +494,16 @@ def _compare_eq_dict(
465494
explanation += ["Omitting %s identical items, use -vv to show" % len(same)]
466495
elif same:
467496
explanation += ["Common items:"]
468-
explanation += pprint.pformat(same).splitlines()
497+
explanation += highlighter(pprint.pformat(same)).splitlines()
469498
diff = {k for k in common if left[k] != right[k]}
470499
if diff:
471500
explanation += ["Differing items:"]
472501
for k in diff:
473-
explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})]
502+
explanation += [
503+
highlighter(saferepr({k: left[k]}))
504+
+ " != "
505+
+ highlighter(saferepr({k: right[k]}))
506+
]
474507
extra_left = set_left - set_right
475508
len_extra_left = len(extra_left)
476509
if len_extra_left:
@@ -479,7 +512,7 @@ def _compare_eq_dict(
479512
% (len_extra_left, "" if len_extra_left == 1 else "s")
480513
)
481514
explanation.extend(
482-
pprint.pformat({k: left[k] for k in extra_left}).splitlines()
515+
highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines()
483516
)
484517
extra_right = set_right - set_left
485518
len_extra_right = len(extra_right)
@@ -489,7 +522,7 @@ def _compare_eq_dict(
489522
% (len_extra_right, "" if len_extra_right == 1 else "s")
490523
)
491524
explanation.extend(
492-
pprint.pformat({k: right[k] for k in extra_right}).splitlines()
525+
highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines()
493526
)
494527
return explanation
495528

@@ -528,17 +561,17 @@ def _compare_eq_cls(
528561
explanation.append("Omitting %s identical items, use -vv to show" % len(same))
529562
elif same:
530563
explanation += ["Matching attributes:"]
531-
explanation += pprint.pformat(same).splitlines()
564+
explanation += highlighter(pprint.pformat(same)).splitlines()
532565
if diff:
533566
explanation += ["Differing attributes:"]
534-
explanation += pprint.pformat(diff).splitlines()
567+
explanation += highlighter(pprint.pformat(diff)).splitlines()
535568
for field in diff:
536569
field_left = getattr(left, field)
537570
field_right = getattr(right, field)
538571
explanation += [
539572
"",
540-
"Drill down into differing attribute %s:" % field,
541-
("%s%s: %r != %r") % (indent, field, field_left, field_right),
573+
f"Drill down into differing attribute {field}:",
574+
f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}",
542575
]
543576
explanation += [
544577
indent + line

testing/io/test_terminalwriter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ def test_combining(self) -> None:
254254
pytest.param(
255255
True,
256256
True,
257-
"{kw}assert{hl-reset} {number}0{hl-reset}{endline}\n",
257+
"{reset}{kw}assert{hl-reset} {number}0{hl-reset}{endline}\n",
258258
id="with markup and code_highlight",
259259
),
260260
pytest.param(

testing/test_assertion.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
def mock_config(verbose: int = 0, assertion_override: Optional[int] = None):
2222
class TerminalWriter:
23-
def _highlight(self, source, lexer):
23+
def _highlight(self, source, lexer="python"):
2424
return source
2525

2626
class Config:
@@ -1933,6 +1933,7 @@ def test():
19331933
assert [0, 1] == [0, 2]
19341934
""",
19351935
[
1936+
"{bold}{red}E At index 1 diff: {reset}{number}1{hl-reset}{endline} != {reset}{number}2*",
19361937
"{bold}{red}E {light-red}- 2,{hl-reset}{endline}{reset}",
19371938
"{bold}{red}E {light-green}+ 1,{hl-reset}{endline}{reset}",
19381939
],
@@ -1945,7 +1946,13 @@ def test():
19451946
}
19461947
""",
19471948
[
1948-
"{bold}{red}E {light-gray} {hl-reset} {{{endline}{reset}",
1949+
"{bold}{red}E Common items:{reset}",
1950+
"{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-1{hl-reset}{str}'{hl-reset}: {number}1*",
1951+
"{bold}{red}E Left contains 1 more item:{reset}",
1952+
"{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-5{hl-reset}{str}'{hl-reset}: {number}5*",
1953+
"{bold}{red}E Right contains 1 more item:{reset}",
1954+
"{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-0{hl-reset}{str}'{hl-reset}: {number}0*",
1955+
"{bold}{red}E {reset}{light-gray} {hl-reset} {{{endline}{reset}",
19491956
"{bold}{red}E {light-gray} {hl-reset} 'number-is-1': 1,{endline}{reset}",
19501957
"{bold}{red}E {light-green}+ 'number-is-5': 5,{hl-reset}{endline}{reset}",
19511958
],

testing/test_terminal.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,13 +1268,13 @@ def test_this():
12681268
"=*= FAILURES =*=",
12691269
"{red}{bold}_*_ test_this _*_{reset}",
12701270
"",
1271-
" {kw}def{hl-reset} {function}test_this{hl-reset}():{endline}",
1271+
" {reset}{kw}def{hl-reset} {function}test_this{hl-reset}():{endline}",
12721272
"> fail(){endline}",
12731273
"",
12741274
"{bold}{red}test_color_yes.py{reset}:5: ",
12751275
"_ _ * _ _*",
12761276
"",
1277-
" {kw}def{hl-reset} {function}fail{hl-reset}():{endline}",
1277+
" {reset}{kw}def{hl-reset} {function}fail{hl-reset}():{endline}",
12781278
"> {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
12791279
"{bold}{red}E assert 0{reset}",
12801280
"",
@@ -1295,9 +1295,9 @@ def test_this():
12951295
"=*= FAILURES =*=",
12961296
"{red}{bold}_*_ test_this _*_{reset}",
12971297
"{bold}{red}test_color_yes.py{reset}:5: in test_this",
1298-
" fail(){endline}",
1298+
" {reset}fail(){endline}",
12991299
"{bold}{red}test_color_yes.py{reset}:2: in fail",
1300-
" {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
1300+
" {reset}{kw}assert{hl-reset} {number}0{hl-reset}{endline}",
13011301
"{bold}{red}E assert 0{reset}",
13021302
"{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}",
13031303
]
@@ -2507,7 +2507,7 @@ def test_foo():
25072507
result.stdout.fnmatch_lines(
25082508
color_mapping.format_for_fnmatch(
25092509
[
2510-
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
2510+
" {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
25112511
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
25122512
"{bold}{red}E assert 1 == 10{reset}",
25132513
]
@@ -2529,7 +2529,7 @@ def test_foo():
25292529
result.stdout.fnmatch_lines(
25302530
color_mapping.format_for_fnmatch(
25312531
[
2532-
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
2532+
" {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
25332533
" {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}",
25342534
"> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
25352535
"{bold}{red}E assert 0{reset}",
@@ -2552,7 +2552,7 @@ def test_foo():
25522552
result.stdout.fnmatch_lines(
25532553
color_mapping.format_for_fnmatch(
25542554
[
2555-
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
2555+
" {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
25562556
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
25572557
"{bold}{red}E assert 1 == 10{reset}",
25582558
]

0 commit comments

Comments
 (0)