Skip to content

Commit

Permalink
feat: use more compact JSON formatter (#121)
Browse files Browse the repository at this point in the history
backport JSON formatter from LedgerHQ/crypto-assets#1417
  • Loading branch information
jnicoulaud-ledger authored Oct 28, 2024
1 parent b55fee4 commit 51af91e
Show file tree
Hide file tree
Showing 37 changed files with 1,230 additions and 2,564 deletions.
96 changes: 95 additions & 1 deletion src/erc7730/common/json.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
from collections.abc import Iterator
from json import JSONEncoder
from pathlib import Path
from typing import Any
from typing import Any, override


def read_jsons_with_includes(paths: list[Path]) -> Any:
Expand Down Expand Up @@ -64,3 +66,95 @@ def _merge_dicts(d1: dict[str, Any], d2: dict[str, Any]) -> dict[str, Any]:
else:
merged[key] = val1
return {**d2, **merged}


class CompactJSONEncoder(JSONEncoder):
"""A JSON Encoder that puts small containers on single lines."""

CONTAINER_TYPES = (list, tuple, dict)
"""Container datatypes include primitives or other containers."""

MAX_WIDTH = 120
"""Maximum width of a container that might be put on a single line."""

MAX_ITEMS = 10
"""Maximum number of items in container that might be put on single line."""

PRIMITIVES_ONLY = False
"""Only put containers containing primitives only on a single line."""

def __init__(self, *args: Any, **kwargs: Any) -> None:
if kwargs.get("indent") is None:
kwargs["indent"] = 2
super().__init__(*args, **kwargs)
self.indentation_level = 0

@override
def encode(self, o: Any) -> str:
if isinstance(o, list | tuple):
return self._encode_list(o)
if isinstance(o, dict):
return self._encode_object(o)
if isinstance(o, float): # Use scientific notation for floats
return format(o, "g")
return json.dumps(
obj=o,
skipkeys=self.skipkeys,
ensure_ascii=self.ensure_ascii,
check_circular=self.check_circular,
allow_nan=self.allow_nan,
sort_keys=self.sort_keys,
indent=self.indent,
separators=(self.item_separator, self.key_separator),
default=self.default if hasattr(self, "default") else None,
)

def _encode_list(self, o: list[Any] | tuple[Any]) -> str:
if self._put_on_single_line(o):
return "[" + ", ".join(self.encode(el) for el in o) + "]"
self.indentation_level += 1
output = [self.indent_str + self.encode(el) for el in o]
self.indentation_level -= 1
return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]"

def _encode_object(self, o: Any) -> str:
if not o:
return "{}"

o = {str(k) if k is not None else "null": v for k, v in o.items()}

if self.sort_keys:
o = dict(sorted(o.items(), key=lambda x: x[0]))

if self._put_on_single_line(o):
return "{ " + ", ".join(f"{json.dumps(k)}: {self.encode(el)}" for k, el in o.items()) + " }"

self.indentation_level += 1
output = [f"{self.indent_str}{json.dumps(k)}: {self.encode(v)}" for k, v in o.items()]
self.indentation_level -= 1

return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}"

@override
def iterencode(self, o: Any, _one_shot: bool = False) -> Iterator[str]:
return self.encode(o) # type: ignore

def _put_on_single_line(self, o: Any) -> bool:
return self._primitives_only(o) and len(o) <= self.MAX_ITEMS and len(str(o)) - 2 <= self.MAX_WIDTH

def _primitives_only(self, o: list[Any] | tuple[Any] | dict[Any, Any]) -> bool:
if not self.PRIMITIVES_ONLY:
return True
if isinstance(o, list | tuple):
return not any(isinstance(el, self.CONTAINER_TYPES) for el in o)
elif isinstance(o, dict):
return not any(isinstance(el, self.CONTAINER_TYPES) for el in o.values())

@property
def indent_str(self) -> str:
if isinstance(self.indent, int):
return " " * (self.indentation_level * self.indent)
elif isinstance(self.indent, str):
return self.indentation_level * self.indent
else:
raise ValueError(f"indent must either be of type int or str (is: {type(self.indent)})")
7 changes: 4 additions & 3 deletions src/erc7730/common/pydantic.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import json
import os
from pathlib import Path
from typing import Any, TypeVar

