Loading README.md +1 −2 Original line number Diff line number Diff line Loading @@ -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` Loading @@ -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 Loading dict_deserializer/deserializer.py +35 −9 Original line number Diff line number Diff line Loading @@ -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): """ Loading @@ -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(): Loading Loading @@ -104,6 +108,7 @@ class Deserializable(metaclass=DeserializableMeta): """ Base class for all automagically deserializing classes. """ @classmethod def get_attrs(cls) -> Dict[str, Rule]: """ Loading Loading @@ -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: Loading @@ -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): Loading @@ -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.') Loading @@ -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, Loading @@ -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)) test_DirectoryExample.py +44 −1 Original line number Diff line number Diff line 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 Loading Loading @@ -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', Loading Loading @@ -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" ] ] }) Loading
README.md +1 −2 Original line number Diff line number Diff line Loading @@ -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` Loading @@ -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 Loading
dict_deserializer/deserializer.py +35 −9 Original line number Diff line number Diff line Loading @@ -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): """ Loading @@ -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(): Loading Loading @@ -104,6 +108,7 @@ class Deserializable(metaclass=DeserializableMeta): """ Base class for all automagically deserializing classes. """ @classmethod def get_attrs(cls) -> Dict[str, Rule]: """ Loading Loading @@ -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: Loading @@ -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): Loading @@ -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.') Loading @@ -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, Loading @@ -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))
test_DirectoryExample.py +44 −1 Original line number Diff line number Diff line 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 Loading Loading @@ -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', Loading Loading @@ -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" ] ] })