Loading dict_deserializer/deserializer.py +28 −15 Original line number Diff line number Diff line Loading @@ -86,22 +86,28 @@ class Deserializable(metaclass=BaseMeta): return fields def get_deserialization_class(t, d, try_all=False): def get_deserialization_classes(t, d, try_all=True) -> List[type]: candidates = [] for sc in t.__subclasses__(): if hasattr(sc, '_discriminators'): for discriminator in sc._discriminators: if not discriminator.check(d): # Invalid break else: # All were valid try: return get_deserialization_class(sc, t, try_all) candidates.extend(get_deserialization_classes(sc, t, try_all)) except TypeError as e: if not try_all: raise e return t if not getattr(t, '_abstract', True): candidates.append(t) return candidates def deserialize(rule: Rule, data, try_all=False, key=None): def deserialize(rule: Rule, data, try_all=True, key='$'): # In case of primitive types, attempt to assign. try: return rule.validate(key, data) Loading @@ -111,7 +117,10 @@ def deserialize(rule: Rule, data, try_all=False, key=None): if type(rule.type) is type(Union): for arg in rule.type.__args__: try: return deserialize(Rule(arg), data, try_all, key) v = deserialize(Rule(arg), data, try_all, key) if v is None: v = rule.default return v except TypeError: pass raise TypeError('{} did not match any of {} for key {}.'.format(type(data), rule.type.__args__, key)) Loading @@ -126,23 +135,27 @@ def deserialize(rule: Rule, data, try_all=False, key=None): t = rule.type.__args__[0] result = [] for i, v in enumerate(data): result.append(deserialize(Rule(t), v, try_all, i)) result.append(deserialize(Rule(t), v, try_all, '{}.{}'.format(key, i))) return result if issubclass(rule.type, Deserializable): if not isinstance(data, dict): raise TypeError('Cannot deserialize non-dict into class.') cls = get_deserialization_class(rule.type, data, try_all) if hasattr(cls, '_abstract') and cls._abstract: raise TypeError('Cannot deserialize into {}: is abstract.'.format(cls.__name__)) classes = get_deserialization_classes(rule.type, data, try_all) for cls in classes: try: instance = cls() for k, r in cls.get_attrs().items(): v = deserialize(r, data[k] if k in data else r.default, try_all, key=k) v = deserialize(r, data[k] if k in data else r.default, try_all, key='{}.{}'.format(key, k)) setattr(instance, k, v) return instance except TypeError as e: if not try_all: raise e raise TypeError('Unable to find matching non-abstract (sub)type of {} with key {}.'.format(rule.type, key)) raise TypeError('Unable to find a deserialization candidate for {} in {}.'.format(data, rule)) No newline at end of file raise TypeError('Unable to find a deserialization candidate for {} with key {}.'.format(rule, key)) No newline at end of file test_DirectoryExample.py 0 → 100644 +103 −0 Original line number Diff line number Diff line import unittest from typing import List, Optional from dict_deserializer.annotations import abstract from dict_deserializer.deserializer import Deserializable, deserialize, Rule @abstract class Object(Deserializable): def __init__(self, name=None): self.name = name name: str def __repr__(self): return 'Object(name="{}")'.format(self.name) def __eq__(self, other): return isinstance(other, Object) and other.name == self.name class User(Object): def __init__(self, full_name=None, calling_name=None, *args, **kwargs): super(User, self).__init__(*args, **kwargs) self.full_name = full_name self.calling_name = calling_name full_name: str calling_name: Optional[str] = 'Unknown' def __repr__(self): if self.calling_name is None: return 'User(super={}, full_name="{}")'\ .format(super(User, self).__repr__(), self.full_name) return 'User(super={}, full_name="{}", calling_name="{}")'\ .format(super(User, self).__repr__(), self.full_name, self.calling_name) def __eq__(self, other): return isinstance(other, User) and super(User, self).__eq__(other) and\ other.full_name == self.full_name and\ other.calling_name == self.calling_name class Group(Object): def __init__(self, members=None, *args, **kwargs): super(Group, self).__init__(*args, **kwargs) self.members = members members: List[Object] def __repr__(self): return 'Group(super={}, members=[{}])'\ .format(super(Group, self).__repr__(), ','.join([m.__repr__() for m in self.members])) def __eq__(self, other): return isinstance(other, Group) and super(Group, self).__eq__(other)\ and other.members == self.members class TestLists(unittest.TestCase): def test_CorrectDeserializationForNestedWithTypeUnionsAndLists(self): self.assertEqual( Group( name='IAPC', members=[ User(name='Rolf', full_name='Rolf van Kleef', calling_name='Unknown'), Group(name='Syscom', members=[ User(name='Kevin', full_name='Kevin Alberts', calling_name='Kevin'), ]), ], ), deserialize(Rule(Object), { 'name': 'IAPC', 'members': [ {'name': 'Rolf', 'full_name': 'Rolf van Kleef'}, {'name': 'Syscom', 'members': [ {'name': 'Kevin', 'full_name': 'Kevin Alberts', 'calling_name': 'Kevin'}, ]}, ], }) ) def test_FailDeserializeWithInvalidTypes(self): with self.assertRaises(TypeError): deserialize(Rule(Object), { 'name': 'Karel', 'full_name': 0.0, }) with self.assertRaises(TypeError): deserialize(Rule(Object), { 'name': 'Rolf', 'full_name': 'Rolf van Kleef', 'calling_name': False, }) def test_DeserializeIntoAbstract(self): with self.assertRaises(TypeError) as ctx: deserialize(Rule(Object), { 'name': 'Test' }) Loading
dict_deserializer/deserializer.py +28 −15 Original line number Diff line number Diff line Loading @@ -86,22 +86,28 @@ class Deserializable(metaclass=BaseMeta): return fields def get_deserialization_class(t, d, try_all=False): def get_deserialization_classes(t, d, try_all=True) -> List[type]: candidates = [] for sc in t.__subclasses__(): if hasattr(sc, '_discriminators'): for discriminator in sc._discriminators: if not discriminator.check(d): # Invalid break else: # All were valid try: return get_deserialization_class(sc, t, try_all) candidates.extend(get_deserialization_classes(sc, t, try_all)) except TypeError as e: if not try_all: raise e return t if not getattr(t, '_abstract', True): candidates.append(t) return candidates def deserialize(rule: Rule, data, try_all=False, key=None): def deserialize(rule: Rule, data, try_all=True, key='$'): # In case of primitive types, attempt to assign. try: return rule.validate(key, data) Loading @@ -111,7 +117,10 @@ def deserialize(rule: Rule, data, try_all=False, key=None): if type(rule.type) is type(Union): for arg in rule.type.__args__: try: return deserialize(Rule(arg), data, try_all, key) v = deserialize(Rule(arg), data, try_all, key) if v is None: v = rule.default return v except TypeError: pass raise TypeError('{} did not match any of {} for key {}.'.format(type(data), rule.type.__args__, key)) Loading @@ -126,23 +135,27 @@ def deserialize(rule: Rule, data, try_all=False, key=None): t = rule.type.__args__[0] result = [] for i, v in enumerate(data): result.append(deserialize(Rule(t), v, try_all, i)) result.append(deserialize(Rule(t), v, try_all, '{}.{}'.format(key, i))) return result if issubclass(rule.type, Deserializable): if not isinstance(data, dict): raise TypeError('Cannot deserialize non-dict into class.') cls = get_deserialization_class(rule.type, data, try_all) if hasattr(cls, '_abstract') and cls._abstract: raise TypeError('Cannot deserialize into {}: is abstract.'.format(cls.__name__)) classes = get_deserialization_classes(rule.type, data, try_all) for cls in classes: try: instance = cls() for k, r in cls.get_attrs().items(): v = deserialize(r, data[k] if k in data else r.default, try_all, key=k) v = deserialize(r, data[k] if k in data else r.default, try_all, key='{}.{}'.format(key, k)) setattr(instance, k, v) return instance except TypeError as e: if not try_all: raise e raise TypeError('Unable to find matching non-abstract (sub)type of {} with key {}.'.format(rule.type, key)) raise TypeError('Unable to find a deserialization candidate for {} in {}.'.format(data, rule)) No newline at end of file raise TypeError('Unable to find a deserialization candidate for {} with key {}.'.format(rule, key)) No newline at end of file
test_DirectoryExample.py 0 → 100644 +103 −0 Original line number Diff line number Diff line import unittest from typing import List, Optional from dict_deserializer.annotations import abstract from dict_deserializer.deserializer import Deserializable, deserialize, Rule @abstract class Object(Deserializable): def __init__(self, name=None): self.name = name name: str def __repr__(self): return 'Object(name="{}")'.format(self.name) def __eq__(self, other): return isinstance(other, Object) and other.name == self.name class User(Object): def __init__(self, full_name=None, calling_name=None, *args, **kwargs): super(User, self).__init__(*args, **kwargs) self.full_name = full_name self.calling_name = calling_name full_name: str calling_name: Optional[str] = 'Unknown' def __repr__(self): if self.calling_name is None: return 'User(super={}, full_name="{}")'\ .format(super(User, self).__repr__(), self.full_name) return 'User(super={}, full_name="{}", calling_name="{}")'\ .format(super(User, self).__repr__(), self.full_name, self.calling_name) def __eq__(self, other): return isinstance(other, User) and super(User, self).__eq__(other) and\ other.full_name == self.full_name and\ other.calling_name == self.calling_name class Group(Object): def __init__(self, members=None, *args, **kwargs): super(Group, self).__init__(*args, **kwargs) self.members = members members: List[Object] def __repr__(self): return 'Group(super={}, members=[{}])'\ .format(super(Group, self).__repr__(), ','.join([m.__repr__() for m in self.members])) def __eq__(self, other): return isinstance(other, Group) and super(Group, self).__eq__(other)\ and other.members == self.members class TestLists(unittest.TestCase): def test_CorrectDeserializationForNestedWithTypeUnionsAndLists(self): self.assertEqual( Group( name='IAPC', members=[ User(name='Rolf', full_name='Rolf van Kleef', calling_name='Unknown'), Group(name='Syscom', members=[ User(name='Kevin', full_name='Kevin Alberts', calling_name='Kevin'), ]), ], ), deserialize(Rule(Object), { 'name': 'IAPC', 'members': [ {'name': 'Rolf', 'full_name': 'Rolf van Kleef'}, {'name': 'Syscom', 'members': [ {'name': 'Kevin', 'full_name': 'Kevin Alberts', 'calling_name': 'Kevin'}, ]}, ], }) ) def test_FailDeserializeWithInvalidTypes(self): with self.assertRaises(TypeError): deserialize(Rule(Object), { 'name': 'Karel', 'full_name': 0.0, }) with self.assertRaises(TypeError): deserialize(Rule(Object), { 'name': 'Rolf', 'full_name': 'Rolf van Kleef', 'calling_name': False, }) def test_DeserializeIntoAbstract(self): with self.assertRaises(TypeError) as ctx: deserialize(Rule(Object), { 'name': 'Test' })