Skip to content

Commit f384b63

Browse files
committed
Pretty exception messages
1 parent 71a43ba commit f384b63

File tree

4 files changed

+68
-15
lines changed

4 files changed

+68
-15
lines changed

jsonpath/env.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,8 +493,9 @@ def check_well_typedness(
493493
"""Check the well-typedness of a function's arguments at compile-time."""
494494
# Correct number of arguments?
495495
if len(args) != len(func.arg_types):
496+
plural = "" if len(func.arg_types) == 1 else "s"
496497
raise JSONPathTypeError(
497-
f"{token.value!r}() requires {len(func.arg_types)} arguments",
498+
f"{token.value}() requires {len(func.arg_types)} argument{plural}",
498499
token=token,
499500
)
500501

jsonpath/exceptions.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from typing import TYPE_CHECKING
66
from typing import Optional
77

8+
from .token import TOKEN_EOF
9+
810
if TYPE_CHECKING:
911
from .token import Token
1012

@@ -22,13 +24,69 @@ def __init__(self, *args: object, token: Optional[Token] = None) -> None:
2224
self.token: Optional[Token] = token
2325

2426
def __str__(self) -> str:
25-
msg = super().__str__()
27+
return self.detailed_message()
2628

29+
def detailed_message(self) -> str:
30+
"""Return an error message formatted with extra context info."""
2731
if not self.token:
28-
return msg
32+
return super().__str__()
2933

30-
line, column = self.token.position()
31-
return f"{msg}, line {line}, column {column}"
34+
lineno, col, _prev, current, _next = self._error_context(
35+
self.token.path, self.token.index
36+
)
37+
38+
if self.token.kind == TOKEN_EOF:
39+
col = len(current)
40+
41+
pad = " " * len(str(lineno))
42+
length = len(self.token.value)
43+
pointer = (" " * col) + ("^" * max(length, 1))
44+
45+
return (
46+
f"{self.message}\n"
47+
f"{pad} -> {self.token.path!r} {lineno}:{col}\n"
48+
f"{pad} |\n"
49+
f"{lineno} | {current}\n"
50+
f"{pad} | {pointer} {self.message}\n"
51+
)
52+
53+
@property
54+
def message(self) -> object:
55+
"""The exception's error message if one was given."""
56+
if self.args:
57+
return self.args[0]
58+
return None
59+
60+
def _error_context(self, text: str, index: int) -> tuple[int, int, str, str, str]:
61+
lines = text.splitlines(keepends=True)
62+
cumulative_length = 0
63+
target_line_index = -1
64+
65+
for i, line in enumerate(lines):
66+
cumulative_length += len(line)
67+
if index < cumulative_length:
68+
target_line_index = i
69+
break
70+
71+
if target_line_index == -1:
72+
raise ValueError("index is out of bounds for the given string")
73+
74+
# Line number (1-based)
75+
line_number = target_line_index + 1
76+
# Column number within the line
77+
column_number = index - (cumulative_length - len(lines[target_line_index]))
78+
79+
previous_line = (
80+
lines[target_line_index - 1].rstrip() if target_line_index > 0 else ""
81+
)
82+
current_line = lines[target_line_index].rstrip()
83+
next_line = (
84+
lines[target_line_index + 1].rstrip()
85+
if target_line_index < len(lines) - 1
86+
else ""
87+
)
88+
89+
return line_number, column_number, previous_line, current_line, next_line
3290

3391

3492
class JSONPathSyntaxError(JSONPathError):

jsonpath/parse.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -675,19 +675,13 @@ def parse_infix_expression(
675675
return InfixExpression(left, operator, right)
676676

677677
def parse_grouped_expression(self, stream: TokenStream) -> BaseExpression:
678-
stream.eat(TOKEN_LPAREN)
678+
_token = stream.eat(TOKEN_LPAREN)
679679
expr = self.parse_filter_expression(stream)
680680

681681
while stream.current().kind != TOKEN_RPAREN:
682682
token = stream.current()
683-
if token.kind == TOKEN_EOF:
684-
raise JSONPathSyntaxError("unbalanced parentheses", token=token)
685-
686-
if token.kind not in self.BINARY_OPERATORS:
687-
raise JSONPathSyntaxError(
688-
f"expected an expression, found '{token.value}'",
689-
token=token,
690-
)
683+
if token.kind in (TOKEN_EOF, TOKEN_RBRACKET):
684+
raise JSONPathSyntaxError("unbalanced parentheses", token=_token)
691685

692686
expr = self.parse_infix_expression(stream, expr)
693687

tests/test_errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def test_unclosed_selection_list(env: JSONPathEnvironment) -> None:
2222

2323

2424
def test_function_missing_param(env: JSONPathEnvironment) -> None:
25-
with pytest.raises(JSONPathTypeError):
25+
with pytest.raises(JSONPathTypeError, match=r"length\(\) requires 1 argument"):
2626
env.compile("$[?(length()==1)]")
2727

2828

0 commit comments

Comments
 (0)