Skip to content

Commit

Permalink
Raise JSONSyntaxError instead of RecursionError
Browse files Browse the repository at this point in the history
  • Loading branch information
Wannes Boeykens committed Feb 28, 2025
1 parent 0add326 commit 0150014
Show file tree
Hide file tree
Showing 5 changed files with 30 additions and 11 deletions.
1 change: 1 addition & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jsonyx 2.0.0 (unreleased)
- Added :func:`jsonyx.paste_values`
- Added :func:`jsonyx.Manipulator`
- Changed error for big integers to :exc:`jsonyx.JSONSyntaxError`
- Changed error for deep nesting to :exc:`jsonyx.JSONSyntaxError`
- Fixed line comment detection
- Fixed spelling of "commas" in error messages
- Fixed ``end_offset`` of :exc:`jsonyx.JSONSyntaxError`
Expand Down
3 changes: 0 additions & 3 deletions src/jsonyx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ def read(
:param hooks: the :ref:`hooks <using_hooks>` used for transforming data
:param use_decimal: use :class:`decimal.Decimal` instead of :class:`float`
:raises JSONSyntaxError: if the JSON file is invalid
:raises RecursionError: if the JSON file is too deeply nested
:raises UnicodeDecodeError: when failing to decode the file
:return: a Python object.
Expand Down Expand Up @@ -157,7 +156,6 @@ def load(
:param root: the path to the archive containing this JSON file
:param use_decimal: use :class:`decimal.Decimal` instead of :class:`float`
:raises JSONSyntaxError: if the JSON file is invalid
:raises RecursionError: if the JSON file is too deeply nested
:raises UnicodeDecodeError: when failing to decode the file
:return: a Python object
Expand Down Expand Up @@ -192,7 +190,6 @@ def loads(
:param hooks: the :ref:`hooks <using_hooks>` used for transforming data
:param use_decimal: use :class:`decimal.Decimal` instead of :class:`float`
:raises JSONSyntaxError: if the JSON string is invalid
:raises RecursionError: if the JSON string is too deeply nested
:raises UnicodeDecodeError: when failing to decode the string
:return: a Python object
Expand Down
15 changes: 10 additions & 5 deletions src/jsonyx/_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,9 +530,17 @@ def scan_value(filename: str, s: str, idx: int) -> tuple[Any, int]:
if nextchar == '"':
value, end = scan_string(filename, s, idx + 1)
elif nextchar == "{":
value, end = scan_object(filename, s, idx + 1)
try:
value, end = scan_object(filename, s, idx + 1)
except RecursionError:
msg = "Object is too deeply nested"
raise _errmsg(msg, filename, s, idx) from None
elif nextchar == "[":
value, end = scan_array(filename, s, idx + 1)
try:
value, end = scan_array(filename, s, idx + 1)
except RecursionError:
msg = "Array is too deeply nested"
raise _errmsg(msg, filename, s, idx) from None
elif nextchar == "n" and s[idx:idx + 4] == "null":
value, end = None, idx + 4
elif nextchar == "t" and s[idx:idx + 4] == "true":
Expand Down Expand Up @@ -649,7 +657,6 @@ def read(self, filename: _StrPath) -> Any:
:param filename: the path to the JSON file
:raises JSONSyntaxError: if the JSON file is invalid
:raises RecursionError: if the JSON file is too deeply nested
:raises UnicodeDecodeError: when failing to decode the file
:return: a Python object
Expand All @@ -675,7 +682,6 @@ def load(
:param fp: an open JSON file
:param root: the path to the archive containing this JSON file
:raises JSONSyntaxError: if the JSON file is invalid
:raises RecursionError: if the JSON file is too deeply nested
:raises UnicodeDecodeError: when failing to decode the file
:return: a Python object
Expand All @@ -702,7 +708,6 @@ def loads(
:param s: a JSON string
:param filename: the path to the JSON file
:raises JSONSyntaxError: if the JSON string is invalid
:raises RecursionError: if the JSON string is too deeply nested
:raises UnicodeDecodeError: when failing to decode the string
:return: a Python object
Expand Down
10 changes: 10 additions & 0 deletions src/jsonyx/_speedups.c
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,11 @@ scan_once_unicode(PyScannerObject *s, PyObject *memo, PyObject *pyfilename, PyOb
return NULL;
res = _parse_object_unicode(s, memo, pyfilename, pystr, idx + 1, next_idx_ptr);
_Py_LeaveRecursiveCall();
if (PyErr_ExceptionMatches(PyExc_RecursionError)) {
PyErr_Clear();
raise_errmsg("Object is too deeply nested", pyfilename, pystr, idx, 0);
return NULL;
}
return res;
case '[':
/* array */
Expand All @@ -1102,6 +1107,11 @@ scan_once_unicode(PyScannerObject *s, PyObject *memo, PyObject *pyfilename, PyOb
return NULL;
res = _parse_array_unicode(s, memo, pyfilename, pystr, idx + 1, next_idx_ptr);
_Py_LeaveRecursiveCall();
if (PyErr_ExceptionMatches(PyExc_RecursionError)) {
PyErr_Clear();
raise_errmsg("Array is too deeply nested", pyfilename, pystr, idx, 0);
return NULL;
}
return res;
case 'n':
/* null */
Expand Down
12 changes: 9 additions & 3 deletions src/jsonyx/test/test_loads.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,12 +611,18 @@ def test_trailing_comma(
assert json.loads(s, allow=TRAILING_COMMA) == expected


@pytest.mark.parametrize("start", ["[", '{"":'])
def test_recursion(json: ModuleType, start: str) -> None:
@pytest.mark.parametrize(("start", "msg"), [
("[", "Array is too deeply nested"),
('{"":', "Object is too deeply nested"),
])
def test_recursion(json: ModuleType, start: str, msg: str) -> None:
"""Test recursion."""
with pytest.raises(RecursionError):
with pytest.raises(json.JSONSyntaxError) as exc_info:
json.loads(start * 100_000)

exc: Any = exc_info.value
assert exc.msg == msg


@pytest.mark.parametrize("s", [
# No whitespace
Expand Down

0 comments on commit 0150014

Please sign in to comment.