...
 
Commits (3)
...@@ -119,6 +119,7 @@ support some of its types. This is a list of verified composite types: ...@@ -119,6 +119,7 @@ support some of its types. This is a list of verified composite types:
* `Union` (Including `Optional`) * `Union` (Including `Optional`)
* `List` * `List`
* `Tuple`
* `Any` * `Any`
* `dict_deserializer.deserializer.Deserializable` * `dict_deserializer.deserializer.Deserializable`
* `dict` * `dict`
...@@ -134,8 +135,6 @@ It supports these types as terminal types: ...@@ -134,8 +135,6 @@ It supports these types as terminal types:
## Planned features ## Planned features
* Tuples
* Lists will probably deserialize into tuples
* NamedTuples * NamedTuples
* The anonymous namedtuple and the class-namedtuples with (optionally) type annotations. * The anonymous namedtuple and the class-namedtuples with (optionally) type annotations.
* Dataclasses * Dataclasses
......
from collections import namedtuple from collections import namedtuple
name = 'Dictionary deserializer' name = 'Dictionary deserializer'
version = '0.0.4' version = '0.0.5'
description = "Dictionary deserializer is a package that aides in the " \ description = "Dictionary deserializer is a package that aides in the " \
"deserializing of JSON (or other structures) that are " \ "deserializing of JSON (or other structures) that are " \
"converted to dicts, into composite classes." "converted to dicts, into composite classes."
......
...@@ -26,7 +26,9 @@ class Rule: ...@@ -26,7 +26,9 @@ class Rule:
self.default = default self.default = default
def __repr__(self: 'Rule'): def __repr__(self: 'Rule'):
if self.default:
return "Rule(type={}, default={})".format(self.type, self.default) return "Rule(type={}, default={})".format(self.type, self.default)
return "Rule(type={})".format(self.type)
def validate(self: 'Rule', key: str, value): def validate(self: 'Rule', key: str, value):
""" """
...@@ -46,8 +48,10 @@ class DeserializableMeta(type): ...@@ -46,8 +48,10 @@ class DeserializableMeta(type):
""" """
Metaclass for all Deserializable Metaclass for all Deserializable
""" """
def __new__( def __new__(
mcs: 'DeserializableMeta', name: str, bases: Tuple[type], namespace: dict)\ mcs: 'DeserializableMeta', name: str, bases: Tuple[type],
namespace: dict) \
-> type: -> type:
def auto_ctor(self, **kwargs): def auto_ctor(self, **kwargs):
for k, v in type(self).get_attrs().items(): for k, v in type(self).get_attrs().items():
...@@ -71,7 +75,7 @@ class DeserializableMeta(type): ...@@ -71,7 +75,7 @@ class DeserializableMeta(type):
return cls return cls
def _rbase(cls: type, ls: List[type]=None) -> List[type]: def _rbase(cls: type, ls: List[type] = None) -> List[type]:
""" """
Get all base classes for cls. Get all base classes for cls.
""" """
...@@ -95,7 +99,7 @@ def _is_valid(key: str, value) -> bool: ...@@ -95,7 +99,7 @@ def _is_valid(key: str, value) -> bool:
""" """
return not key.startswith('_') and \ return not key.startswith('_') and \
not callable(value) and \ not callable(value) and \
not isinstance(value, classmethod) and\ not isinstance(value, classmethod) and \
not isinstance(value, staticmethod) and \ not isinstance(value, staticmethod) and \
not isinstance(value, property) not isinstance(value, property)
...@@ -104,6 +108,7 @@ class Deserializable(metaclass=DeserializableMeta): ...@@ -104,6 +108,7 @@ class Deserializable(metaclass=DeserializableMeta):
""" """
Base class for all automagically deserializing classes. Base class for all automagically deserializing classes.
""" """
@classmethod @classmethod
def get_attrs(cls) -> Dict[str, Rule]: def get_attrs(cls) -> Dict[str, Rule]:
""" """
...@@ -167,7 +172,7 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]: ...@@ -167,7 +172,7 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]:
return candidates return candidates
def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'): def deserialize(rule: Rule, data, try_all: bool = True, key: str = '$'):
""" """
Converts the passed in data into a type that is compatible with rule. Converts the passed in data into a type that is compatible with rule.
...@@ -179,12 +184,13 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'): ...@@ -179,12 +184,13 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'):
path to the current value. path to the current value.
:return: An instance matching Rule. :return: An instance matching Rule.
""" """
# In case of primitive types, attempt to assign. # Deserialize primitives
try: try:
return rule.validate(key, data) return rule.validate(key, data)
except TypeError: except TypeError:
pass pass
# Deserialize type unions
if type(rule.type) is type(Union): if type(rule.type) is type(Union):
for arg in rule.type.__args__: for arg in rule.type.__args__:
try: try:
...@@ -197,13 +203,15 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'): ...@@ -197,13 +203,15 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'):
raise TypeError('{} did not match any of {} for key {}.' raise TypeError('{} did not match any of {} for key {}.'
.format(type(data), rule.type.__args__, key)) .format(type(data), rule.type.__args__, key))
# Deserialize lists
if type(rule.type) is type(List): if type(rule.type) is type(List):
if len(rule.type.__args__) != 1: if len(rule.type.__args__) != 1:
raise TypeError( 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: if type(data) != list:
raise TypeError( raise TypeError(
'Cannot deserialize non-list into list.') 'Cannot deserialize non-list into list at {}.'.format(key))
t = rule.type.__args__[0] t = rule.type.__args__[0]
result = [] result = []
for i, v in enumerate(data): for i, v in enumerate(data):
...@@ -215,6 +223,17 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'): ...@@ -215,6 +223,17 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'):
)) ))
return result 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 issubclass(rule.type, Deserializable):
if not isinstance(data, dict): if not isinstance(data, dict):
raise TypeError('Cannot deserialize non-dict into class.') raise TypeError('Cannot deserialize non-dict into class.')
...@@ -225,6 +244,12 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'): ...@@ -225,6 +244,12 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'):
for cls in classes: for cls in classes:
try: 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( return cls(**{k: deserialize(
r, r,
data[k] if k in data else r.default, data[k] if k in data else r.default,
...@@ -238,7 +263,8 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'): ...@@ -238,7 +263,8 @@ def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'):
cause = e cause = e
raise TypeError('Unable to find matching non-abstract (sub)type of ' 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 ' raise TypeError('Unable to find a deserialization candidate for '
'{} with key {}.'.format(rule, key)) '{} with key {}.'.format(rule, key))
...@@ -18,7 +18,7 @@ setuptools.setup( ...@@ -18,7 +18,7 @@ setuptools.setup(
description=dict_deserializer.description, description=dict_deserializer.description,
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", 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(), packages=setuptools.find_packages(),
install_requires=requirements, install_requires=requirements,
classifiers=[ classifiers=[
......
import unittest import unittest
from typing import List, Optional from typing import List, Optional, Tuple
from dict_deserializer.annotations import abstract from dict_deserializer.annotations import abstract
from dict_deserializer.deserializer import Deserializable, deserialize, Rule from dict_deserializer.deserializer import Deserializable, deserialize, Rule
...@@ -47,8 +47,19 @@ class Group(Object): ...@@ -47,8 +47,19 @@ class Group(Object):
and other.members == self.members 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): class TestLists(unittest.TestCase):
def test_CorrectDeserializationForNestedWithTypeUnionsAndLists(self): def test_CorrectDeserializationForNestedWithTypeUnionsAndLists(self):
# noinspection PyArgumentList
self.assertEqual( self.assertEqual(
Group( Group(
name='IAPC', name='IAPC',
...@@ -89,3 +100,35 @@ class TestLists(unittest.TestCase): ...@@ -89,3 +100,35 @@ class TestLists(unittest.TestCase):
deserialize(Rule(Object), { deserialize(Rule(Object), {
'name': 'Test' '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"
]
]
})