jsonplus.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. # Modify from: https://github.com/randomir/jsonplus/blob/master/python/jsonplus/__init__.py
  2. """Custom datatypes (like datetime) serialization to/from JSON."""
  3. # TODO: handle environments without threads
  4. # (Python compiled without thread support)
  5. import numpy as np
  6. import simplejson as json
  7. import threading
  8. import uuid
  9. from collections import namedtuple
  10. from datetime import timedelta
  11. from dateutil.parser import parse as parse_datetime
  12. from decimal import Decimal
  13. from fractions import Fraction
  14. from functools import partial, wraps
  15. from operator import attrgetter, methodcaller
  16. from sortedcontainers import SortedList
  17. try:
  18. from moneyed import Currency, Money
  19. except ImportError:
  20. # defer failing to actual (de-)serialization
  21. pass
  22. __all__ = [
  23. "loads", "dumps", "pretty", "json_loads", "json_dumps", "json_prettydump",
  24. "encoder", "decoder"
  25. ]
  26. # Should we aim for the *exact* reproduction of Python types,
  27. # or for maximum *compatibility* when (de-)serializing?
  28. #
  29. # By default, we prefer the exactness of reproduction.
  30. # For example, `tuple`, `namedtuple`, `Decimal`, or `datetime` will all be
  31. # restored to the appropriate type (same as the starting type -- even the
  32. # custom class for the `namedtuple` is recreated).
  33. # When compatible coding if turned on, we shall fallback to standard JSON
  34. # types, and values from the example above will be serialized as
  35. # `list` (`Array`), `dict` (`Object`), `Number` and `ISO8601 timestamp string`,
  36. # respectively.
  37. #
  38. # Please note:
  39. # - `compat` mode is lossy -- `namedtuple` serialized as `dict`/`Object`
  40. # can never be deserialized as `namedtuple`.
  41. # - `exact` mode is verbose -- and if you have a standard JSON decoder on
  42. # the other end, all that additional type info is useless/discared.
  43. #
  44. # To switch between representation styles, use `jsonplus.prefer(coding)`,
  45. # where `coding` is `jsonplus.EXACT` or `jsonplus.COMPAT`. Another way, maybe
  46. # simpler, is to use `jsonplus.prefer_exact()` and `jsonplus.prefer_compat()`.
  47. #
  48. # The preference is stored thread-local.
  49. EXACT = 1
  50. COMPAT = 2
  51. CODING_DEFAULT = EXACT
  52. _local = threading.local()
  53. def prefer(coding):
  54. _local.coding = coding
  55. def prefer_exact():
  56. prefer(EXACT)
  57. def prefer_compat():
  58. prefer(COMPAT)
  59. def getattrs(value, attrs):
  60. """Helper function that extracts a list of attributes from
  61. `value` object in a `dict`/mapping of (attr, value[attr]).
  62. Args:
  63. value (object):
  64. Any Python object upon which `getattr` can act.
  65. attrs (iterable):
  66. Any iterable containing attribute names for extract.
  67. Returns:
  68. `dict` of attr -> val mappings.
  69. Example:
  70. >>> getattrs(complex(2,3), ['imag', 'real'])
  71. {'imag': 3.0, 'real': 2.0}
  72. """
  73. return dict([(attr, getattr(value, attr)) for attr in attrs])
  74. def kwargified(constructor):
  75. """Function decorator that wraps a function receiving
  76. keyword arguments into a function receiving a dictionary
  77. of arguments.
  78. Example:
  79. @kwargified
  80. def test(a=1, b=2):
  81. return a + b
  82. >>> test({'b': 3})
  83. 4
  84. """
  85. @wraps(constructor)
  86. def kwargs_constructor(kwargs):
  87. return constructor(**kwargs)
  88. return kwargs_constructor
  89. _PredicatedEncoder = namedtuple('_PredicatedEncoder',
  90. 'priority predicate encoder typename')
  91. def encoder(classname, predicate=None, priority=None, exact=True):
  92. """A decorator for registering a new encoder for object type
  93. defined either by a `classname`, or detected via `predicate`.
  94. Predicates are tested according to priority (low to high),
  95. but always before classname.
  96. Args:
  97. classname (str):
  98. Classname of the object serialized, equal to
  99. ``type(obj).__name__``.
  100. predicate (callable, default=None):
  101. A predicate for testing if object is of certain type.
  102. The predicate shall receive a single argument, the object
  103. being encoded, and it has to return a boolean `True/False`.
  104. See examples below.
  105. priority (int, default=None):
  106. Predicate priority. If undefined, encoder is added at
  107. the end, with lowest priority.
  108. exact (bool, default=True):
  109. Determines the kind of encoder registered, an exact
  110. (default), or a compact representation encoder.
  111. Examples:
  112. @encoder('mytype')
  113. def mytype_exact_encoder(myobj):
  114. return myobj.to_json()
  115. Functional discriminator usage is appropriate for serialization
  116. of objects with a different classname, but which can be encoded
  117. with the same encoder:
  118. @encoder('BaseClass', lambda obj: isinstance(obj, BaseClass))
  119. def all_derived_classes_encoder(derived):
  120. return derived.base_encoder()
  121. """
  122. if exact:
  123. subregistry = _encode_handlers['exact']
  124. else:
  125. subregistry = _encode_handlers['compat']
  126. # if priority undefined, set it to lowest
  127. if priority is None:
  128. if len(subregistry['predicate']) > 0:
  129. priority = subregistry['predicate'][-1].priority + 100
  130. else:
  131. priority = 1000
  132. def _decorator(f):
  133. if predicate:
  134. subregistry['predicate'].add(
  135. _PredicatedEncoder(priority, predicate, f, classname))
  136. else:
  137. subregistry['classname'].setdefault(classname, f)
  138. return f
  139. return _decorator
  140. def _json_default_exact(obj):
  141. """Serialization handlers for types unsupported by `simplejson`
  142. that try to preserve the exact data types.
  143. """
  144. # first try predicate-based encoders
  145. for handler in _encode_handlers['exact']['predicate']:
  146. if handler.predicate(obj):
  147. return {
  148. "__class__": handler.typename,
  149. "__value__": handler.encoder(obj)
  150. }
  151. # then classname-based
  152. classname = type(obj).__name__
  153. if classname in _encode_handlers['exact']['classname']:
  154. return {
  155. "__class__": classname,
  156. "__value__": _encode_handlers['exact']['classname'][classname](obj)
  157. }
  158. raise TypeError(repr(obj) + " is not JSON serializable")
  159. def _json_default_compat(obj):
  160. """Serialization handlers that try to dump objects in
  161. compatibility mode. Similar to above.
  162. """
  163. for handler in _encode_handlers['compat']['predicate']:
  164. if handler.predicate(obj):
  165. return handler.encoder(obj)
  166. classname = type(obj).__name__
  167. if classname in _encode_handlers['compat']['classname']:
  168. return _encode_handlers['compat']['classname'][classname](obj)
  169. raise TypeError(repr(obj) + " is not JSON serializable")
  170. def decoder(classname):
  171. """A decorator for registering a new decoder for `classname`.
  172. Only ``exact`` decoders can be registered, since it is an assumption
  173. the ``compat`` mode serializes to standard JSON.
  174. Example:
  175. @decoder('mytype')
  176. def mytype_decoder(value):
  177. return mytype(value, reconstruct=True)
  178. """
  179. def _decorator(f):
  180. _decode_handlers.setdefault(classname, f)
  181. return _decorator
  182. def _json_object_hook(dict):
  183. """Deserialization handlers for types unsupported by `simplejson`.
  184. """
  185. classname = dict.get('__class__')
  186. if classname:
  187. constructor = _decode_handlers.get(classname)
  188. value = dict.get('__value__')
  189. if constructor:
  190. return constructor(value)
  191. raise TypeError("Unknown class: '%s'" % classname)
  192. return dict
  193. def _encoder_default_args(kw):
  194. """Shape default arguments for encoding functions."""
  195. # manual override of the preferred coding with `exact=False`
  196. if kw.pop('exact', getattr(_local, 'coding', CODING_DEFAULT) == EXACT):
  197. # settings necessary for the "exact coding"
  198. kw.update({
  199. 'default': _json_default_exact,
  200. 'use_decimal': False, # don't encode `Decimal` as JSON's `Number`
  201. 'tuple_as_array': False, # don't encode `tuple` as `Array`
  202. 'namedtuple_as_object':
  203. False # don't call `_asdict` on `namedtuple`
  204. })
  205. else:
  206. # settings for the "compatibility coding"
  207. kw.update({
  208. 'default': _json_default_compat,
  209. 'ignore_nan': True # be compliant with the ECMA-262 specification:
  210. # serialize nan/inf as null
  211. })
  212. # NOTE: if called from ``simplejson.dumps()`` with ``cls=JSONEncoder``,
  213. # we will receive all kw set to simplejson defaults -- and our defaults for
  214. # ``separators`` and ``for_json`` will not be applied. In contrast, they
  215. # are applied when called from ``jsonplus.dumps()``, unless user explicitly
  216. # sets some of those.
  217. # This causes inconsistent behaviour between ``dumps()`` and ``JSONEncoder()``.
  218. # prefer compact json repr
  219. kw.setdefault('separators', (',', ':'))
  220. # allow objects to provide json serialization on its behalf
  221. kw.setdefault('for_json', True)
  222. def _decoder_default_args(kw):
  223. """Shape default arguments for decoding functions."""
  224. kw.update({'object_hook': _json_object_hook})
  225. class JSONEncoder(json.JSONEncoder):
  226. def __init__(self, **kw):
  227. """Constructor for simplejson.JSONEncoder, with defaults overridden
  228. for jsonplus.
  229. """
  230. _encoder_default_args(kw)
  231. super(JSONEncoder, self).__init__(**kw)
  232. class JSONDecoder(json.JSONDecoder):
  233. def __init__(self, **kw):
  234. """Constructor for simplejson.JSONDecoder, with defaults overridden
  235. for jsonplus.
  236. """
  237. _decoder_default_args(kw)
  238. super(JSONDecoder, self).__init__(**kw)
  239. def dumps(*pa, **kw):
  240. _encoder_default_args(kw)
  241. return json.dumps(*pa, **kw)
  242. def loads(*pa, **kw):
  243. _decoder_default_args(kw)
  244. return json.loads(*pa, **kw)
  245. def pretty(x, sort_keys=True, indent=4 * ' ', separators=(',', ': '), **kw):
  246. kw.setdefault('sort_keys', sort_keys)
  247. kw.setdefault('indent', indent)
  248. kw.setdefault('separators', separators)
  249. return dumps(x, **kw)
  250. json_dumps = dumps
  251. json_loads = loads
  252. json_prettydump = pretty
  253. def np_to_list(value):
  254. return value.tolist()
  255. def generic_to_item(value):
  256. return value.item()
  257. _encode_handlers = {
  258. 'exact': {
  259. 'classname': {
  260. 'datetime':
  261. methodcaller('isoformat'),
  262. 'date':
  263. methodcaller('isoformat'),
  264. 'time':
  265. methodcaller('isoformat'),
  266. 'timedelta':
  267. partial(getattrs, attrs=['days', 'seconds', 'microseconds']),
  268. 'tuple':
  269. list,
  270. 'set':
  271. list,
  272. 'ndarray':
  273. np_to_list,
  274. 'float16':
  275. generic_to_item,
  276. 'float32':
  277. generic_to_item,
  278. 'frozenset':
  279. list,
  280. 'complex':
  281. partial(getattrs, attrs=['real', 'imag']),
  282. 'Decimal':
  283. str,
  284. 'Fraction':
  285. partial(getattrs, attrs=['numerator', 'denominator']),
  286. 'UUID':
  287. partial(getattrs, attrs=['hex']),
  288. 'Money':
  289. partial(getattrs, attrs=['amount', 'currency'])
  290. },
  291. 'predicate': SortedList(key=attrgetter('priority'))
  292. },
  293. 'compat': {
  294. 'classname': {
  295. 'datetime': methodcaller('isoformat'),
  296. 'date': methodcaller('isoformat'),
  297. 'time': methodcaller('isoformat'),
  298. 'set': list,
  299. 'ndarray': np_to_list,
  300. 'float16': generic_to_item,
  301. 'float32': generic_to_item,
  302. 'frozenset': list,
  303. 'complex': partial(getattrs, attrs=['real', 'imag']),
  304. 'Fraction': partial(getattrs, attrs=['numerator', 'denominator']),
  305. 'UUID': str,
  306. 'Currency': str,
  307. 'Money': str,
  308. },
  309. 'predicate': SortedList(key=attrgetter('priority'))
  310. }
  311. }
  312. # all decode handlers are for EXACT decoding BY CLASSNAME
  313. _decode_handlers = {
  314. 'datetime': parse_datetime,
  315. 'date': lambda v: parse_datetime(v).date(),
  316. 'time': lambda v: parse_datetime(v).timetz(),
  317. 'timedelta': kwargified(timedelta),
  318. 'tuple': tuple,
  319. 'set': set,
  320. 'ndarray': np.asarray,
  321. 'float16': np.float16,
  322. 'float32': np.float32,
  323. 'frozenset': frozenset,
  324. 'complex': kwargified(complex),
  325. 'Decimal': Decimal,
  326. 'Fraction': kwargified(Fraction),
  327. 'UUID': kwargified(uuid.UUID)
  328. }
  329. @encoder('namedtuple',
  330. lambda obj: isinstance(obj, tuple) and hasattr(obj, '_fields'))
  331. def _dump_namedtuple(obj):
  332. return {
  333. "name": type(obj).__name__,
  334. "fields": list(obj._fields),
  335. "values": list(obj)
  336. }
  337. @decoder('namedtuple')
  338. def _load_namedtuple(val):
  339. cls = namedtuple(val['name'], val['fields'])
  340. return cls(*val['values'])
  341. @encoder('timedelta', exact=False)
  342. def _timedelta_total_seconds(td):
  343. # timedelta.total_seconds() is only available since python 2.7
  344. return (td.microseconds +
  345. (td.seconds + td.days * 24 * 3600.0) * 10**6) / 10**6
  346. @encoder('Currency')
  347. def _dump_currency(obj):
  348. """Serialize standard (ISO-defined) currencies to currency code only,
  349. and non-standard (user-added) currencies in full.
  350. """
  351. from moneyed import CurrencyDoesNotExist, get_currency
  352. try:
  353. get_currency(obj.code)
  354. return obj.code
  355. except CurrencyDoesNotExist:
  356. return getattrs(obj, ['code', 'numeric', 'name', 'countries'])
  357. @decoder('Currency')
  358. def _load_currency(val):
  359. """Deserialize string values as standard currencies, but
  360. manually define fully-defined currencies (with code/name/numeric/countries).
  361. """
  362. from moneyed import get_currency
  363. try:
  364. return get_currency(code=val)
  365. except:
  366. return Currency(**val)
  367. @decoder('Money')
  368. def _load_money(val):
  369. # wrap with function to delay Currency/Money
  370. # parsing if not installed (and not needed)
  371. return Money(**val)