Verified Commit 42e275a9 authored by Rolf van Kleef's avatar Rolf van Kleef
Browse files

Added support for tuples

parent 22c854dc
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -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
+35 −9
Original line number Diff line number Diff line
@@ -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))
+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
@@ -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"
                    ]
                ]
            })