Skip to content

Commit

Permalink
tests: add json serialisatio format
Browse files Browse the repository at this point in the history
  • Loading branch information
tomjnixon committed Aug 30, 2023
1 parent a07e50a commit 099d99e
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 0 deletions.
111 changes: 111 additions & 0 deletions ear/test/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import attrs
import ear.common
import ear.core.metadata_input
import ear.fileio.adm.elements
from fractions import Fraction


def value_to_json(value, include_defaults=True):
"""turn EAR types into JSON
This is not complete but will be extended as needed.
The result can be turned back into objects using json_to_value
Parameters:
include_defaults (bool): add default values (True), or skip them (False)
Returns:
a json-serialisable object, composed of dict, list, bool, int, float,
str or None
objects are represented as a dictionary containing the keyword
arguments for the constructor, and a _type key naming the type
"""

def recurse(v):
return value_to_json(v, include_defaults=include_defaults)

if value is None:
return None
elif isinstance(value, (bool, int, float, str)):
return value
elif isinstance(value, list):
return [recurse(v) for v in value]
elif isinstance(value, dict):
return {k: recurse(v) for k, v in value.items()}
elif attrs.has(type(value)):
d = dict(_type=type(value).__name__)

for field in attrs.fields(type(value)):
v = getattr(value, field.name)

if include_defaults or not _field_is_default(field, v):
d[field.name] = recurse(v)
return d
# types that need special handling; make this generic if we need to add more
elif isinstance(value, Fraction):
return dict(
_type="Fraction", numerator=value.numerator, denominator=value.denominator
)
else:
assert False, "unknown type"


def json_to_value(value):
"""turn the results of value_to_json back into objects"""
if isinstance(value, dict):
converted = {k: json_to_value(v) for k, v in value.items()}
if "_type" in value:
t = _known_types[value["_type"]]
del converted["_type"]
return t(**converted)
else:
return converted
if isinstance(value, list):
return [json_to_value(v) for v in value]
else:
return value


def _field_is_default(field: attrs.Attribute, value):
"""does an attrs field have the default value?
this depends on values being properly equality-comparable
"""
default = field.default
if default is attrs.NOTHING:
return False

if isinstance(default, attrs.Factory):
assert not default.takes_self, "not implemented"

default = default.factory()

return type(value) is type(default) and value == default


def _get_known_types():
known_types = [
ear.common.CartesianPosition,
ear.common.CartesianScreen,
ear.common.PolarPosition,
ear.common.PolarScreen,
ear.core.metadata_input.ExtraData,
ear.core.metadata_input.ObjectTypeMetadata,
ear.fileio.adm.elements.AudioBlockFormatObjects,
ear.fileio.adm.elements.CartesianZone,
ear.fileio.adm.elements.ChannelLock,
ear.fileio.adm.elements.Frequency,
ear.fileio.adm.elements.JumpPosition,
ear.fileio.adm.elements.ObjectCartesianPosition,
ear.fileio.adm.elements.ObjectDivergence,
ear.fileio.adm.elements.ObjectPolarPosition,
ear.fileio.adm.elements.PolarZone,
ear.fileio.adm.elements.ScreenEdgeLock,
Fraction,
]

return {t.__name__: t for t in known_types}


_known_types = _get_known_types()
55 changes: 55 additions & 0 deletions ear/test/test_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import pytest
from .json import json_to_value, value_to_json
from ear.core.metadata_input import AudioBlockFormatObjects, ObjectTypeMetadata
from fractions import Fraction


def _check_json_subset(d1, d2):
"""check that dictionaries in d1 are a subset of d2, and that all values
are representable in json"""

def is_type(t):
return isinstance(d1, t) and isinstance(d2, t)

if is_type(dict):
for key in d1:
assert key in d2
_check_json_subset(d1[key], d2[key])
elif is_type(list):
assert len(d1) == len(d2)
for v1, v2 in zip(d1, d2):
_check_json_subset(v1, v2)
elif is_type(bool) or is_type(int) or is_type(float) or is_type(str):
assert d1 == d2
elif d1 is None and d2 is None:
pass
else:
assert False, "mismatching or unknown types"


@pytest.mark.parametrize("include_defaults", [False, True])
def test_json_OTM(include_defaults):
otm = ObjectTypeMetadata(
block_format=AudioBlockFormatObjects(
rtime=Fraction(1, 2), position=dict(azimuth=0, elevation=0)
)
)
otm_j = value_to_json(otm, include_defaults=include_defaults)
expected = {
"_type": "ObjectTypeMetadata",
"block_format": {
"_type": "AudioBlockFormatObjects",
"position": {
"_type": "ObjectPolarPosition",
"azimuth": 0.0,
"elevation": 0.0,
},
"rtime": {"_type": "Fraction", "denominator": 2, "numerator": 1},
},
}

_check_json_subset(expected, otm_j)

otm_j_otm = json_to_value(otm_j)

assert otm_j_otm == otm

0 comments on commit 099d99e

Please sign in to comment.