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:
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
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))
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