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

JSON serialization and schema generation #414

Merged
merged 53 commits into from
Jul 13, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
8b8e0ab
Refactored initial prototype to use JSONSerialization utility class
jlstevens Jun 10, 2020
be76c78
Defined Serialization base class
jlstevens Jun 10, 2020
707bd7d
Added safe keyword option to validate schema safety
jlstevens Jun 10, 2020
f7b4591
Fixed flakes
jlstevens Jun 10, 2020
bc4c070
Refactor to use an intermediate representation
jlstevens Jun 15, 2020
fc9bfb8
Generalized use of JSONNullable to support allow_None
jlstevens Jun 15, 2020
0acabb7
Simplified tuple schema handling and now enforcing tuple length
jlstevens Jun 15, 2020
bc75b82
Refactored enforcement of numeric bounds
jlstevens Jun 15, 2020
9de53c9
Setting the parameter label as the schema title
jlstevens Jun 15, 2020
2c608a1
Added support for ObjectSelector schema
jlstevens Jun 15, 2020
c353f7b
Added support for ListSelector schema
jlstevens Jun 15, 2020
e8a8879
Fixed flake
jlstevens Jun 15, 2020
56d5089
Removed underscore from schema, serialize and deserialize methods
jlstevens Jun 15, 2020
edadd08
Declared serialize and deserialize as classmethods
jlstevens Jun 15, 2020
4d58cb8
Removed use of the custom JSONEncoder for now
jlstevens Jun 15, 2020
1095a73
Removed unused import
jlstevens Jun 15, 2020
268df55
Added support for DataFrame serialization and schema
jlstevens Jun 18, 2020
c1c3978
Made serializer import optional
jlstevens Jun 18, 2020
4652be7
Fixed flake
jlstevens Jun 18, 2020
b2c43b8
Added subset argument to choose from list of available parameters
jlstevens Jul 6, 2020
8fe0c45
Parameterized serialization type with 'mode' argument
jlstevens Jul 6, 2020
b1ba0ff
Deleted trailing whitespace
jlstevens Jul 6, 2020
86fbb9c
Added deserialization methods
jlstevens Jul 6, 2020
c271dae
Preserving microseconds information in datetimes
jlstevens Jul 6, 2020
c2877eb
Added initial set of unit tests for JSON serialization
jlstevens Jul 6, 2020
2ffe8de
Removed filtering out of the 'name' parameter
jlstevens Jul 6, 2020
f53514f
Fixed flake
jlstevens Jul 6, 2020
de8c90b
Fixed allow_None argument to DataFrame parameter
jlstevens Jul 6, 2020
1724aa6
Separated out parameters that depend on availability of pandas
jlstevens Jul 6, 2020
f9148da
Moved parameters that depend on pandas
jlstevens Jul 6, 2020
6cca052
Removed pandas specific comparison
jlstevens Jul 6, 2020
42df5cc
Updated testing dependencies
jlstevens Jul 6, 2020
a660c67
Replaced use of isoformat method with strftime for 2.7+ support
jlstevens Jul 7, 2020
0121be6
Removed pandas from test dependency list
jlstevens Jul 7, 2020
737d26b
Replaced use of fromisoformat method with strptime for 2.7+ support
jlstevens Jul 7, 2020
0b0c884
Fixed unqualified use of datetime
jlstevens Jul 7, 2020
3084a0f
Added dict_schema method
jlstevens Jul 7, 2020
c165634
Added support for CalendarDate parameters
jlstevens Jul 7, 2020
1b5b0fe
Added support for datetime64 serialization from param.Date
jlstevens Jul 7, 2020
157d8bf
Added CalendarDate test parameter
jlstevens Jul 9, 2020
f5bb573
Added serialization support for param.Array
jlstevens Jul 9, 2020
f1a838c
Added param.Array test parameter
jlstevens Jul 9, 2020
4e0445b
Fixed flake
jlstevens Jul 9, 2020
8427df7
Updated tests involving numpy and pandas
jlstevens Jul 9, 2020
ac9fc52
Excluding numpy and pandas from regular instance tests
jlstevens Jul 9, 2020
e296a14
Checking unsafe parameter types raise exception in schema
jlstevens Jul 9, 2020
5e9bab1
Fixed unsafe testing when numpy/pandas are unavailable
jlstevens Jul 9, 2020
b7670e3
Added tests for conditionally unsafe schemas
jlstevens Jul 9, 2020
a7a4449
Renamed serialize and deserialize methods to ir_serialize and ir_dese…
jlstevens Jul 9, 2020
1b883fb
Added test for param.Dict
jlstevens Jul 9, 2020
a204a79
Added noqa annotation to stub methods
jlstevens Jul 9, 2020
191d431
Removed ir_ prefix from serialize and deserialize methods
jlstevens Jul 10, 2020
8c1adb9
Using asarray to avoid copying
jlstevens Jul 13, 2020
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
20 changes: 20 additions & 0 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import random
import numbers
import operator
from .serializer import JSONSerialization
jlstevens marked this conversation as resolved.
Show resolved Hide resolved

from collections import defaultdict, namedtuple, OrderedDict
from operator import itemgetter,attrgetter
Expand Down Expand Up @@ -687,6 +688,8 @@ class Foo(Bar):
# class is created, owner, name, and _internal_name are
# set.

_serializer = JSONSerialization

