| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- # Modify from: https://github.com/randomir/jsonplus/blob/master/python/jsonplus/__init__.py
- """Custom datatypes (like datetime) serialization to/from JSON."""
- # TODO: handle environments without threads
- # (Python compiled without thread support)
- import numpy as np
- import simplejson as json
- import threading
- import uuid
- from collections import namedtuple
- from datetime import timedelta
- from dateutil.parser import parse as parse_datetime
- from decimal import Decimal
- from fractions import Fraction
- from functools import partial, wraps
- from operator import attrgetter, methodcaller
- from sortedcontainers import SortedList
- try:
- from moneyed import Currency, Money
- except ImportError:
- # defer failing to actual (de-)serialization
- pass
- __all__ = [
- "loads", "dumps", "pretty", "json_loads", "json_dumps", "json_prettydump",
- "encoder", "decoder"
- ]
- # Should we aim for the *exact* reproduction of Python types,
- # or for maximum *compatibility* when (de-)serializing?
- #
- # By default, we prefer the exactness of reproduction.
- # For example, `tuple`, `namedtuple`, `Decimal`, or `datetime` will all be
- # restored to the appropriate type (same as the starting type -- even the
- # custom class for the `namedtuple` is recreated).
- # When compatible coding if turned on, we shall fallback to standard JSON
- # types, and values from the example above will be serialized as
- # `list` (`Array`), `dict` (`Object`), `Number` and `ISO8601 timestamp string`,
- # respectively.
- #
- # Please note:
- # - `compat` mode is lossy -- `namedtuple` serialized as `dict`/`Object`
- # can never be deserialized as `namedtuple`.
- # - `exact` mode is verbose -- and if you have a standard JSON decoder on
- # the other end, all that additional type info is useless/discared.
- #
- # To switch between representation styles, use `jsonplus.prefer(coding)`,
- # where `coding` is `jsonplus.EXACT` or `jsonplus.COMPAT`. Another way, maybe
- # simpler, is to use `jsonplus.prefer_exact()` and `jsonplus.prefer_compat()`.
- #
- # The preference is stored thread-local.
- EXACT = 1
- COMPAT = 2
- CODING_DEFAULT = EXACT
- _local = threading.local()
- def prefer(coding):
- _local.coding = coding
- def prefer_exact():
- prefer(EXACT)
- def prefer_compat():
- prefer(COMPAT)
- def getattrs(value, attrs):
- """Helper function that extracts a list of attributes from
- `value` object in a `dict`/mapping of (attr, value[attr]).
- Args:
- value (object):
- Any Python object upon which `getattr` can act.
- attrs (iterable):
- Any iterable containing attribute names for extract.
- Returns:
- `dict` of attr -> val mappings.
- Example:
- >>> getattrs(complex(2,3), ['imag', 'real'])
- {'imag': 3.0, 'real': 2.0}
- """
- return dict([(attr, getattr(value, attr)) for attr in attrs])
- def kwargified(constructor):
- """Function decorator that wraps a function receiving
- keyword arguments into a function receiving a dictionary
- of arguments.
- Example:
- @kwargified
- def test(a=1, b=2):
- return a + b
- >>> test({'b': 3})
- 4
- """
- @wraps(constructor)
- def kwargs_constructor(kwargs):
- return constructor(**kwargs)
- return kwargs_constructor
- _PredicatedEncoder = namedtuple('_PredicatedEncoder',
- 'priority predicate encoder typename')
- def encoder(classname, predicate=None, priority=None, exact=True):
- """A decorator for registering a new encoder for object type
- defined either by a `classname`, or detected via `predicate`.
- Predicates are tested according to priority (low to high),
- but always before classname.
- Args:
- classname (str):
- Classname of the object serialized, equal to
- ``type(obj).__name__``.
- predicate (callable, default=None):
- A predicate for testing if object is of certain type.
- The predicate shall receive a single argument, the object
- being encoded, and it has to return a boolean `True/False`.
- See examples below.
- priority (int, default=None):
- Predicate priority. If undefined, encoder is added at
- the end, with lowest priority.
- exact (bool, default=True):
- Determines the kind of encoder registered, an exact
- (default), or a compact representation encoder.
- Examples:
- @encoder('mytype')
- def mytype_exact_encoder(myobj):
- return myobj.to_json()
- Functional discriminator usage is appropriate for serialization
- of objects with a different classname, but which can be encoded
- with the same encoder:
- @encoder('BaseClass', lambda obj: isinstance(obj, BaseClass))
- def all_derived_classes_encoder(derived):
- return derived.base_encoder()
- """
- if exact:
- subregistry = _encode_handlers['exact']
- else:
- subregistry = _encode_handlers['compat']
- # if priority undefined, set it to lowest
- if priority is None:
- if len(subregistry['predicate']) > 0:
- priority = subregistry['predicate'][-1].priority + 100
- else:
- priority = 1000
- def _decorator(f):
- if predicate:
- subregistry['predicate'].add(
- _PredicatedEncoder(priority, predicate, f, classname))
- else:
- subregistry['classname'].setdefault(classname, f)
- return f
- return _decorator
- def _json_default_exact(obj):
- """Serialization handlers for types unsupported by `simplejson`
- that try to preserve the exact data types.
- """
- # first try predicate-based encoders
- for handler in _encode_handlers['exact']['predicate']:
- if handler.predicate(obj):
- return {
- "__class__": handler.typename,
- "__value__": handler.encoder(obj)
- }
- # then classname-based
- classname = type(obj).__name__
- if classname in _encode_handlers['exact']['classname']:
- return {
- "__class__": classname,
- "__value__": _encode_handlers['exact']['classname'][classname](obj)
- }
- raise TypeError(repr(obj) + " is not JSON serializable")
- def _json_default_compat(obj):
- """Serialization handlers that try to dump objects in
- compatibility mode. Similar to above.
- """
- for handler in _encode_handlers['compat']['predicate']:
- if handler.predicate(obj):
- return handler.encoder(obj)
- classname = type(obj).__name__
- if classname in _encode_handlers['compat']['classname']:
- return _encode_handlers['compat']['classname'][classname](obj)
- raise TypeError(repr(obj) + " is not JSON serializable")
- def decoder(classname):
- """A decorator for registering a new decoder for `classname`.
- Only ``exact`` decoders can be registered, since it is an assumption
- the ``compat`` mode serializes to standard JSON.
- Example:
- @decoder('mytype')
- def mytype_decoder(value):
- return mytype(value, reconstruct=True)
- """
- def _decorator(f):
- _decode_handlers.setdefault(classname, f)
- return _decorator
- def _json_object_hook(dict):
- """Deserialization handlers for types unsupported by `simplejson`.
- """
- classname = dict.get('__class__')
- if classname:
- constructor = _decode_handlers.get(classname)
- value = dict.get('__value__')
- if constructor:
- return constructor(value)
- raise TypeError("Unknown class: '%s'" % classname)
- return dict
- def _encoder_default_args(kw):
- """Shape default arguments for encoding functions."""
- # manual override of the preferred coding with `exact=False`
- if kw.pop('exact', getattr(_local, 'coding', CODING_DEFAULT) == EXACT):
- # settings necessary for the "exact coding"
- kw.update({
- 'default': _json_default_exact,
- 'use_decimal': False, # don't encode `Decimal` as JSON's `Number`
- 'tuple_as_array': False, # don't encode `tuple` as `Array`
- 'namedtuple_as_object':
- False # don't call `_asdict` on `namedtuple`
- })
- else:
- # settings for the "compatibility coding"
- kw.update({
- 'default': _json_default_compat,
- 'ignore_nan': True # be compliant with the ECMA-262 specification:
- # serialize nan/inf as null
- })
- # NOTE: if called from ``simplejson.dumps()`` with ``cls=JSONEncoder``,
- # we will receive all kw set to simplejson defaults -- and our defaults for
- # ``separators`` and ``for_json`` will not be applied. In contrast, they
- # are applied when called from ``jsonplus.dumps()``, unless user explicitly
- # sets some of those.
- # This causes inconsistent behaviour between ``dumps()`` and ``JSONEncoder()``.
- # prefer compact json repr
- kw.setdefault('separators', (',', ':'))
- # allow objects to provide json serialization on its behalf
- kw.setdefault('for_json', True)
- def _decoder_default_args(kw):
- """Shape default arguments for decoding functions."""
- kw.update({'object_hook': _json_object_hook})
- class JSONEncoder(json.JSONEncoder):
- def __init__(self, **kw):
- """Constructor for simplejson.JSONEncoder, with defaults overridden
- for jsonplus.
- """
- _encoder_default_args(kw)
- super(JSONEncoder, self).__init__(**kw)
- class JSONDecoder(json.JSONDecoder):
- def __init__(self, **kw):
- """Constructor for simplejson.JSONDecoder, with defaults overridden
- for jsonplus.
- """
- _decoder_default_args(kw)
- super(JSONDecoder, self).__init__(**kw)
- def dumps(*pa, **kw):
- _encoder_default_args(kw)
- return json.dumps(*pa, **kw)
- def loads(*pa, **kw):
- _decoder_default_args(kw)
- return json.loads(*pa, **kw)
- def pretty(x, sort_keys=True, indent=4 * ' ', separators=(',', ': '), **kw):
- kw.setdefault('sort_keys', sort_keys)
- kw.setdefault('indent', indent)
- kw.setdefault('separators', separators)
- return dumps(x, **kw)
- json_dumps = dumps
- json_loads = loads
- json_prettydump = pretty
- def np_to_list(value):
- return value.tolist()
- def generic_to_item(value):
- return value.item()
- _encode_handlers = {
- 'exact': {
- 'classname': {
- 'datetime':
- methodcaller('isoformat'),
- 'date':
- methodcaller('isoformat'),
- 'time':
- methodcaller('isoformat'),
- 'timedelta':
- partial(getattrs, attrs=['days', 'seconds', 'microseconds']),
- 'tuple':
- list,
- 'set':
- list,
- 'ndarray':
- np_to_list,
- 'float16':
- generic_to_item,
- 'float32':
- generic_to_item,
- 'frozenset':
- list,
- 'complex':
- partial(getattrs, attrs=['real', 'imag']),
- 'Decimal':
- str,
- 'Fraction':
- partial(getattrs, attrs=['numerator', 'denominator']),
- 'UUID':
- partial(getattrs, attrs=['hex']),
- 'Money':
- partial(getattrs, attrs=['amount', 'currency'])
- },
- 'predicate': SortedList(key=attrgetter('priority'))
- },
- 'compat': {
- 'classname': {
- 'datetime': methodcaller('isoformat'),
- 'date': methodcaller('isoformat'),
- 'time': methodcaller('isoformat'),
- 'set': list,
- 'ndarray': np_to_list,
- 'float16': generic_to_item,
- 'float32': generic_to_item,
- 'frozenset': list,
- 'complex': partial(getattrs, attrs=['real', 'imag']),
- 'Fraction': partial(getattrs, attrs=['numerator', 'denominator']),
- 'UUID': str,
- 'Currency': str,
- 'Money': str,
- },
- 'predicate': SortedList(key=attrgetter('priority'))
- }
- }
- # all decode handlers are for EXACT decoding BY CLASSNAME
- _decode_handlers = {
- 'datetime': parse_datetime,
- 'date': lambda v: parse_datetime(v).date(),
- 'time': lambda v: parse_datetime(v).timetz(),
- 'timedelta': kwargified(timedelta),
- 'tuple': tuple,
- 'set': set,
- 'ndarray': np.asarray,
- 'float16': np.float16,
- 'float32': np.float32,
- 'frozenset': frozenset,
- 'complex': kwargified(complex),
- 'Decimal': Decimal,
- 'Fraction': kwargified(Fraction),
- 'UUID': kwargified(uuid.UUID)
- }
- @encoder('namedtuple',
- lambda obj: isinstance(obj, tuple) and hasattr(obj, '_fields'))
- def _dump_namedtuple(obj):
- return {
- "name": type(obj).__name__,
- "fields": list(obj._fields),
- "values": list(obj)
- }
- @decoder('namedtuple')
- def _load_namedtuple(val):
- cls = namedtuple(val['name'], val['fields'])
- return cls(*val['values'])
- @encoder('timedelta', exact=False)
- def _timedelta_total_seconds(td):
- # timedelta.total_seconds() is only available since python 2.7
- return (td.microseconds +
- (td.seconds + td.days * 24 * 3600.0) * 10**6) / 10**6
- @encoder('Currency')
- def _dump_currency(obj):
- """Serialize standard (ISO-defined) currencies to currency code only,
- and non-standard (user-added) currencies in full.
- """
- from moneyed import CurrencyDoesNotExist, get_currency
- try:
- get_currency(obj.code)
- return obj.code
- except CurrencyDoesNotExist:
- return getattrs(obj, ['code', 'numeric', 'name', 'countries'])
- @decoder('Currency')
- def _load_currency(val):
- """Deserialize string values as standard currencies, but
- manually define fully-defined currencies (with code/name/numeric/countries).
- """
- from moneyed import get_currency
- try:
- return get_currency(code=val)
- except:
- return Currency(**val)
- @decoder('Money')
- def _load_money(val):
- # wrap with function to delay Currency/Money
- # parsing if not installed (and not needed)
- return Money(**val)
|