Coverage for human_requests/abstraction/json_debug.py: 91%

91 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-05-28 00:39 +0000

1from __future__ import annotations 

2 

3import json 

4from json import JSONDecodeError 

5from typing import Any 

6 

7from rich.console import Console 

8from rich.panel import Panel 

9from rich.syntax import Syntax 

10from rich.table import Table 

11from rich.text import Text 

12 

13console = Console() 

14 

15 

16def _detect_payload_type(text: str) -> str: 

17 stripped = text.lstrip().lower() 

18 

19 if text == "": 

20 return "empty" 

21 

22 if stripped.startswith("<!doctype html") or stripped.startswith("<html"): 

23 return "html" 

24 

25 if stripped.startswith("{") or stripped.startswith("["): 

26 return "json" 

27 

28 return "text" 

29 

30 

31def _strip_rich_newline(text: Text) -> Text: 

32 while text.plain.endswith("\n") or text.plain.endswith("\r"): 

33 text = text[:-1] 

34 return text 

35 

36 

37def _render_truncated_notice(line_no_width: int, skipped_lines: int) -> Text: 

38 suffix = "line" if skipped_lines == 1 else "lines" 

39 notice = Text() 

40 notice.append(f"{' ' * line_no_width}") 

41 notice.append(f"… skipped {skipped_lines} {suffix}", style="bold bright_black") 

42 return notice 

43 

44 

45def _print_fragment( 

46 text: str, 

47 error: JSONDecodeError, 

48 *, 

49 lexer: str | None = None, 

50 context_lines: int = 8, 

51 tab_size: int = 4, 

52) -> None: 

53 lines = text.splitlines() 

54 

55 if not lines: 

56 console.print("[dim]<no visible lines>[/dim]") 

57 return 

58 

59 payload_type = _detect_payload_type(text) 

60 

61 if lexer is None: 

62 if payload_type == "html": 

63 lexer = "html" 

64 elif payload_type == "json": 

65 lexer = "json" 

66 else: 

67 lexer = "text" 

68 

69 syntax = Syntax( 

70 "", 

71 lexer, 

72 theme="monokai", 

73 background_color="default", 

74 word_wrap=False, 

75 ) 

76 

77 error_line_index = max(error.lineno - 1, 0) 

78 

79 if error.pos == 0: 

80 start = 0 

81 end = min(context_lines, len(lines)) 

82 else: 

83 start = max(error_line_index - context_lines // 2, 0) 

84 end = min(error_line_index + context_lines // 2 + 1, len(lines)) 

85 

86 line_no_width = len(str(end)) 

87 

88 console.print("[bold]Fragment:[/bold]") 

89 

90 if start > 0: 

91 console.print(_render_truncated_notice(line_no_width, start)) 

92 

93 for index in range(start, end): 

94 line_no = index + 1 

95 raw_line = lines[index] 

96 visible_line = raw_line.expandtabs(tab_size) 

97 is_error_line = index == error_line_index 

98 

99 prefix = Text(f"{line_no:>{line_no_width}}", style="red" if is_error_line else "dim") 

100 highlighted_line = syntax.highlight(visible_line) 

101 highlighted_line = _strip_rich_newline(highlighted_line) 

102 

103 console.print(prefix, highlighted_line, sep="", highlight=False) 

104 

105 if is_error_line: 

106 raw_before_error = raw_line[: max(error.colno - 1, 0)] 

107 visible_before_error = raw_before_error.expandtabs(tab_size) 

108 pointer_col = len(visible_before_error) 

109 

110 pointer = Text() 

111 pointer.append(f"{' ' * line_no_width}") 

112 pointer.append(" " * pointer_col) 

113 pointer.append("^ here", style="bold red") 

114 

115 console.print(pointer) 

116 

117 if end < len(lines): 

118 console.print(_render_truncated_notice(line_no_width, len(lines) - end)) 

119 

120 

121def _print_json_error(text: str, error: JSONDecodeError) -> None: 

122 payload_type = _detect_payload_type(text) 

123 

124 info = Table.grid(padding=(0, 1)) 

125 info.add_column(style="bold cyan") 

126 info.add_column() 

127 

128 info.add_row("Reason", f"[yellow]{error.msg}[/yellow]") 

129 info.add_row("Position", f"line={error.lineno}, column={error.colno}, char={error.pos}") 

130 info.add_row("Payload", payload_type) 

131 

132 console.print( 

133 Panel( 

134 info, 

135 title="[bold red]JSON parse failed[/bold red]", 

136 border_style="red", 

137 expand=False, 

138 ) 

139 ) 

140 

141 if payload_type == "empty": 

142 console.print("[yellow]Input is empty.[/yellow]") 

143 return 

144 

145 _print_fragment(text, error) 

146 

147 

148def loads_json_debug(text: str) -> Any: 

149 try: 

150 return json.loads(text) 

151 except JSONDecodeError as error: 

152 _print_json_error(text, error) 

153 raise