Coverage for /home/crpier/Projects/snektest/snektest/presenter/diff.py: 16%
81 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 00:30 +0300
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 00:30 +0300
1import difflib
2import pprint
3from collections.abc import Callable
4from typing import Any, cast
6from rich.console import Console
8from snektest.models import AssertionFailure
11def render_assertion_failure(
12 console: Console,
13 exc: AssertionFailure,
14 *,
15 ndiff_func: Callable[[list[str], list[str]], Any] = difflib.ndiff,
16) -> None:
17 """Pretty-print an AssertionFailure using Rich, styled like pytest."""
18 actual = exc.actual
19 expected = exc.expected
21 console.print(f"E {exc.args[0]}", style="red", markup=False)
23 if isinstance(actual, list) and isinstance(expected, list):
24 # I'm just casting list[Unknown] to list[Any] here to please our strict type check rules
25 actual = cast("list[Any]", actual)
26 expected = cast("list[Any]", expected)
27 render_list_diff(console, actual, expected, ndiff_func=ndiff_func)
28 elif isinstance(actual, dict) and isinstance(expected, dict):
29 # I'm just casting dict[Unknown] to list[Any] here to please our strict type check rules
30 actual = cast("dict[Any, Any]", actual)
31 expected = cast("dict[Any, Any]", expected)
32 render_dict_diff(console, actual, expected, ndiff_func=ndiff_func)
33 elif (
34 isinstance(actual, str)
35 and isinstance(expected, str)
36 and ("\n" in actual or "\n" in expected)
37 ):
38 render_multiline_string_diff(
39 console,
40 actual,
41 expected,
42 ndiff_func=ndiff_func,
43 )
44 else:
45 return
48def _first_diff_index(actual: list[Any], expected: list[Any]) -> int | None:
49 for index, (actual_item, expected_item) in enumerate(
50 zip(actual, expected, strict=False)
51 ):
52 if actual_item != expected_item:
53 return index
54 return None
57def _length_mismatch_message(actual_len: int, expected_len: int) -> str | None:
58 if actual_len == expected_len:
59 return None
60 if actual_len > expected_len:
61 return f"E Left contains {actual_len - expected_len} more items"
62 return f"E Right contains {expected_len - actual_len} more items"
65def _print_ndiff(
66 console: Console,
67 expected_lines: list[str],
68 actual_lines: list[str],
69 *,
70 ndiff_func: Callable[[list[str], list[str]], Any] = difflib.ndiff,
71) -> None:
72 style_by_prefix: dict[str, str] = {
73 "- ": "red",
74 "+ ": "green",
75 "? ": "dim red",
76 " ": "red",
77 }
79 for line in ndiff_func(expected_lines, actual_lines):
80 prefix = line[:2]
81 style = style_by_prefix.get(prefix)
82 if style is None:
83 continue
84 console.print(f"E {line}", style=style, markup=False)
87def render_list_diff(
88 console: Console,
89 actual: list[Any],
90 expected: list[Any],
91 *,
92 ndiff_func: Callable[[list[str], list[str]], Any] = difflib.ndiff,
93) -> None:
94 """Render a pytest-like diff for lists."""
95 console.print()
97 diff_idx = _first_diff_index(actual, expected)
98 if diff_idx is not None:
99 console.print(
100 f"E At index {diff_idx} diff: {actual[diff_idx]!r} != {expected[diff_idx]!r}",
101 style="red",
102 markup=False,
103 )
104 else:
105 msg = _length_mismatch_message(len(actual), len(expected))
106 if msg is not None:
107 console.print(msg, style="red", markup=False)
109 console.print("E ", style="red", markup=False)
111 expected_lines = pprint.pformat(expected, width=80).splitlines()
112 actual_lines = pprint.pformat(actual, width=80).splitlines()
113 _print_ndiff(console, expected_lines, actual_lines, ndiff_func=ndiff_func)
116def render_dict_diff(
117 console: Console,
118 actual: dict[Any, Any],
119 expected: dict[Any, Any],
120 *,
121 ndiff_func: Callable[[list[str], list[str]], Any] = difflib.ndiff,
122) -> None:
123 """Render a pytest-like diff for dicts."""
124 console.print()
126 expected_lines = pprint.pformat(expected, width=80).splitlines()
127 actual_lines = pprint.pformat(actual, width=80).splitlines()
129 diff = ndiff_func(expected_lines, actual_lines)
131 for line in diff:
132 match line[:2]:
133 case "- ":
134 console.print(f"E {line}", style="red", markup=False)
135 case "+ ":
136 console.print(f"E {line}", style="green", markup=False)
137 case "? ":
138 console.print(f"E {line}", style="yellow", markup=False)
139 case " ":
140 console.print(f"E {line}", style="red", markup=False)
141 case _:
142 ...
145def render_multiline_string_diff(
146 console: Console,
147 actual: str,
148 expected: str,
149 *,
150 ndiff_func: Callable[[list[str], list[str]], Any] = difflib.ndiff,
151) -> None:
152 """Colored diff output for multiline strings using difflib."""
153 console.print()
155 diff_lines = ndiff_func(expected.splitlines(), actual.splitlines())
157 for line in diff_lines:
158 match line[:2]:
159 case "+ ":
160 console.print(f"E {line}", style="green", markup=False)
161 case "- ":
162 console.print(f"E {line}", style="red", markup=False)
163 case "? ":
164 console.print(f"E {line}", style="yellow", markup=False)
165 case _:
166 console.print(f"E {line}", markup=False)