Commit e2b97bcf authored by Rolf H. B. van Kleef's avatar Rolf H. B. van Kleef

Documentation and style!

parent 00e99d9c
Pipeline #1261 canceled with stages
...@@ -15,7 +15,8 @@ class KeyValueDiscriminator: ...@@ -15,7 +15,8 @@ class KeyValueDiscriminator:
def __repr__(self): def __repr__(self):
if self.has_value: 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) return 'KeyValueDiscriminator(key={})'.format(self.key)
def check(self, d: dict): def check(self, d: dict):
...@@ -56,4 +57,4 @@ def discriminate(key=None, value=sentinel, matcher=None): ...@@ -56,4 +57,4 @@ def discriminate(key=None, value=sentinel, matcher=None):
def abstract(cls): def abstract(cls):
cls._abstract = True cls._abstract = True
return cls return cls
\ No newline at end of file
from typing import Optional, Union, List from typing import Optional, Union, List, Tuple, Dict
from typeguard import check_type from typeguard import check_type
class Rule: class Rule:
"""
This class is primarily used as a container to store type information.
"""
@staticmethod @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): if isinstance(tpe, Rule):
return tpe return tpe
return Rule(tpe) return Rule(tpe)
def __init__(self, type, default=None): # noinspection PyShadowingBuiltins
def __init__(self: 'Rule', type, default=None):
self.type = type self.type = type
self.default = default self.default = default
def __repr__(self): def __repr__(self: 'Rule'):
return "Rule(type={}, default={})".format(self.type, self.default) 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) check_type(key, value, self.type)
if value is None: if value is None:
value = self.default value = self.default
...@@ -26,7 +43,13 @@ class Rule: ...@@ -26,7 +43,13 @@ class Rule:
class BaseMeta(type): 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['_discriminators'] = []
namespace['_abstract'] = False namespace['_abstract'] = False
...@@ -44,7 +67,10 @@ class BaseMeta(type): ...@@ -44,7 +67,10 @@ class BaseMeta(type):
return cls 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: if ls is None:
ls = [] ls = []
...@@ -56,14 +82,31 @@ def rbase(cls, ls=None): ...@@ -56,14 +82,31 @@ def rbase(cls, ls=None):
return ls return ls
def _is_valid(key: str, value): def _is_valid(key: str, value) -> bool:
return not key.startswith('_') and not callable(value) and not isinstance(value, classmethod) and\ """
not isinstance(value, staticmethod) and not isinstance(value, property) 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): class Deserializable(metaclass=BaseMeta):
"""
Base class for all automagically deserializing classes.
"""
@classmethod @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 = {} fields = {}
defaults = {} defaults = {}
rl = list(reversed(rbase(cls))) rl = list(reversed(rbase(cls)))
...@@ -72,7 +115,8 @@ class Deserializable(metaclass=BaseMeta): ...@@ -72,7 +115,8 @@ class Deserializable(metaclass=BaseMeta):
for k in c.__dict__: for k in c.__dict__:
if _is_valid(k, c.__dict__[k]): if _is_valid(k, c.__dict__[k]):
defaults[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__: for k in cls.__annotations__:
if k in defaults and not _is_valid(k, defaults[k]): if k in defaults and not _is_valid(k, defaults[k]):
...@@ -87,9 +131,18 @@ class Deserializable(metaclass=BaseMeta): ...@@ -87,9 +131,18 @@ class Deserializable(metaclass=BaseMeta):
def get_deserialization_classes(t, d, try_all=True) -> List[type]: 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 = [] candidates = []
for sc in t.__subclasses__(): for sc in t.__subclasses__():
if hasattr(sc, '_discriminators'): if hasattr(sc, '_discriminators'):
# noinspection PyProtectedMember
for discriminator in sc._discriminators: for discriminator in sc._discriminators:
if not discriminator.check(d): if not discriminator.check(d):
# Invalid # Invalid
...@@ -97,7 +150,8 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]: ...@@ -97,7 +150,8 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]:
else: else:
# All were valid # All were valid
try: try:
candidates.extend(get_deserialization_classes(sc, t, try_all)) candidates.extend(
get_deserialization_classes(sc, t, try_all))
except TypeError as e: except TypeError as e:
if not try_all: if not try_all:
raise e raise e
...@@ -107,7 +161,18 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]: ...@@ -107,7 +161,18 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]:
return candidates 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. # In case of primitive types, attempt to assign.
try: try:
return rule.validate(key, data) return rule.validate(key, data)
...@@ -123,7 +188,8 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): ...@@ -123,7 +188,8 @@ def deserialize(rule: Rule, data, try_all=True, key='$'):
return v return v
except TypeError: except TypeError:
pass 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 type(rule.type) is type(List):
if len(rule.type.__args__) != 1: if len(rule.type.__args__) != 1:
...@@ -135,7 +201,12 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): ...@@ -135,7 +201,12 @@ def deserialize(rule: Rule, data, try_all=True, key='$'):
t = rule.type.__args__[0] t = rule.type.__args__[0]
result = [] result = []
for i, v in enumerate(data): 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 return result
if issubclass(rule.type, Deserializable): if issubclass(rule.type, Deserializable):
...@@ -148,7 +219,12 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): ...@@ -148,7 +219,12 @@ def deserialize(rule: Rule, data, try_all=True, key='$'):
try: try:
instance = cls() instance = cls()
for k, r in cls.get_attrs().items(): 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) setattr(instance, k, v)
return instance return instance
...@@ -156,6 +232,8 @@ def deserialize(rule: Rule, data, try_all=True, key='$'): ...@@ -156,6 +232,8 @@ def deserialize(rule: Rule, data, try_all=True, key='$'):
if not try_all: if not try_all:
raise e 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)) raise TypeError('Unable to find a deserialization candidate for '
\ No newline at end of file '{} with key {}.'.format(rule, key))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment