- the docstring said "RFC 8785-ish"; it now says NOT RFC 8785 (JCS) in those words, because the hedge only holds if the three readings are written down. - measured at this file (scratch/q82/floats.py, output in the closure record): -0.0 -> "-0.0" where RFC 8785 Table 1 gives "0"; 1e-06 -> "1e-06" where the table gives "0.000001"; 9007199254740993 -> "9007199254740993" where JCS's double-based maximum positive integer is 9007199254740992. - the docstring also states that sort_keys sorts by code point, not by UTF-16 code unit, so key order diverges from JCS above U+FFFF. - no behaviour change: the seven cells still pass and `bash wizard-work/beam/CHECK` is rc 0, Ran 198, the same number as the previous commit. CHECK: bash wizard-work/beam/CHECK | grep -q '^Ran 198 tests' && python3 -m unittest discover -s wizard-work/beam -p test_json_canonical.py
52 lines
1.7 KiB
Python
52 lines
1.7 KiB
Python
"""Canonical JSON serialization: sorted keys, compact separators, no NaN/Infinity,
|
|
deterministic float repr, UTF-8, ensure_ascii=False.
|
|
|
|
NOT RFC 8785 (JCS), and not claimed to be. JCS is IEEE-754 double-based with its
|
|
own number grammar; this module emits Python's `repr` for floats and passes int
|
|
through unchanged. Three measured divergences from RFC 8785 Table 1:
|
|
|
|
-0.0 -> "-0.0" (JCS: "0")
|
|
1e-06 -> "1e-06" (JCS: "0.000001")
|
|
2**53+1 -> "9007199254740993" (JCS max positive integer: 9007199254740992)
|
|
|
|
Key order is `sort_keys=True`, i.e. by Unicode code point; JCS sorts by UTF-16
|
|
code unit, so the two diverge above U+FFFF.
|
|
"""
|
|
from __future__ import annotations
|
|
import json
|
|
import math
|
|
from typing import Any
|
|
|
|
|
|
class CanonicalError(ValueError):
|
|
pass
|
|
|
|
|
|
def _normalize(obj):
|
|
if obj is None or isinstance(obj, bool) or isinstance(obj, int) or isinstance(obj, str):
|
|
return obj
|
|
if isinstance(obj, float):
|
|
if not math.isfinite(obj):
|
|
raise CanonicalError("non-finite float: %r" % obj)
|
|
return obj
|
|
if isinstance(obj, (list, tuple)):
|
|
return [_normalize(x) for x in obj]
|
|
if isinstance(obj, dict):
|
|
out = {}
|
|
for k, v in obj.items():
|
|
if not isinstance(k, str):
|
|
raise CanonicalError("non-string key: %r" % (k,))
|
|
out[k] = _normalize(v)
|
|
return out
|
|
raise CanonicalError("unsupported type: %r" % (type(obj),))
|
|
|
|
|
|
def canonical(obj) -> str:
|
|
n = _normalize(obj)
|
|
return json.dumps(n, sort_keys=True, separators=(",", ":"), ensure_ascii=False,
|
|
allow_nan=False)
|
|
|
|
|
|
def canonical_bytes(obj) -> bytes:
|
|
return canonical(obj).encode("utf-8")
|