mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-08-23 08:48:01 -03:00
84 lines
2.8 KiB
Python
84 lines
2.8 KiB
Python
"""JSON in and out of the scripts, stricter than the json module in both directions.
|
|
|
|
Input is model-written and output is read by other tools, so load() refuses
|
|
what json.load admits but a conforming reader does not (NaN, Infinity, a
|
|
number that overflows to infinity, nesting past the interpreter's limit), and
|
|
text() keeps non-ASCII readable while escaping what UTF-8 or a JSONL line
|
|
reader cannot take (an unpaired surrogate, the Unicode line separators).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import re
|
|
from collections.abc import Mapping
|
|
from typing import TYPE_CHECKING, NoReturn, cast
|
|
|
|
if TYPE_CHECKING:
|
|
import os
|
|
from typing import TypeGuard
|
|
|
|
JsonMap = Mapping[str, object]
|
|
|
|
# The separators a naive line reader would split a JSONL record on.
|
|
_SEPARATOR_ESCAPES = {0x85: "\\u0085", 0x2028: "\\u2028", 0x2029: "\\u2029"}
|
|
# The code units UTF-8 cannot encode; surrogateescape mints them from bytes.
|
|
_SURROGATES = re.compile(r"[\ud800-\udfff]")
|
|
|
|
|
|
def is_map(value: object) -> TypeGuard[JsonMap]:
|
|
"""Whether `value` is a JSON object."""
|
|
return isinstance(value, dict)
|
|
|
|
|
|
def is_list(value: object) -> TypeGuard[list[object]]:
|
|
"""Whether `value` is a JSON array."""
|
|
return isinstance(value, list)
|
|
|
|
|
|
def is_str(value: object) -> TypeGuard[str]:
|
|
"""Whether `value` is a JSON string."""
|
|
return isinstance(value, str)
|
|
|
|
|
|
def is_int(value: object) -> TypeGuard[int]:
|
|
"""Whether `value` is a JSON integer; JSON's booleans are not numbers."""
|
|
return isinstance(value, int) and not isinstance(value, bool)
|
|
|
|
|
|
def has_lone_surrogate(value: str) -> bool:
|
|
"""Whether `value` holds an unpaired surrogate, the one code unit UTF-8 cannot encode."""
|
|
return _SURROGATES.search(value) is not None
|
|
|
|
|
|
def _refuse(token: str) -> NoReturn:
|
|
msg = f"{token} is not JSON"
|
|
raise ValueError(msg)
|
|
|
|
|
|
def _finite(digits: str) -> float:
|
|
value = float(digits)
|
|
if not math.isfinite(value):
|
|
msg = f"{digits} is out of range"
|
|
raise ValueError(msg)
|
|
return value
|
|
|
|
|
|
def load(path: str | os.PathLike[str]) -> object:
|
|
"""The JSON value in `path`; OSError if unreadable, ValueError if not strict JSON."""
|
|
with open(path, encoding="utf-8") as handle:
|
|
try:
|
|
return cast("object", json.load(handle, parse_constant=_refuse, parse_float=_finite))
|
|
except RecursionError as error:
|
|
msg = "nested too deeply"
|
|
raise ValueError(msg) from error
|
|
|
|
|
|
def text(value: object, indent: int | None = None) -> str:
|
|
"""`value` as UTF-8-encodable JSON text: non-ASCII kept, finite only, separators escaped."""
|
|
dumped = json.dumps(value, ensure_ascii=False, allow_nan=False, indent=indent)
|
|
return _SURROGATES.sub(
|
|
lambda match: f"\\u{ord(match.group()):04x}", dumped.translate(_SEPARATOR_ESCAPES)
|
|
)
|