#
# This file is a part of the normalize python library
#
# normalize is free software: you can redistribute it and/or modify
# it under the terms of the MIT License.
#
# normalize is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# MIT License for more details.
#
# You should have received a copy of the MIT license along with
# normalize. If not, refer to the upstream repository at
# http://github.com/hearsaycorp/normalize
#
from __future__ import absolute_import
from copy import deepcopy
import inspect
import json
import types
from normalize.coll import Collection
from normalize.coll import ListCollection as RecordList
from normalize.diff import Diff
from normalize.diff import DiffInfo
import normalize.exc as exc
from normalize.property.json import JsonProperty
from normalize.record import OhPickle
from normalize.record import Record
def _json_to_value_initializer(json_val, proptype):
if proptype:
if isinstance(proptype, JsonRecord):
return json_val
elif hasattr(proptype, "from_json"):
return proptype.from_json(json_val)
elif isinstance(proptype, Record) and isinstance(json_val, dict):
return from_json(proptype, json_val)
return json_val
[docs]def json_to_initkwargs(record_type, json_struct, kwargs=None):
"""This function converts a JSON dict (json_struct) to a set of init
keyword arguments for the passed Record (or JsonRecord).
It is called by the JsonRecord constructor. This function takes a JSON
data structure and returns a keyword argument list to be passed to the
class constructor. Any keys in the input dictionary which are not known
are passed as a single ``unknown_json_keys`` value as a dict.
This function should generally not be called directly, except as a part of
a ``__init__`` or specialized visitor application.
"""
if kwargs is None:
kwargs = {}
if json_struct is None:
json_struct = {}
if not isinstance(json_struct, dict):
raise TypeError(
"dict expected, found %s" % type(json_struct).__name__
)
unknown_keys = set(json_struct.keys())
for propname, prop in record_type.properties.iteritems():
# think "does" here rather than "is"; the slot does JSON
if isinstance(prop, JsonProperty):
json_name = prop.json_name
if json_name is not None:
if json_name in json_struct:
if propname not in kwargs:
kwargs[propname] = _json_to_value_initializer(
prop.from_json(
json_struct[json_name]
),
prop.valuetype,
)
unknown_keys.remove(json_name)
elif prop.name in json_struct:
json_val = json_struct[prop.name]
unknown_keys.remove(prop.name)
if prop.name not in kwargs:
proptype = prop.valuetype
kwargs[propname] = _json_to_value_initializer(
json_val, proptype,
)
if unknown_keys:
kwargs["unknown_json_keys"] = dict(
(k, deepcopy(json_struct[k])) for k in unknown_keys
)
return kwargs
[docs]def from_json(record_type, json_struct):
"""JSON marshall in function: a 'visitor' function which looks for JSON
types/hints on types being converted to, but does not require them.
Args:
``record_type=``\ *TYPE*
Record type to convert data to
``json_struct=``\ *DICT|LIST*
a loaded (via ``json.loads``) data structure, normally a
dict or a list.
"""
if issubclass(record_type, JsonRecord):
return record_type(json_struct)
elif issubclass(record_type, Record):
# do what the default JsonRecord __init__ does
init_kwargs = json_to_initkwargs(record_type, json_struct)
instance = record_type(**init_kwargs)
return instance
else:
raise exc.JsonRecordCoerceError(
given=repr(json_struct),
type=record_type.__name__,
)
# caches for _json_data
has_json_data = dict()
json_data_takes_extraneous = dict()
def _json_data(x, extraneous):
"""This function calls a json_json method, if the type has one, otherwise
calls back into to_json(). It also check whether the method takes an
'extraneous' argument and passes that through if possible."""
if type(x) in has_json_data and has_json_data[type(x)]:
if json_data_takes_extraneous[type(x)]:
return x.json_data(extraneous=extraneous)
else:
return x.json_data()
else:
htj = hasattr(x, "json_data") and callable(x.json_data)
has_json_data[type(x)] = htj
if htj:
argspec = inspect.getargspec(x.json_data)
tjte = 'extraneous' in argspec.args or argspec.keywords
json_data_takes_extraneous[type(x)] = tjte
if tjte:
return x.json_data(extraneous=extraneous)
else:
return x.json_data()
else:
return to_json(x, extraneous)
[docs]def to_json(record, extraneous=True, prop=None):
"""JSON conversion function: a 'visitor' function which implements marshall
out (to JSON data form), honoring JSON property types/hints but does not
require them. To convert to an actual JSON document, pass the return value
to ``json.dumps`` or a similar function.
args:
``record=``\ *anything*
This object can be of any type; a best-effort attempt is made to
convert to a form which ``json.dumps`` can accept; this function
will call itself recursively, respecting any types which define
``.json_data()`` as a method and calling that.
``extraneous=``\ *BOOL*
This parameter is passed through to any ``json_data()`` methods
which support it.
``prop=``\ *PROPNAME*\ |\ *PROPERTY*
Specifies to return the given property from an object, calling any
``to_json`` mapping defined on the property. Does not catch the
``AttributeError`` that is raised by the property not being set.
"""
if prop:
if isinstance(prop, basestring):
prop = type(record).properties[prop]
val = prop.__get__(record)
if hasattr(prop, "to_json"):
return prop.to_json(val, extraneous, _json_data)
else:
return _json_data(val, extraneous)
elif isinstance(record, Collection):
return list(_json_data(x, extraneous) for x in record)
elif isinstance(record, Record):
rv_dict = {}
for propname, prop in type(record).properties.iteritems():
if not extraneous and prop.extraneous:
pass
elif not hasattr(prop, "json_name") or prop.json_name is not None:
json_name = getattr(prop, "json_name", prop.name)
try:
rv_dict[json_name] = to_json(record, extraneous, prop)
except AttributeError:
pass
return rv_dict
elif isinstance(record, long):
return str(record) if abs(record) > 2**50 else record
elif isinstance(record, dict):
return dict(
(k, _json_data(v, extraneous)) for k, v in record.iteritems()
)
elif isinstance(record, (list, tuple, set, frozenset)):
return list(_json_data(x, extraneous) for x in record)
elif isinstance(record, (basestring, int, float, types.NoneType)):
return record
else:
raise TypeError(
"I don't know how to marshall a %s to JSON" %
type(record).__name__
)
[docs]class JsonRecord(Record):
"""Version of a Record which deals primarily in JSON form.
This means:
1. The first argument to the constructor is assumed to be a JSON data
dictionary, and passed through the class' ``json_to_initkwargs``
method before being used to set actual properties
2. Unknown keys are permitted, and saved in the "unknown_json_keys"
property, which is merged back on output (ie, calling ``.json_data()``
or ``to_json()``)
"""
unknown_json_keys = JsonProperty(json_name=None, extraneous=True)
[docs] def __init__(self, json_data=None, **kwargs):
"""Build a new JsonRecord sub-class.
args:
``json_data=``\ *DICT|other*
JSON data (string or already ``json.loads``'d). If not
a JSON dictionary with keys corresponding to the
``json_name`` or the properties within, then
``json_to_initkwargs`` should be overridden to handle
the unpacking differently
``**kwargs``
``JsonRecord`` instances may also be constructed by
passing in attribute initializers in keyword form. The
keys here should be the names of the attributes and the
python values, not the JSON names or form.
"""
if isinstance(json_data, OhPickle):
return
if isinstance(json_data, basestring):
json_data = json.loads(json_data)
if json_data is not None:
kwargs = type(self).json_to_initkwargs(json_data, kwargs)
super(JsonRecord, self).__init__(**kwargs)
@classmethod
[docs] def json_to_initkwargs(self, json_data, kwargs):
"""Subclassing hook to specialize how JSON data is converted
to keyword arguments"""
if isinstance(json_data, basestring):
json_data = json.loads(json_data)
return json_to_initkwargs(self, json_data, kwargs)
@classmethod
[docs] def from_json(self, json_data):
"""This method can be overridden to specialize how the class is loaded
when marshalling in; however beware that it is not invoked when the
caller uses the ``from_json()`` function directly."""
return self(json_data)
[docs] def json_data(self, extraneous=False):
"""Returns the JSON data form of this ``JsonRecord``. The 'unknown'
JSON keys will be merged back in, if:
1. the ``extraneous=True`` argument is passed.
2. the ``unknown_json_keys`` property on this class is replaced by one
not marked as ``extraneous``
"""
jd = to_json(self, extraneous)
if hasattr(self, "unknown_json_keys"):
prop = type(self).properties['unknown_json_keys']
if extraneous or not prop.extraneous:
for k, v in self.unknown_json_keys.iteritems():
if k not in jd:
jd[k] = v
return jd
[docs] def diff_iter(self, other, **kwargs):
"""Generator method which returns the differences from the invocant to
the argument. This specializes :py:meth:`Record.diff_iter` by
returning :py:class:`JsonDiffInfo` objects.
"""
for diff in super(JsonRecord, self).diff_iter(other, **kwargs):
# TODO: object copy/upgrade constructor
newargs = diff.__getstate__()
yield JsonDiffInfo(**(newargs))
[docs] def diff(self, other, **kwargs):
"""Compare an object with another. This specializes
:py:meth:`Record.diff` by returning a :py:class:`JsonDiff` object.
"""
return JsonDiff(
base_type_name=type(self).__name__,
other_type_name=type(other).__name__,
values=self.diff_iter(other, **kwargs),
)
[docs]class JsonRecordList(RecordList, JsonRecord):
"""Version of a RecordList which deals primarily in JSON"""
[docs] def __init__(self, json_data=None, **kwargs):
"""Build a new JsonRecord sub-class.
Args:
``json_data=``\ *LIST|other*
JSON data (string or already ``json.loads``'d)
``**kwargs``
Other initializer attributes, for lists with extra
attributes (eg, paging information)
"""
if isinstance(json_data, OhPickle):
return
if isinstance(json_data, basestring):
json_data = json.loads(json_data)
if json_data is not None:
kwargs = type(self).json_to_initkwargs(json_data, kwargs)
super(JsonRecordList, self).__init__(**kwargs)
@classmethod
def json_to_initkwargs(cls, json_struct, kwargs):
member_type = cls.itemtype
if kwargs.get('values', None) is None:
kwargs['values'] = values = []
if not json_struct:
json_struct = tuple()
if hasattr(member_type, "from_json"):
for x in json_struct:
values.append(member_type.from_json(x))
elif issubclass(member_type, Record):
for x in json_struct:
values.append(from_json(member_type, x))
else:
raise exc.CollectionDefinitionError(
coll="JsonRecordList",
property='itemtype',
)
return kwargs
def json_data(self, extraneous=False):
# this method intentionally does not call the superclass json_data,
# because this function returns a collection.
return to_json(self, extraneous)
def __repr__(self):
super_repr = super(JsonRecordList, self).__repr__()
return super_repr.replace("[", "values=[", 1)
[docs]class JsonDiffInfo(DiffInfo, JsonRecord):
"""Version of 'DiffInfo' that supports ``.json_data()``"""
def json_data(self):
return dict(
diff_type=self.diff_type.canonical_name,
base=self.base.selectors,
other=self.other.selectors,
)
[docs]class JsonDiff(Diff, JsonRecordList):
"""Version of 'Diff' that supports ``.json_data()``"""
itemtype = JsonDiffInfo