Commit 42e275a9 authored by Rolf H. B. van Kleef's avatar Rolf H. B. van Kleef

Added support for tuples

parent 22c854dc
......@@ -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
......
......@@ -26,7 +26,9 @@ class Rule:
self.default = default
def __repr__(self: 'Rule'):
return "Rule(type={}, default={})".format(self.type, self.default)
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():
......@@ -71,7 +75,7 @@ class DeserializableMeta(type):
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.
"""
......@@ -95,7 +99,7 @@ def _is_valid(key: str, value) -> bool:
"""
return not key.startswith('_') and \
not callable(value) and \
not isinstance(value, classmethod) and\
not isinstance(value, classmethod) and \
not isinstance(value, staticmethod) and \
not isinstance(value, property)
......@@ -104,6 +108,7 @@ class Deserializable(metaclass=DeserializableMeta):
"""
Base class for all automagically deserializing classes.
"""
@classmethod
def get_attrs(cls) -> Dict[str, Rule]:
"""
......@@ -167,7 +172,7 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]:
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.
......@@ -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))
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"
]
]
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment