diff --git a/.travis.yml b/.travis.yml index 1a56f26..bb38d26 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,7 @@ language: python python: - - "2.7" -# command to install dependencies -install: "pip install -r requirements.txt" -# command to run tests +- '3.6' +install: pip install -r requirements.txt script: py.test deploy: provider: script @@ -12,5 +10,5 @@ deploy: branch: master env: global: - - ENCRYPTION_LABEL: "3e8c259749ef" - - COMMIT_AUTHOR_EMAIL: "voidfiles@gmail.com" + - ENCRYPTION_LABEL: 1dbde6ec09a5 + - COMMIT_AUTHOR_EMAIL: voidfiles@gmail.com diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9340b01 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3 + + +ADD . /opt/code +WORKDIR /opt/code/ + +RUN pip install -r requirements.txt +CMD ["python", "benchmark.py"] diff --git a/README.md b/README.md index cd59d92..34eefc2 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,27 @@ You can find the latest benchmarks on [this page](https://voidfiles.github.io/py Currently the following projects are benchmarked. -* [Django REST framework](http://www.django-rest-framework.org/) -* [serpy](https://github.com/clarkduvall/serpy) -* [marshmallow](https://marshmallow.readthedocs.io/en/latest/) +* [Django REST Framework](http://www.django-rest-framework.org/) +* [serpy](http://serpy.readthedocs.io/) +* [Marshmallow](https://marshmallow.readthedocs.io/en/latest/) * [Strainer](https://github.com/voidfiles/strainer) -* [Lollipop](https://github.com/maximkulkin/lollipop) +* [Lollipop](http://lollipop.readthedocs.io/en/latest/) +* [Kim](http://kim.readthedocs.io/en/latest/) +* [Toasted Marshmallow](https://github.com/lyft/toasted-marshmallow) +* [Colander](https://docs.pylonsproject.org/projects/colander/en/latest/) +* [Lima](https://github.com/b6d/lima/) +- [Serpyco](https://gitlab.com/sgrignard/serpyco) +* [Avro](https://avro.apache.org/) -Along with a baseline of a custom function that doesn't use a framework. +Along with a baseline custom function that doesn't use a framework. -Each framework is asked to serialize a list of 2 objects a 1000 times, and then 1 object a 1000 times. +## Running the test suite + +A Docker container is bundled with the repository which you can use to run the benchmarks. Firstly make sure you have Docker installed. + +1. Install Docker + +2. Build the container `$ docker-compose build` + +3. Run the tests. `$ docker-compose run --rm tests` diff --git a/benchmark.py b/benchmark.py old mode 100644 new mode 100755 index e8d9dd9..c757131 --- a/benchmark.py +++ b/benchmark.py @@ -2,10 +2,10 @@ from tabulate import tabulate from contextlib import contextmanager -from subjects import (marsh, rf, serp, strain, hand, loli) +from subjects import (marsh, rf, serp, strain, col, hand, loli, k, lim, tmarsh, avro, pickle, serpy) from data import ParentTestObject -SUBJECTS = (marsh, rf, serp, strain, hand, loli) +SUBJECTS = (marsh, rf, serp, strain, col, hand, loli, k, lim, tmarsh, avro, pickle, serpy) test_object = ParentTestObject() @@ -19,17 +19,17 @@ def timer(tracker): def test_many(func, limit=1000): - for i in xrange(0, limit): + for i in range(0, limit): subject.serialization_func([test_object, test_object], True) def test_one(func, limit=1000): - for i in xrange(0, limit): + for i in range(0, limit): subject.serialization_func(test_object, False) table = [] for subject in SUBJECTS: - row = [subject.__name__] + row = [subject.name] test_many(subject.serialization_func, 2) # Warmup with timer(row): @@ -42,4 +42,8 @@ def test_one(func, limit=1000): table += [row] table = sorted(table, key=lambda x: x[1] + x[2]) -print tabulate(table, headers=['Library', 'Many Objects', 'One Object',]) +relative_base = min([x[1] + x[2] for x in table]) +for row in table: + result = (row[1] + row[2]) / relative_base + row.append(result) +print(tabulate(table, headers=['Library', 'Many Objects (seconds)', 'One Object (seconds)', 'Relative'])) diff --git a/create_report.sh b/create_report.sh index 8e2adf2..320a75c 100755 --- a/create_report.sh +++ b/create_report.sh @@ -1,8 +1,18 @@ #! /bin/bash cat README.md > REPORT.md -echo "
" >> REPORT.md
+echo "\`\`\`" >> REPORT.md
 python benchmark.py >> REPORT.md
-echo "
" >> REPORT.md +echo "\`\`\`" >> REPORT.md +cat disscussion.md >> REPORT.md mkdir -p out -python -m markdown REPORT.md > out/index.html +echo '' > out/index.html +echo '' >> out/index.html +echo '' >> out/index.html +echo '' >> out/index.html +echo '' >> out/index.html +echo '
' >> out/index.html +python -m markdown -x markdown.extensions.fenced_code REPORT.md >> out/index.html +echo '
' >> out/index.html +echo '' >> out/index.html +echo '' >> out/index.html diff --git a/data.py b/data.py index e7aa044..ff11bab 100644 --- a/data.py +++ b/data.py @@ -11,7 +11,7 @@ class ParentTestObject(object): def __init__(self): self.foo = 'bar' self.sub = ChildTestObject() - self.subs = [ChildTestObject(i) for i in xrange(10)] + self.subs = [ChildTestObject(i) for i in range(10)] def bar(self): return 5 diff --git a/deploy-key.enc b/deploy-key.enc new file mode 100644 index 0000000..4c226d2 Binary files /dev/null and b/deploy-key.enc differ diff --git a/deploy.sh b/deploy.sh index 1965fda..40dbb59 100755 --- a/deploy.sh +++ b/deploy.sh @@ -48,10 +48,13 @@ ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key" ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv" ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR} ENCRYPTED_IV=${!ENCRYPTED_IV_VAR} -openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in ../deploy_key.enc -out deploy_key -d -chmod 600 deploy_key +openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in ../deploy-key.enc -out deploy-key -d +chmod 600 deploy-key eval `ssh-agent -s` -ssh-add deploy_key +ssh-add deploy-key + +rm -fR __pycache__ +rm -fR subjects/__pycache__ # Now that we're all set up, we can push. git push $SSH_REPO $TARGET_BRANCH diff --git a/deploy_key.enc b/deploy_key.enc deleted file mode 100644 index 878c05c..0000000 Binary files a/deploy_key.enc and /dev/null differ diff --git a/disscussion.md b/disscussion.md new file mode 100644 index 0000000..1494574 --- /dev/null +++ b/disscussion.md @@ -0,0 +1,127 @@ +## The Benchmark + +Each framework is asked to serialize a list of 2 objects a 1000 times, and then 1 object a 1000 times. + +This is the current object that is being serialized. + +```python +class ChildTestObject(object): + def __init__(self, multiplier=None): + self.w = 1000 * multiplier if multiplier else 100 + self.x = 20 * multiplier if multiplier else 20 + self.y = 'hello' * multiplier if multiplier else 'hello' + self.z = 10 * multiplier if multiplier else 10 + + +class ParentTestObject(object): + def __init__(self): + self.foo = 'bar' + self.sub = ChildTestObject() + self.subs = [ChildTestObject(i) for i in xrange(10)] + + def bar(self): + return 5 + +benchmark_object = ParentTestObject() +``` + +## Discussion + +Serialization from python objects to JSON, XML, or other transmission formats is a common task for many web related projects. In order to fill that need a number of frameworks have arised. While their aims are similar, they don't all share the same attributes. Here are how some of the features comapre. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProjectSerializationEncodingDeserializationValidation
Django REST FrameworkYesYesYesYes
serpyYesNoNoNo
MarshmallowYesYesYesYes
LollipopYesNoYesYes
StrainerYesNoYesYes
KimYesNoYesYes
serpycoYesYesYesYes
Toasted MarshmallowYesYesYesYes
ColanderYesNoYes<Yes
LimaYesNoNoNo
AvroYesYesYesNo
+ +* **Serialization**: Does the framework provide a way of serializing python objects to simple datastructures +* **Encoding**: Does the framework provide a way of encoding data into a wire format +* **Deserialization**: Does the framework provide a way of deserializing simple data structures into complex data structures +* **Validation**: Does the framework provide a way of validating datastructures, and reprorting error conditions +* **Part of Framework**: Is serialization apart of a larger framework diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..21ff9bc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,6 @@ +tests: + build: . + command: python benchmark.py + dockerfile: Dockerfile + volumes: + - .:/opt/code diff --git a/requirements.txt b/requirements.txt index 172f71b..8d71761 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,14 @@ -marshmallow==2.10.4 -serpy==0.1.1 -djangorestframework==3.5.3 -django==1.9 +toastedmarshmallow==2.15.2.post1 +serpy==0.3.1 +colander==1.7.0 +djangorestframework==3.9.4 +django==2.2.13 pytest -pystrainer==0.0.5 +pystrainer==1.3.0 tabulate==0.7.7 -lollipop==1.0.1 +lollipop==1.1.7 +py-kim==1.2.2 +lima==0.5 markdown +avro-python3==1.8.2 +serpyco==1.1.0 \ No newline at end of file diff --git a/subjects/avro.py b/subjects/avro.py new file mode 100644 index 0000000..fe797b2 --- /dev/null +++ b/subjects/avro.py @@ -0,0 +1,74 @@ +import avro.schema +from avro.io import DatumWriter + +name = 'Avro' + +class NullWriter(object): + def write(self, *args, **kwargs): + pass + + def write_utf8(self, *args, **kwargs): + self.write(args, kwargs) + + def write_int(self, *args, **kwargs): + self.write(args, kwargs) + + def write_long(self, *args, **kwargs): + self.write(args, kwargs) + +schema_name_tracker = avro.schema.Names() + +child_test_object_schema = avro.schema.SchemaFromJSONData({ + "namespace": "benchmark.avro", + "type": "record", + "name": "childTestObject", + "fields": [ + {"name": "w", "type": "int"}, + {"name": "x", "type": "int"}, + {"name": "y", "type": "string"}, + {"name": "z", "type": "int"} + ] +}, names=schema_name_tracker) + +parent_test_object_schema = avro.schema.SchemaFromJSONData({ + "namespace": "benchmark.avro", + "type": "record", + "name": "parentTestObject", + "fields": [ + {"name": "foo", "type": "string"}, + {"name": "sub", "type": "childTestObject"}, + { + "name": "subs", + "type": { + "type": "array", + "items": "childTestObject" + } + } + ] +}, names=schema_name_tracker) + +datum_writer = DatumWriter(writer_schema=parent_test_object_schema) +null_binary_writer = NullWriter() + +# Avro for Python can't directly serialize a class, it must be a dictionary. +def nested_class_to_dict(obj): + if not hasattr(obj,"__dict__"): + return obj + result = {} + for key, val in obj.__dict__.items(): + if key.startswith("_"): + continue + element = [] + if isinstance(val, list): + for item in val: + element.append(nested_class_to_dict(item)) + else: + element = nested_class_to_dict(val) + result[key] = element + return result + +def serialization_func(to_serialize, many): + if many: + return [datum_writer.write(nested_class_to_dict(obj), null_binary_writer) for obj in to_serialize] + else: + return datum_writer.write(nested_class_to_dict(to_serialize), null_binary_writer) diff --git a/subjects/col.py b/subjects/col.py new file mode 100755 index 0000000..fe2e3b5 --- /dev/null +++ b/subjects/col.py @@ -0,0 +1,111 @@ +import colander +from colander import null + +name = 'Colander' + + +class ObjectType(colander.Mapping): + """ + A colander type representing a generic python object + (uses colander mapping-based serialization). + """ + + def serialize(self, node, appstruct): + appstruct = appstruct.__dict__ + return super(ObjectType, self).serialize(node, appstruct) + + def deserialize(self, node, cstruct): + data = super(ObjectType, self).deserialize(node, cstruct) + appstruct = node.instance.__class__() + appstruct.__dict__.update(data) + return appstruct + + +class ObjectSchema(colander.SchemaNode): + schema_type = ObjectType + instance = None + + def serialize(self, appstruct): + if not self.instance: + # set the instance on all child schema nodes as they + # may need to access the instance environment + self.instance = appstruct + for subnode in self.children: + if isinstance(subnode, MethodSchema): + setattr(subnode, 'instance', appstruct) + return super(ObjectSchema, self).serialize(appstruct) + + def deserialize(self, cstruct): + appstruct = super(ObjectSchema, self).deserialize(cstruct) + if not self.instance: + self.instance = appstruct + return appstruct + + +class CallableSchema(colander.SchemaNode): + def serialize(self, appstruct): + if appstruct is null: + return null + appstruct = appstruct() + return super(CallableSchema, self).serialize(appstruct) + + def deserialize(self, cstruct): + if cstruct is null: + return null + appstruct = super(CallableSchema, self).deserialize(cstruct) + return lambda: appstruct + + +class MethodSchema(CallableSchema): + def serialize(self, appstruct): + if appstruct is null: + appstruct = getattr(self.instance, self.name) + return super(MethodSchema, self).serialize(appstruct) + + +class Differential(colander.SchemaNode): + def __init__(self, typ, differential=0): + self.differential = differential + super(Differential, self).__init__(typ) + + def serialize(self, appstruct): + # operator could be overloaded by the appstruct class if necessary + appstruct += self.differential + return super(Differential, self).serialize(appstruct) + + def deserialize(self, cstruct): + # operator could be overloaded by the appstruct class if necessary + appstruct = super(Differential, self).deserialize(cstruct) + appstruct -= self.differential + return appstruct + + +class ChildSchema(ObjectSchema): + w = colander.SchemaNode(colander.Int()) + y = colander.SchemaNode(colander.String()) + x = Differential(colander.Int(), 10) + z = colander.SchemaNode(colander.Int()) + + +class ChildListSchema(colander.SequenceSchema): + sub = ChildSchema() + + +class ParentSchema(ObjectSchema): + foo = colander.SchemaNode(colander.String()) + bar = MethodSchema(colander.Int()) + sub = ChildSchema() + subs = ChildListSchema() + + +class ParentListSchema(colander.SequenceSchema): + parents = ParentSchema() + + +unit_schema = ParentSchema() +seq_schema = ParentListSchema() + + +def serialization_func(obj, many): + schema = seq_schema if many else unit_schema + return schema.serialize(obj) diff --git a/subjects/hand.py b/subjects/hand.py index 15788bd..8e29b8e 100644 --- a/subjects/hand.py +++ b/subjects/hand.py @@ -1,4 +1,4 @@ -__name__ = 'Custom' +name = 'Custom' def sub_to_cstruct(obj): diff --git a/subjects/k.py b/subjects/k.py new file mode 100644 index 0000000..7e49c89 --- /dev/null +++ b/subjects/k.py @@ -0,0 +1,42 @@ +from kim import Mapper, field + +name = 'kim' + + +class Complex(object): + pass + + +class SubResource(object): + pass + + +def bar_pipe(session): + session.output['bar'] = session.data() + + +def x_pipe(session): + session.output['x'] = session.data + 10 + + +class SubMapper(Mapper): + __type__ = SubResource + w = field.String() + x = field.String(extra_serialize_pipes={'output': [x_pipe]}) + y = field.String() + z = field.String() + + +class ComplexMapper(Mapper): + __type__ = Complex + foo = field.String() + bar = field.String(extra_serialize_pipes={'output': [bar_pipe]}) + sub = field.Nested(SubMapper) + subs = field.Collection(field.Nested(SubMapper)) + + +def serialization_func(obj, many): + if many: + return ComplexMapper.many().serialize(obj) + else: + return ComplexMapper(obj=obj).serialize() diff --git a/subjects/lim.py b/subjects/lim.py new file mode 100644 index 0000000..4061d9f --- /dev/null +++ b/subjects/lim.py @@ -0,0 +1,28 @@ +import lima + +name = 'lima' + + +class SubM(lima.Schema): + w = lima.fields.Integer() + x = lima.fields.Integer(get=lambda obj: obj.x + 10) + y = lima.fields.Integer() + z = lima.fields.Integer() + + +class ComplexM(lima.Schema): + foo = lima.fields.String() + bar = lima.fields.Integer(get=lambda obj: obj.bar()) + sub = lima.fields.Embed(schema=SubM) + subs = lima.fields.Embed(schema=SubM, many=True) + + +schema = ComplexM() +many_scheam = ComplexM(many=True) + + +def serialization_func(obj, many): + if many: + return many_scheam.dump(obj) + else: + return schema.dump(obj) diff --git a/subjects/loli.py b/subjects/loli.py index e230409..1c2e247 100644 --- a/subjects/loli.py +++ b/subjects/loli.py @@ -1,7 +1,7 @@ from collections import namedtuple from lollipop.types import Object, String, Integer, List, FunctionField, MethodField -__name__ = 'Lollipop' +name = 'Lollipop' SubS = namedtuple('SubS', ['w', 'x', 'y', 'z']) ComplexS = namedtuple('ComplexS', ['foo', 'bar', 'sub', 'subs']) diff --git a/subjects/marsh.py b/subjects/marsh.py index b0ccc17..34112ff 100644 --- a/subjects/marsh.py +++ b/subjects/marsh.py @@ -1,6 +1,6 @@ import marshmallow -__name__ = 'Marshmallow' +name = 'Marshmallow' class SubM(marshmallow.Schema): @@ -14,8 +14,8 @@ def get_x(self, obj): class ComplexM(marshmallow.Schema): - foo = marshmallow.fields.Str() bar = marshmallow.fields.Int() + foo = marshmallow.fields.Str() sub = marshmallow.fields.Nested(SubM) subs = marshmallow.fields.Nested(SubM, many=True) diff --git a/subjects/pickle.py b/subjects/pickle.py new file mode 100644 index 0000000..b67a780 --- /dev/null +++ b/subjects/pickle.py @@ -0,0 +1,7 @@ +import pickle + +name = 'Pickle' + + +def serialization_func(obj, many): + return pickle.dumps(obj) diff --git a/subjects/rf.py b/subjects/rf.py index 19027d0..5c36cbc 100644 --- a/subjects/rf.py +++ b/subjects/rf.py @@ -6,11 +6,11 @@ from rest_framework import serializers as rf_serializers -__name__ = 'Django REST Framework' +name = 'Django REST Framework' class SubRF(rf_serializers.Serializer): - w = rf_serializers.FloatField() + w = rf_serializers.IntegerField() x = rf_serializers.SerializerMethodField() y = rf_serializers.CharField() z = rf_serializers.IntegerField() diff --git a/subjects/serp.py b/subjects/serp.py index 3aa60b0..28f5668 100644 --- a/subjects/serp.py +++ b/subjects/serp.py @@ -1,6 +1,6 @@ import serpy -__name__ = 'serpy' +name = 'serpy' class SubS(serpy.Serializer): diff --git a/subjects/serpy.py b/subjects/serpy.py new file mode 100644 index 0000000..66511f9 --- /dev/null +++ b/subjects/serpy.py @@ -0,0 +1,35 @@ +import dataclasses +import typing + +import serpyco + +from data import ParentTestObject + +name = "serpyco" + + +def get_x(obj): + return obj.x + 10 + + +@dataclasses.dataclass +class SubM: + w: int + y: str + z: int + x: int = serpyco.field(getter=get_x) + + +@dataclasses.dataclass +class ComplexM: + foo: str + sub: SubM + subs: typing.List[SubM] + bar: int = serpyco.field(getter=ParentTestObject.bar) + + +serializer = serpyco.Serializer(ComplexM) + + +def serialization_func(obj, many): + return serializer.dump(obj, many=many) diff --git a/subjects/strain.py b/subjects/strain.py index 2f48f73..922799a 100644 --- a/subjects/strain.py +++ b/subjects/strain.py @@ -1,17 +1,17 @@ import strainer -__name__ = 'Strainer' +name = 'Strainer' -sub_strainer_serializer = strainer.create_serializer( +sub_strainer_serializer = strainer.serializer( strainer.field('w'), - strainer.field('x', to_representation=lambda obj: obj.x + 10), + strainer.field('x', attr_getter=lambda obj: obj.x + 10), strainer.field('y'), strainer.field('z'), ) -complex_strainer_serializer = strainer.create_serializer( +complex_strainer_serializer = strainer.serializer( strainer.field('foo'), - strainer.field('bar', to_representation=lambda obj: obj.bar()), + strainer.field('bar', attr_getter=lambda obj: obj.bar()), strainer.child('sub', serializer=sub_strainer_serializer), strainer.many('subs', serializer=sub_strainer_serializer), ) @@ -19,6 +19,6 @@ def serialization_func(obj, many): if many: - return [complex_strainer_serializer.to_representation(x) for x in obj] + return [complex_strainer_serializer.serialize(x) for x in obj] else: - return complex_strainer_serializer.to_representation(obj) + return complex_strainer_serializer.serialize(obj) diff --git a/subjects/tmarsh.py b/subjects/tmarsh.py new file mode 100644 index 0000000..39d44cd --- /dev/null +++ b/subjects/tmarsh.py @@ -0,0 +1,29 @@ +import toastedmarshmallow +import marshmallow + +name = 'Toasted Marshmallow' + + +class SubM(marshmallow.Schema): + w = marshmallow.fields.Int() + x = marshmallow.fields.Method('get_x') + y = marshmallow.fields.Str() + z = marshmallow.fields.Int() + + def get_x(self, obj): + return obj.x + 10 + + +class ComplexM(marshmallow.Schema): + foo = marshmallow.fields.Str() + bar = marshmallow.fields.Int() + sub = marshmallow.fields.Nested(SubM) + subs = marshmallow.fields.Nested(SubM, many=True) + + +schema = ComplexM() +schema.jit = toastedmarshmallow.Jit + + +def serialization_func(obj, many): + return schema.dump(obj, many=many).data diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 64eaa83..1b8016a 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -1,6 +1,6 @@ -from subjects import marsh, rf, serp, strain, hand, loli +from subjects import rf, serp, strain, col, hand, loli, k, lim, tmarsh from data import ParentTestObject - +import pprint TARGET = { 'foo': 'bar', 'bar': 5, @@ -32,18 +32,22 @@ def test_serializers(): test_object = ParentTestObject() - for subject in (rf, marsh, serp, strain, hand, loli): - print subject.__name__ + for subject in (rf, tmarsh, serp, strain, col, hand, loli, k, lim): + print(subject.__name__) data = subject.serialization_func(test_object, False) - assert data['foo'] == TARGET['foo'] - assert data['bar'] == TARGET['bar'] - assert data['sub']['w'] == TARGET['sub']['w'] - assert data['subs'][3]['y'] == TARGET['subs'][3]['y'] - assert data['subs'][3]['x'] == TARGET['subs'][3]['x'] + pprint.pprint(data) + assert str(data['foo']) == str(TARGET['foo']) + assert str(data['bar']) == str(TARGET['bar']) + assert str(data['sub']['w']) == str(TARGET['sub']['w']) + assert str(data['subs'][3]['y']) == str(TARGET['subs'][3]['y']) + assert str(data['subs'][3]['x']) == str(TARGET['subs'][3]['x']) datas = subject.serialization_func([test_object, test_object], True) for data in datas: - assert data['foo'] == TARGET['foo'] - assert data['sub']['w'] == TARGET['sub']['w'] - assert data['subs'][3]['y'] == TARGET['subs'][3]['y'] - assert data['subs'][3]['x'] == TARGET['subs'][3]['x'] + assert str(data['foo']) == str(TARGET['foo']) + assert str(data['sub']['w']) == str(TARGET['sub']['w']) + assert str(data['subs'][3]['y']) == str(TARGET['subs'][3]['y']) + assert str(data['subs'][3]['x']) == str(TARGET['subs'][3]['x']) + +if __name__ == '__main__': + test_serializers()