Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions parse_errors/json_source_map/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Thin wrapper around the json-source-map package that returns our own types."""

from __future__ import annotations

import json_source_map as _ext

from ..source_map import Entry, Location, TSourceMap


def calculate(source: str) -> TSourceMap:
"""Calculate the source map for a JSON document."""
return {
pointer: Entry(
value_start=_loc(e.value_start),
value_end=_loc(e.value_end),
key_start=_loc(e.key_start) if e.key_start is not None else None,
key_end=_loc(e.key_end) if e.key_end is not None else None,
)
for pointer, e in _ext.calculate(source).items()
}


def _loc(ext: _ext.Location) -> Location:
"""Translate to our internal structure."""
return Location(line=ext.line, column=ext.column, position=ext.position)
7 changes: 7 additions & 0 deletions parse_errors/json_source_map/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
if __name__ == "__main__": # pragma: no cover
import sys
from . import calculate

source = open(sys.argv[1]).read()
for pointer, entry in calculate(source).items():
print(f"{pointer!r:40s} {entry}")
7 changes: 7 additions & 0 deletions parse_errors/source_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def detect_format(path: Path) -> str | None:
"""Detect the format of a file based on its extension."""
suffix = path.suffix.lower()
return {
".json": "json",
".toml": "toml",
".yaml": "yaml",
".yml": "yaml",
Expand All @@ -51,6 +52,12 @@ def build_source_map(source: str | bytes, fmt: str) -> TSourceMap:
return yaml_source_map.calculate(
source.decode("utf-8") if isinstance(source, bytes) else source
)
elif fmt == "json":
from . import json_source_map

return json_source_map.calculate(
source.decode("utf-8") if isinstance(source, bytes) else source
)
else:
raise ValueError(f"Unknown format: {fmt!r}")

Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ setup_requires =
setuptools >= 65
include_package_data = true
install_requires =
json-source-map
pyyaml
tree-sitter
tree-sitter-toml
Expand Down
79 changes: 79 additions & 0 deletions tests/test_parse_context_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import pytest
import msgspec

from parse_errors import ParseContext, ParseError

from ._types import Config, Nested


JSON_GOOD = b'{"host": "localhost", "port": 8080}'
JSON_BAD = b'{"host": "localhost", "port": "not-an-int"}'
JSON_NESTED_BAD = b'{"server": {"host": "localhost", "port": "not-an-int"}}'

JSON_SOURCE = """\
{
"host": "localhost",
"port": "not-an-int"
}
"""

JSON_NESTED_SOURCE = """\
{
"server": {
"host": "localhost",
"port": "not-an-int"
}
}
"""


def test_json_no_error():
with ParseContext("config.json", data=JSON_GOOD.decode()):
msgspec.json.decode(JSON_GOOD, type=Config)


def test_json_non_jsonpath_exception_passes_through():
with pytest.raises(ZeroDivisionError):
with ParseContext("config.json", data=JSON_SOURCE):
raise ZeroDivisionError("oops")


def test_json_raises_parse_error():
with pytest.raises(ParseError) as exc_info:
with ParseContext("config.json", data=JSON_SOURCE):
msgspec.json.decode(JSON_SOURCE.encode(), type=Config)

err = exc_info.value
assert err.filename == "config.json"
assert err.line == 3
assert str(err) == "config.json:3:11: Expected `int`, got `str` - at `$.port`"


def test_json_nested_raises_parse_error():
with pytest.raises(ParseError) as exc_info:
with ParseContext("config.json", data=JSON_NESTED_SOURCE):
msgspec.json.decode(JSON_NESTED_SOURCE.encode(), type=Nested)

err = exc_info.value
assert (
str(err) == "config.json:4:13: Expected `int`, got `str` - at `$.server.port`"
)


def test_json_bytes_data():
with pytest.raises(ParseError) as exc_info:
with ParseContext("config.json", data=JSON_SOURCE.encode()):
msgspec.json.decode(JSON_SOURCE.encode(), type=Config)

assert (
str(exc_info.value)
== "config.json:3:11: Expected `int`, got `str` - at `$.port`"
)


def test_json_original_exception_is_cause():
with pytest.raises(ParseError) as exc_info:
with ParseContext("config.json", data=JSON_SOURCE):
msgspec.json.decode(JSON_SOURCE.encode(), type=Config)

assert isinstance(exc_info.value.__cause__, msgspec.ValidationError)
Loading