def __init__(self,default=None,doc=None,label=None,precedence=None, # pylint: disable-msg=R0913
instantiate=False,constant=False,readonly=False,
pickle_default_value=True, allow_None=False,
Expand Down Expand Up @@ -731,6 +734,15 @@ class hierarchy (see ParameterizedMetaclass).
self.per_instance = per_instance


def _serialize(self, value):
return JSONSerialization.serialize(self.__class__.__name__, value)

def _deserialize(self, string):
return JSONSerialization.deserialize(self.__class__.__name__, string)

def _schema(self, safe=False):
return JSONSerialization.parameter_schema(self.__class__.__name__, self, safe=safe)

@property
def label(self):
if self.name and self._label is None:
Expand Down Expand Up @@ -1566,6 +1578,14 @@ class or instance that contains an iterable collection of
for obj in sublist:
obj.param.set_dynamic_time_fn(time_fn,sublistattr)

def serialize_parameters(self_):
self_or_cls = self_.self_or_cls
return Parameter._serializer.serialize_parameters(self_or_cls)

def schema(self_, safe=False):
self_or_cls = self_.self_or_cls
return Parameter._serializer.schema(self_or_cls, safe=safe)

def get_param_values(self_,onlychanged=False):
"""
Return a list of name,value pairs for all Parameters of this
Expand Down
178 changes: 178 additions & 0 deletions param/serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""
Classes used to support string serialization of Parameters and
Parameterized objects.
"""

import json
import datetime as dt

class UnserializableException(Exception):
pass

class UnsafeserializableException(Exception):
pass

def JSONNullable(json_type):
"Express a JSON schema type as nullable to easily support Parameters that allow_None"
return { "anyOf": [ json_type, { "type": "null"}] }



class Serialization(object):
"""
Base class used to implement different types of serialization.
"""

@classmethod
def schema(cls, pobj):
raise NotImplementedError

@classmethod
def serialize_parameters(cls, pobj):
raise NotImplementedError



class JSONSerialization(Serialization):
"""
Class responsible for specifying JSON serialization, deserialization
and JSON schemas for Parameters and Parameterized classes and
objects.
"""

unserializable_parameter_types = ['Callable']

json_schema_literal_types = {int:'integer', float:'number', str:'string'}
philippjfr marked this conversation as resolved.
Show resolved Hide resolved


@classmethod
def schema(cls, pobj, safe=False):
schema = {}
for name, p in pobj.param.objects('existing').items():
schema[name] = p._schema(safe=safe)
if p.doc:
schema[name]["description"] = p.doc.strip()
return schema

@classmethod
def serialize_parameters(cls, pobj):
components = {}
for name, p in pobj.param.objects('existing').items():
value = pobj.param.get_value_generator(name)
components[name] = p._serialize(value)

contents = ', '.join('"%s":%s' % (name, sval) for name, sval in components.items())
return '{{{contents}}}'.format(contents=contents)

# Parameter level methods

@classmethod
def _get_method(cls, ptype, suffix):
"Returns specialized method if available, otherwise None"
method_name = ptype.lower()+'_' + suffix
return getattr(cls, method_name, None)

@classmethod
def parameter_schema(cls, ptype, p, safe=False):
if ptype in cls.unserializable_parameter_types:
raise UnserializableException
dispatch_method = cls._get_method(ptype, 'schema')
if dispatch_method:
return dispatch_method(p, safe=safe)
else:
return { "type": ptype.lower()}

@classmethod
def serialize(cls, ptype, value):
dispatch_method = cls._get_method(ptype, 'serialize')
if dispatch_method:
return dispatch_method(value)
else:
return json.dumps(value)

@classmethod
def deserialize(cls, ptype, string):
dispatch_method = cls._get_method(ptype, 'deserialize')
if dispatch_method:
return dispatch_method(string)
else:
return json.loads(string)

# Custom Serializers

@classmethod
def date_serialize(cls, value):
string = value.replace(microsecond=0).isoformat() # Test *with* microseconds.
return json.dumps(string)

# Custom Deserializers

@classmethod
def date_deserialize(cls, string):
return dt.datetime.fromisoformat(string) # FIX: 3.7+ only

@classmethod
def tuple_deserialize(cls, string):
return tuple(json.loads(string))

# Custom Schemas

@classmethod
def date_schema(cls, p, safe=False):
return { "type": "string", "format": "date-time"}

@classmethod
def tuple_schema(cls, p, safe=False):
return { "type": "array"}
philippjfr marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
def number_schema(cls, p, safe=False):
schema = { "type": p.__class__.__name__.lower() }
if p.bounds is not None:
(low, high) = p.bounds
if low is not None:
key = 'minimum' if p.inclusive_bounds[0] else 'exclusiveMinimum'
schema[key] = low
if high is not None:
key = 'maximum' if p.inclusive_bounds[0] else 'exclusiveMaximum'
schema[key] = high

return JSONNullable(schema) if p.allow_None else schema

@classmethod
def integer_schema(cls, p, safe=False):
return cls.number_schema(p)

@classmethod
def numerictuple_schema(cls, p, safe=False):
return {"type": "array",
"additionalItems": { "type": "number" }}
philippjfr marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
def xycoordinates_schema(cls, p, safe=False):
return {
"type": "array",
"items": [
{"type": "number"}, {"type": "number"}],
"additionalItems": False
}

@classmethod
def range_schema(cls, p, safe=False):
# Extend for allow None
return {
"type": "array",
"items": [{"type": "number"},{"type": "number"}],
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
#"additionalItems": "false"
}

@classmethod
def list_schema(cls, p, safe=False):
schema = { "type": "array"}
if safe is True and p.class_ is None:
msg = ('List without a class specified cannot be guaranteed '
'to be safe for serialization')
raise UnsafeserializableException(msg)
if p.class_ is not None and p.class_ in cls.json_schema_literal_types:
schema['items'] = {"type": cls.json_schema_literal_types[p.class_]}
return schema