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

1import difflib 

2import pprint 

3from collections.abc import Callable 

4from typing import Any, cast 

5 

6from rich.console import Console 

7 

8from snektest.models import AssertionFailure 

9 

10 

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 

20 

21 console.print(f"E {exc.args[0]}", style="red", markup=False) 

22 

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 

46 

47 

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 

55 

56 

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" 

63 

64 

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 } 

78 

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) 

85 

86 

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() 

96 

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) 

108 

109 console.print("E ", style="red", markup=False) 

110 

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) 

114 

115 

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() 

125 

126 expected_lines = pprint.pformat(expected, width=80).splitlines() 

127 actual_lines = pprint.pformat(actual, width=80).splitlines() 

128 

129 diff = ndiff_func(expected_lines, actual_lines) 

130 

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 ... 

143 

144 

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() 

154 

155 diff_lines = ndiff_func(expected.splitlines(), actual.splitlines()) 

156 

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)