from pydantic import BaseModel

from erc7730.common.json import read_json_with_includes
from erc7730.common.json import CompactJSONEncoder, read_json_with_includes

_BaseModel = TypeVar("_BaseModel", bound=BaseModel)

Expand All @@ -31,12 +32,12 @@ def model_from_json_file_with_includes_or_none(path: Path, model: type[_BaseMode

def model_to_json_dict(obj: _BaseModel) -> dict[str, Any]:
"""Serialize a pydantic model into a JSON dict."""
return obj.model_dump(by_alias=True, exclude_none=True)
return obj.model_dump(mode="json", by_alias=True, exclude_none=True)


def model_to_json_str(obj: _BaseModel) -> str:
"""Serialize a pydantic model into a JSON string."""
return obj.model_dump_json(by_alias=True, exclude_none=True, indent=4)
return json.dumps(model_to_json_dict(obj), indent=2, cls=CompactJSONEncoder)


def model_to_json_file(path: Path, model: _BaseModel) -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,77 +1,35 @@
{
"context": {
"eip712": {
"deployments": [
{
"chainId": 1,
"address": "0x0000000000000000000000000000000000000000"
}
"context": {
"eip712": {
"deployments": [{ "chainId": 1, "address": "0x0000000000000000000000000000000000000000" }],
"schemas": [
{
"primaryType": "TestPrimaryType",
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "chainId", "type": "uint256" },
{ "name": "verifyingContract", "type": "address" }
],
"schemas": [
{
"primaryType": "TestPrimaryType",
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"TestPrimaryType": [
{
"name": "param1",
"type": "address"
}
]
}
}
]
}
},
"metadata": {
"enums": {}
},
"display": {
"formats": {
"TestPrimaryType": {
"fields": [
{
"path": {
"type": "data",
"absolute": true,
"elements": [
{
"type": "field",
"identifier": "param1"
}
]
},
"label": "Param 1",
"format": "addressName",
"params": {
"types": [
"wallet",
"eoa",
"token",
"contract",
"collection"
],
"sources": [
"local",
"ens"
]
}
}
]
}
"TestPrimaryType": [{ "name": "param1", "type": "address" }]
}
}
]
}
},
"metadata": { "enums": {} },
"display": {
"formats": {
"TestPrimaryType": {
"fields": [
{
"path": { "type": "data", "absolute": true, "elements": [{ "type": "field", "identifier": "param1" }] },
"label": "Param 1",
"format": "addressName",
"params": { "types": ["wallet", "eoa", "token", "contract", "collection"], "sources": ["local", "ens"] }
}
]
}
}
}
}
88 changes: 29 additions & 59 deletions tests/convert/resolved/data/definition_format_amount_resolved.json
Original file line number Diff line number Diff line change
@@ -1,64 +1,34 @@
{
"context": {
"eip712": {
"deployments": [
{
"chainId": 1,
"address": "0x0000000000000000000000000000000000000000"
}
"context": {
"eip712": {
"deployments": [{ "chainId": 1, "address": "0x0000000000000000000000000000000000000000" }],
"schemas": [
{
"primaryType": "TestPrimaryType",
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "chainId", "type": "uint256" },
{ "name": "verifyingContract", "type": "address" }
],
"schemas": [
{
"primaryType": "TestPrimaryType",
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"TestPrimaryType": [
{
"name": "param1",
"type": "uint256"
}
]
}
}
]
}
},
"metadata": {
"enums": {}
},
"display": {
"formats": {
"TestPrimaryType": {
"fields": [
{
"path": {
"type": "data",
"absolute": true,
"elements": [
{
"type": "field",
"identifier": "param1"
}
]
},
"label": "Param 1",
"format": "amount"
}
]
}
"TestPrimaryType": [{ "name": "param1", "type": "uint256" }]
}
}
]
}
},
"metadata": { "enums": {} },
"display": {
"formats": {
"TestPrimaryType": {
"fields": [
{
"path": { "type": "data", "absolute": true, "elements": [{ "type": "field", "identifier": "param1" }] },
"label": "Param 1",
"format": "amount"
}
]
}
}
}
}
Loading

0 comments on commit 51af91e

Please sign in to comment.