Skip to content
Commits on Source (3)
......@@ -119,6 +119,7 @@ support some of its types. This is a list of verified composite types:
* `Union` (Including `Optional`)
* `List`
* `Tuple`
* `Any`
* `dict_deserializer.deserializer.Deserializable`
* `dict`
......@@ -134,8 +135,6 @@ It supports these types as terminal types:
## Planned features
* Tuples
* Lists will probably deserialize into tuples
* NamedTuples
* The anonymous namedtuple and the class-namedtuples with (optionally) type annotations.
* Dataclasses
......
from collections import namedtuple
name = 'Dictionary deserializer'
version = '0.0.4'
version = '0.0.5'
description = "Dictionary deserializer is a package that aides in the " \
"deserializing of JSON (or other structures) that are " \
"converted to dicts, into composite classes."
......
......@@ -26,7 +26,9 @@ class Rule:
self.default = default
def __repr__(self: 'Rule'):
if self.default:
return "Rule(type={}, default={})".format(self.type, self.default)
return "Rule(type={})".format(self.type)
def validate(self: 'Rule', key: str, value):
"""
......@@ -46,8 +48,10 @@ class DeserializableMeta(type):
"""
Metaclass for all Deserializable
"""
def __new__(
mcs: 'DeserializableMeta', name: str, bases: Tuple[type], namespace: dict)\
mcs: 'DeserializableMeta', name: str, bases: Tuple[type],
namespace: dict) \
-> type:
def auto_ctor(self, **kwargs):
for k, v in type(self).get_attrs().items():
......@@ -104,6 +108,7 @@ class Deserializable(metaclass=DeserializableMeta):
"""
Base class for all automagically deserializing classes.
"""
@classmethod
def get_attrs(cls) -> Dict[str, Rule]:
"""
......@@ -179,12 +184,13 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'):
path to the current value.
:return: An instance matching Rule.
"""
# In case of primitive types, attempt to assign.
# Deserialize primitives
try:
return rule.validate(key, data)
except TypeError:
pass
# Deserialize type unions
if type(rule.type) is type(Union):
for arg in rule.type.__args__:
try:
......@@ -197,13 +203,15 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'):
raise TypeError('{} did not match any of {} for key {}.'
.format(type(data), rule.type.__args__, key))
# Deserialize lists
if type(rule.type) is type(List):
if len(rule.type.__args__) != 1:
raise TypeError(
'Cannot handle list with 0 or more than 1 type arguments.')
'Cannot handle list with 0 or more than 1 type arguments at {}.'
.format(key))
if type(data) != list:
raise TypeError(
'Cannot deserialize non-list into list.')
'Cannot deserialize non-list into list at {}.'.format(key))
t = rule.type.__args__[0]
result = []
for i, v in enumerate(data):
......@@ -215,6 +223,17 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'):
))
return result
# Deserialize tuples
if type(rule.type) is type(Tuple):
if len(rule.type.__args__) != len(data):
raise TypeError(
'Expected a list of {} elements, but got {} elements at {}.'
.format(len(rule.type.__args__), len(data), key))
return tuple(deserialize(Rule(v[0]), v[1], key="{}.{}".format(key, k))
for k, v in enumerate(zip(rule.type.__args__, data)))
# Deserialize classes
if issubclass(rule.type, Deserializable):
if not isinstance(data, dict):
raise TypeError('Cannot deserialize non-dict into class.')
......@@ -225,6 +244,12 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'):
for cls in classes:
try:
# Instantiate cls with parameters generated by
# the list comprehension.
# It loops through all defined attributes, and
# defines it by recursively calling deserialize on
# each of those attributes with the values found in
# either data, or by using a default.
return cls(**{k: deserialize(
r,
data[k] if k in data else r.default,
......@@ -238,7 +263,8 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'):
cause = e
raise TypeError('Unable to find matching non-abstract (sub)type of '
'{} with key {}. Reason: {}.'.format(rule.type, key, cause))
'{} with key {}. Reason: {}.'.format(rule.type, key,
cause))
raise TypeError('Unable to find a deserialization candidate for '
'{} with key {}.'.format(rule, key))
......@@ -18,7 +18,7 @@ setuptools.setup(
description=dict_deserializer.description,
long_description=long_description,
long_description_content_type="text/markdown",
url="https://git.iapc.utwente.nl/rkleef/serializer_utils",
url="https://github.com/rhbvkleef/dict_deserializer",
packages=setuptools.find_packages(),
install_requires=requirements,
classifiers=[
......
import unittest
from typing import List, Optional
from typing import List, Optional, Tuple
from dict_deserializer.annotations import abstract
from dict_deserializer.deserializer import Deserializable, deserialize, Rule
......@@ -47,8 +47,19 @@ class Group(Object):
and other.members == self.members
class NestedTypingStuff(Deserializable):
test: List[Tuple[int, int, int]]
def __str__(self):
return "NestedTypingStuff(test={})".format(self.test)
def __eq__(self, other):
return isinstance(other, NestedTypingStuff) and self.test == other.test
class TestLists(unittest.TestCase):
def test_CorrectDeserializationForNestedWithTypeUnionsAndLists(self):
# noinspection PyArgumentList
self.assertEqual(
Group(
name='IAPC',
......@@ -89,3 +100,35 @@ class TestLists(unittest.TestCase):
deserialize(Rule(Object), {
'name': 'Test'
})
def test_DeserializeTupleCorrect(self):
# noinspection PyArgumentList
self.assertEqual(
NestedTypingStuff(test=[(1, 2, 3)]),
deserialize(Rule(NestedTypingStuff), {
"test": [
[
1, 2, 3
]
]
})
)
def test_DeserializeTupleFail(self):
with self.assertRaises(TypeError) as ctx:
deserialize(Rule(NestedTypingStuff), {
"test": [
[
1, 2
]
]
})
with self.assertRaises(TypeError) as ctx:
deserialize(Rule(NestedTypingStuff), {
"test": [
[
1, 2, "boo"
]
]
})