diff --git a/dict_deserializer/annotations.py b/dict_deserializer/annotations.py index cc87b47ed0403acd1bb9fee1b8180ccb38ebeda5..867331f50cef7be3b9b3218248df2387549c84fd 100644 --- a/dict_deserializer/annotations.py +++ b/dict_deserializer/annotations.py @@ -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): @@ -56,4 +57,4 @@ def discriminate(key=None, value=sentinel, matcher=None): def abstract(cls): cls._abstract = True - return cls \ No newline at end of file + return cls diff --git a/dict_deserializer/deserializer.py b/dict_deserializer/deserializer.py index ab74ece0cc9549342e999d9d6d618d3d7b0878e2..57f658ec02c53eaec2d69c4082642edb0776718f 100644 --- a/dict_deserializer/deserializer.py +++ b/dict_deserializer/deserializer.py @@ -1,23 +1,40 @@ -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 @@ -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 @@ -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 = [] @@ -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))) @@ -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]): @@ -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 @@ -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 @@ -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) @@ -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: @@ -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): @@ -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 @@ -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))