1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
|
from __future__ import annotations
import enum
import json
import weakref
from collections.abc import Mapping
from typing import Any
from tatsu.util import debug, is_namedtuple, isiter
class AsJSONMixin:
def __json__(self, seen: set[int] | None = None) -> Any:
return {
'__class__': type(self).__name__,
**asjson(self._pubdict(), seen=seen),
}
def _pubdict(self) -> dict[str, Any]:
return {
name: value
for name, value in vars(self).items()
if not name.startswith('_')
}
def asjson(obj, seen: set[int] | None = None) -> Any: # noqa: PLR0911, PLR0912
if obj is None or isinstance(obj, int | float | str | bool):
return obj
if seen is None:
seen = set()
elif id(obj) in seen:
return f'{type(obj).__name__}@{id(obj)}'
if isinstance(obj, Mapping | AsJSONMixin) or isiter(obj):
seen.add(id(obj))
try:
if isinstance(obj, weakref.ReferenceType | weakref.ProxyType):
return f'{obj.__class__.__name__}@0x{hex(id(obj)).upper()[2:]}'
elif hasattr(obj, '__json__'):
return obj.__json__(seen=seen)
elif is_namedtuple(obj):
return asjson(obj._asdict(), seen=seen)
elif isinstance(obj, Mapping):
result = {}
for k, v in obj.items():
try:
result[k] = asjson(v, seen=seen)
except TypeError:
debug('Unhashable key?', type(k), str(k))
raise
return result
elif isiter(obj):
return [asjson(e, seen=seen) for e in obj]
elif isinstance(obj, enum.Enum):
return asjson(obj.value)
else:
return repr(obj)
finally:
# NOTE: id()s may be reused
# https://docs.python.org/3/library/functions.html#id
seen -= {id(obj)}
def plainjson(obj: Any) -> Any:
if isinstance(obj, Mapping):
return {
name: plainjson(value)
for name, value in obj.items()
if name not in {'__class__', 'parseinfo'}
}
elif isinstance(obj, weakref.ReferenceType | weakref.ProxyType):
return '@ref'
elif isinstance(obj, str) and obj.startswith('@'):
return '@ref'
elif isiter(obj):
return [plainjson(e) for e in obj]
else:
return obj
class FallbackJSONEncoder(json.JSONEncoder):
"""A JSON Encoder that falls back to repr() for non-JSON-aware objects."""
def default(self, o: Any) -> str:
return repr(o)
def asjsons(obj: Any) -> str:
return json.dumps(asjson(obj), indent=2, cls=FallbackJSONEncoder)
|