Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use more compact JSON formatter #121

Merged
merged 2 commits into from
Oct 28, 2024
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
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"] }
}
]
}
}
}
}
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
Loading