Loading dict_deserializer/annotations.py +3 −2 Original line number Diff line number Diff line Loading @@ -15,7 +15,8 @@ class KeyValueDiscriminator: def __repr__(self): if self.has_value: return 'KeyValueDiscriminator(key={}, value={})'.format(self.key, self.value) return 'KeyValueDiscriminator(key={}, value={})'\ .format(self.key, self.value) return 'KeyValueDiscriminator(key={})'.format(self.key) def check(self, d: dict): Loading dict_deserializer/deserializer.py +97 −19 Original line number Diff line number Diff line from typing import Optional, Union, List from typing import Optional, Union, List, Tuple, Dict from typeguard import check_type class Rule: """ This class is primarily used as a container to store type information. """ @staticmethod def to_rule(tpe): def to_rule(tpe) -> 'Rule': """ Ensures type is a rule. Otherwise, it will be converted into a rule. :param tpe: The type/rule. :return: a Rule """ if isinstance(tpe, Rule): return tpe return Rule(tpe) def __init__(self, type, default=None): # noinspection PyShadowingBuiltins def __init__(self: 'Rule', type, default=None): self.type = type self.default = default def __repr__(self): def __repr__(self: 'Rule'): return "Rule(type={}, default={})".format(self.type, self.default) def validate(self, key, value): def validate(self: 'Rule', key: str, value): """ Returns the original value, a default value, or throws. :param key: The key of this field. :param value: Which value to validate. :return: value, default. """ check_type(key, value, self.type) if value is None: value = self.default Loading @@ -26,7 +43,13 @@ class Rule: class BaseMeta(type): def __new__(mcs, name, bases, namespace): """ Metaclass for all Deserializable """ def __new__( mcs: 'BaseMeta', name: str, bases: Tuple[type], namespace: dict)\ -> type: namespace['_discriminators'] = [] namespace['_abstract'] = False Loading @@ -44,7 +67,10 @@ class BaseMeta(type): return cls def rbase(cls, ls=None): def rbase(cls: type, ls: List[type]=None) -> List[type]: """ Get all base classes for cls. """ if ls is None: ls = [] Loading @@ -56,14 +82,31 @@ def rbase(cls, ls=None): return ls def _is_valid(key: str, value): return not key.startswith('_') and not callable(value) and not isinstance(value, classmethod) and\ not isinstance(value, staticmethod) and not isinstance(value, property) def _is_valid(key: str, value) -> bool: """ Value is not a method and key does not start with an underscore. :param key: The name of the field :param value: The value of the field :return: Boolean. """ return not key.startswith('_') and \ not callable(value) and \ not isinstance(value, classmethod) and\ not isinstance(value, staticmethod) and \ not isinstance(value, property) class Deserializable(metaclass=BaseMeta): """ Base class for all automagically deserializing classes. """ @classmethod def get_attrs(cls): def get_attrs(cls) -> Dict[str, Rule]: """ Returns a list of all type rules for the given class. :return: a dict from property to type rule. """ fields = {} defaults = {} rl = list(reversed(rbase(cls))) Loading @@ -72,7 +115,8 @@ class Deserializable(metaclass=BaseMeta): for k in c.__dict__: if _is_valid(k, c.__dict__[k]): defaults[k] = c.__dict__[k] fields[k] = Rule(Optional[type(defaults[k])], default=defaults[k]) fields[k] = Rule(Optional[type(defaults[k])], default=defaults[k]) for k in cls.__annotations__: if k in defaults and not _is_valid(k, defaults[k]): Loading @@ -87,9 +131,18 @@ class Deserializable(metaclass=BaseMeta): def get_deserialization_classes(t, d, try_all=True) -> List[type]: """ Find all candidates that are a (sub)type of t, matching d. :param t: The type to match from. :param d: The dict to match onto. :param try_all: Whether to support automatic discrimination. :return: """ candidates = [] for sc in t.__subclasses__(): if hasattr(sc, '_discriminators'): # noinspection PyProtectedMember for discriminator in sc._discriminators: if not discriminator.check(d): # Invalid Loading @@ -97,7 +150,8 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]: else: # All were valid try: candidates.extend(get_deserialization_classes(sc, t, try_all)) candidates.extend( get_deserialization_classes(sc, t, try_all)) except TypeError as e: if not try_all: raise e Loading @@ -107,7 +161,18 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]: return candidates def deserialize(rule: Rule, data, try_all=True, key='$'): def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'): """ Converts the passed in data into a type that is compatible with rule. :param rule: :param data: :param try_all: Whether to attempt other subtypes when a TypeError has occurred. This is useful when automatically deriving discriminators. :param key: Used for exceptions and error reporting. Preferrably the full path to the current value. :return: An instance matching Rule. """ # In case of primitive types, attempt to assign. try: return rule.validate(key, data) Loading @@ -123,7 +188,8 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): return v except TypeError: pass raise TypeError('{} did not match any of {} for key {}.'.format(type(data), rule.type.__args__, key)) raise TypeError('{} did not match any of {} for key {}.' .format(type(data), rule.type.__args__, key)) if type(rule.type) is type(List): if len(rule.type.__args__) != 1: Loading @@ -135,7 +201,12 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): t = rule.type.__args__[0] result = [] for i, v in enumerate(data): result.append(deserialize(Rule(t), v, try_all, '{}.{}'.format(key, i))) result.append(deserialize( Rule(t), v, try_all, '{}.{}'.format(key, i) )) return result if issubclass(rule.type, Deserializable): Loading @@ -148,7 +219,12 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): 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='{}.{}'.format(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 Loading @@ -156,6 +232,8 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): 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 matching non-abstract (sub)type of ' '{} with key {}.'.format(rule.type, key)) raise TypeError('Unable to find a deserialization candidate for {} with key {}.'.format(rule, key)) No newline at end of file raise TypeError('Unable to find a deserialization candidate for ' '{} with key {}.'.format(rule, key)) Loading
dict_deserializer/annotations.py +3 −2 Original line number Diff line number Diff line Loading @@ -15,7 +15,8 @@ class KeyValueDiscriminator: def __repr__(self): if self.has_value: return 'KeyValueDiscriminator(key={}, value={})'.format(self.key, self.value) return 'KeyValueDiscriminator(key={}, value={})'\ .format(self.key, self.value) return 'KeyValueDiscriminator(key={})'.format(self.key) def check(self, d: dict): Loading
dict_deserializer/deserializer.py +97 −19 Original line number Diff line number Diff line from typing import Optional, Union, List from typing import Optional, Union, List, Tuple, Dict from typeguard import check_type class Rule: """ This class is primarily used as a container to store type information. """ @staticmethod def to_rule(tpe): def to_rule(tpe) -> 'Rule': """ Ensures type is a rule. Otherwise, it will be converted into a rule. :param tpe: The type/rule. :return: a Rule """ if isinstance(tpe, Rule): return tpe return Rule(tpe) def __init__(self, type, default=None): # noinspection PyShadowingBuiltins def __init__(self: 'Rule', type, default=None): self.type = type self.default = default def __repr__(self): def __repr__(self: 'Rule'): return "Rule(type={}, default={})".format(self.type, self.default) def validate(self, key, value): def validate(self: 'Rule', key: str, value): """ Returns the original value, a default value, or throws. :param key: The key of this field. :param value: Which value to validate. :return: value, default. """ check_type(key, value, self.type) if value is None: value = self.default Loading @@ -26,7 +43,13 @@ class Rule: class BaseMeta(type): def __new__(mcs, name, bases, namespace): """ Metaclass for all Deserializable """ def __new__( mcs: 'BaseMeta', name: str, bases: Tuple[type], namespace: dict)\ -> type: namespace['_discriminators'] = [] namespace['_abstract'] = False Loading @@ -44,7 +67,10 @@ class BaseMeta(type): return cls def rbase(cls, ls=None): def rbase(cls: type, ls: List[type]=None) -> List[type]: """ Get all base classes for cls. """ if ls is None: ls = [] Loading @@ -56,14 +82,31 @@ def rbase(cls, ls=None): return ls def _is_valid(key: str, value): return not key.startswith('_') and not callable(value) and not isinstance(value, classmethod) and\ not isinstance(value, staticmethod) and not isinstance(value, property) def _is_valid(key: str, value) -> bool: """ Value is not a method and key does not start with an underscore. :param key: The name of the field :param value: The value of the field :return: Boolean. """ return not key.startswith('_') and \ not callable(value) and \ not isinstance(value, classmethod) and\ not isinstance(value, staticmethod) and \ not isinstance(value, property) class Deserializable(metaclass=BaseMeta): """ Base class for all automagically deserializing classes. """ @classmethod def get_attrs(cls): def get_attrs(cls) -> Dict[str, Rule]: """ Returns a list of all type rules for the given class. :return: a dict from property to type rule. """ fields = {} defaults = {} rl = list(reversed(rbase(cls))) Loading @@ -72,7 +115,8 @@ class Deserializable(metaclass=BaseMeta): for k in c.__dict__: if _is_valid(k, c.__dict__[k]): defaults[k] = c.__dict__[k] fields[k] = Rule(Optional[type(defaults[k])], default=defaults[k]) fields[k] = Rule(Optional[type(defaults[k])], default=defaults[k]) for k in cls.__annotations__: if k in defaults and not _is_valid(k, defaults[k]): Loading @@ -87,9 +131,18 @@ class Deserializable(metaclass=BaseMeta): def get_deserialization_classes(t, d, try_all=True) -> List[type]: """ Find all candidates that are a (sub)type of t, matching d. :param t: The type to match from. :param d: The dict to match onto. :param try_all: Whether to support automatic discrimination. :return: """ candidates = [] for sc in t.__subclasses__(): if hasattr(sc, '_discriminators'): # noinspection PyProtectedMember for discriminator in sc._discriminators: if not discriminator.check(d): # Invalid Loading @@ -97,7 +150,8 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]: else: # All were valid try: candidates.extend(get_deserialization_classes(sc, t, try_all)) candidates.extend( get_deserialization_classes(sc, t, try_all)) except TypeError as e: if not try_all: raise e Loading @@ -107,7 +161,18 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]: return candidates def deserialize(rule: Rule, data, try_all=True, key='$'): def deserialize(rule: Rule, data, try_all: bool=True, key: str='$'): """ Converts the passed in data into a type that is compatible with rule. :param rule: :param data: :param try_all: Whether to attempt other subtypes when a TypeError has occurred. This is useful when automatically deriving discriminators. :param key: Used for exceptions and error reporting. Preferrably the full path to the current value. :return: An instance matching Rule. """ # In case of primitive types, attempt to assign. try: return rule.validate(key, data) Loading @@ -123,7 +188,8 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): return v except TypeError: pass raise TypeError('{} did not match any of {} for key {}.'.format(type(data), rule.type.__args__, key)) raise TypeError('{} did not match any of {} for key {}.' .format(type(data), rule.type.__args__, key)) if type(rule.type) is type(List): if len(rule.type.__args__) != 1: Loading @@ -135,7 +201,12 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): t = rule.type.__args__[0] result = [] for i, v in enumerate(data): result.append(deserialize(Rule(t), v, try_all, '{}.{}'.format(key, i))) result.append(deserialize( Rule(t), v, try_all, '{}.{}'.format(key, i) )) return result if issubclass(rule.type, Deserializable): Loading @@ -148,7 +219,12 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): 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='{}.{}'.format(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 Loading @@ -156,6 +232,8 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): 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 matching non-abstract (sub)type of ' '{} with key {}.'.format(rule.type, key)) raise TypeError('Unable to find a deserialization candidate for {} with key {}.'.format(rule, key)) No newline at end of file raise TypeError('Unable to find a deserialization candidate for ' '{} with key {}.'.format(rule, key))