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
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-28 00:39 +0000
1from __future__ import annotations
3import json
4from json import JSONDecodeError
5from typing import Any
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
13console = Console()
16def _detect_payload_type(text: str) -> str:
17 stripped = text.lstrip().lower()
19 if text == "":
20 return "empty"
22 if stripped.startswith("<!doctype html") or stripped.startswith("<html"):
23 return "html"
25 if stripped.startswith("{") or stripped.startswith("["):
26 return "json"
28 return "text"
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
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
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()
55 if not lines:
56 console.print("[dim]<no visible lines>[/dim]")
57 return
59 payload_type = _detect_payload_type(text)
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"
69 syntax = Syntax(
70 "",
71 lexer,
72 theme="monokai",
73 background_color="default",
74 word_wrap=False,
75 )
77 error_line_index = max(error.lineno - 1, 0)
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))
86 line_no_width = len(str(end))
88 console.print("[bold]Fragment:[/bold]")
90 if start > 0:
91 console.print(_render_truncated_notice(line_no_width, start))
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
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)
103 console.print(prefix, highlighted_line, sep="", highlight=False)
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)
110 pointer = Text()
111 pointer.append(f"{' ' * line_no_width} │ ")
112 pointer.append(" " * pointer_col)
113 pointer.append("^ here", style="bold red")
115 console.print(pointer)
117 if end < len(lines):
118 console.print(_render_truncated_notice(line_no_width, len(lines) - end))
121def _print_json_error(text: str, error: JSONDecodeError) -> None:
122 payload_type = _detect_payload_type(text)
124 info = Table.grid(padding=(0, 1))
125 info.add_column(style="bold cyan")
126 info.add_column()
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)
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 )
141 if payload_type == "empty":
142 console.print("[yellow]Input is empty.[/yellow]")
143 return
145 _print_fragment(text, error)
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