Skip to content

Commit

Permalink
Merge pull request #31 from mdrachuk/validate-options
Browse files Browse the repository at this point in the history
Validate options
  • Loading branch information
mdrachuk committed Aug 14, 2019
2 parents ef7815d + ed432a3 commit b42d066
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 56 deletions.
22 changes: 15 additions & 7 deletions docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,20 +187,28 @@ class JsonModel(Generic[T]):
def __init__(
self,
cls: Type[T],
*,
serializers: Iterable[Type[FieldSerializer]] = field_serializers(),
*,
allow_any: bool = False,
allow_missing: bool = False,
allow_unexpected: bool = False,
validate_on_load: bool = True,
validate_on_dump: bool = False,
ensure_frozen: Union[bool, Iterable[Type]] = False,
camel_case: bool = True,
indent: Optional[int] = None,
):
self._descriptor = describe(cls)
self._serializer = SeriousModel(
self.descriptor,
self._serializer: SeriousModel = SeriousModel(
self._descriptor,
serializers,
allow_any=allow_any,
allow_missing=allow_missing,
allow_unexpected=allow_unexpected,
validate_on_load=validate_on_load,
validate_on_dump=validate_on_dump,
ensure_frozen=ensure_frozen,
key_mapper=JsonKeyMapper() if camel_case else None,
)
self._dump_indentation = indent

Expand All @@ -210,24 +218,24 @@ class JsonModel(Generic[T]):

def load(self, json_: str) -> T:
data: MutableMapping = self._load_from_str(json_)
_check_that_loading_an_object(data, self.cls)
check_that_loading_an_object(data, self.cls)
return self._from_dict(data)

def load_many(self, json_: str) -> List[T]:
data: Collection = self._load_from_str(json_)
_check_that_loading_a_list(data, self.cls)
check_that_loading_a_list(data, self.cls)
return [self._from_dict(each) for each in data]

def dump(self, o: T) -> str:
_check_is_instance(o, self.cls)
check_is_instance(o, self.cls)
return self._dump_to_str(self._serializer.dump(o))

def dump_many(self, items: Collection[T]) -> str:
dict_items = list(map(self._dump, items))
return self._dump_to_str(dict_items)

def _dump(self, o) -> Dict[str, Any]:
return self._serializer.dump(_check_is_instance(o, self.cls))
return self._serializer.dump(check_is_instance(o, self.cls))

def _from_dict(self, data: MutableMapping) -> T:
return self._serializer.load(data)
Expand Down
3 changes: 2 additions & 1 deletion docs/validation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Validating Objects

In serious any “loaded” object is validated before being returned.
By default, any “loaded” object is validated before being returned.
This can be overridden by passing `validate_on_load` and `validate_on_dump` options to models.

To add validators to an object just create a `__validate__(self)` method in it.

Expand Down
6 changes: 6 additions & 0 deletions serious/dict/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def __init__(
allow_any: bool = False,
allow_missing: bool = False,
allow_unexpected: bool = False,
validate_on_load: bool = True,
validate_on_dump: bool = False,
ensure_frozen: Union[bool, Iterable[Type]] = False,
):
"""
Expand All @@ -29,6 +31,8 @@ def __init__(
(this includes generics like `List[Any]`, or simply `list`).
@param allow_missing `False` to raise during load if data is missing the optional fields.
@param allow_unexpected `False` to raise during load if data contains some unknown fields.
@param validate_on_load to call dataclass __validate__ method after object construction.
@param validate_on_load to call object __validate__ before dumping.
@param ensure_frozen `False` to skip check of model immutability; `True` will perform the check
against built-in immutable types; a list of custom immutable types is added to built-ins.
"""
Expand All @@ -39,6 +43,8 @@ def __init__(
allow_any=allow_any,
allow_missing=allow_missing,
allow_unexpected=allow_unexpected,
validate_on_load=validate_on_load,
validate_on_dump=validate_on_dump,
ensure_frozen=ensure_frozen,
)

Expand Down
6 changes: 6 additions & 0 deletions serious/json/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def __init__(
allow_any: bool = False,
allow_missing: bool = False,
allow_unexpected: bool = False,
validate_on_load: bool = True,
validate_on_dump: bool = False,
ensure_frozen: Union[bool, Iterable[Type]] = False,
camel_case: bool = True,
indent: Optional[int] = None,
Expand All @@ -34,6 +36,8 @@ def __init__(
(this includes generics like `List[Any]`, or simply `list`).
@param allow_missing `False` to raise during load if data is missing the optional fields.
@param allow_unexpected `False` to raise during load if data contains some unknown fields.
@param validate_on_load to call dataclass __validate__ method after object construction.
@param validate_on_load to call object __validate__ before dumping.
@param ensure_frozen `False` to skip check of model immutability; `True` will perform the check
against built-in immutable types; a list of custom immutable types is added to built-ins.
@param camel_case `True` to transform dataclass "snake_case" to JSON "camelCase".
Expand All @@ -46,6 +50,8 @@ def __init__(
allow_any=allow_any,
allow_missing=allow_missing,
allow_unexpected=allow_unexpected,
validate_on_load=validate_on_load,
validate_on_dump=validate_on_dump,
ensure_frozen=ensure_frozen,
key_mapper=JsonKeyMapper() if camel_case else None,
)
Expand Down
2 changes: 1 addition & 1 deletion serious/serialization/field_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ def fits(cls, desc: TypeDescriptor) -> bool:
return issubclass(desc.cls, UUID)


_decimal_re = re.compile(r'\A\d+?\.\d+?\Z')
_decimal_re = re.compile(r'\A\d+(\.\d+)?\Z')


class DecimalSerializer(FieldSerializer[Decimal, str]):
Expand Down
37 changes: 26 additions & 11 deletions serious/serialization/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ def __init__(
allow_any: bool,
allow_missing: bool,
allow_unexpected: bool,
ensure_frozen: Union[bool, Iterable[Type]] = False,
validate_on_load: bool,
validate_on_dump: bool,
ensure_frozen: Union[bool, Iterable[Type]],
key_mapper: Optional[KeyMapper] = None,
_registry: Dict[TypeDescriptor, SeriousModel] = None
):
Expand All @@ -52,9 +54,11 @@ def __init__(
(this includes generics like `List[Any]`, or simply `list`).
@param allow_missing `False` to raise during load if data is missing the optional fields.
@param allow_unexpected `False` to raise during load if data contains some unknown fields.
@param validate_on_load to call dataclass __validate__ method after object construction.
@param validate_on_load to call object __validate__ before dumping.
@param ensure_frozen `False` to skip check of model immutability; `True` will perform the check
against built-in immutable types; a list of custom immutable types is added to built-ins.
@param key_mapper remap field names of between dataclass and serialized objects
@param key_mapper remap field names of between dataclass and serialized objects.
@param _registry a mapping of dataclass type descriptors to corresponding serious serializer;
used internally to create child serializers.
"""
Expand All @@ -71,9 +75,12 @@ def __init__(
raise MutableTypesInModel(descriptor.cls, mutable_types)
self._descriptor = descriptor
self._serializers = tuple(serializers)
self._allow_any = allow_any
self._allow_missing = allow_missing
self._allow_unexpected = allow_unexpected
self._allow_any = allow_any
self._validate_on_load = validate_on_load
self._validate_on_dump = validate_on_dump
self._ensure_frozen = ensure_frozen
self._serializer_registry = {descriptor: self} if _registry is None else _registry
self._keys = key_mapper or NoopKeyMapper()
self._serializers_by_field = {name: self.find_serializer(desc) for name, desc in descriptor.fields.items()}
Expand All @@ -88,7 +95,7 @@ def load(self, data: Mapping, _ctx: Optional[Loading] = None) -> T:

check_is_instance(data, Mapping, f'Invalid data for {self._cls}') # type: ignore
root = _ctx is None
loading: Loading = Loading(validating=True) if root else _ctx # type: ignore # checked above
loading: Loading = Loading(validating=self._validate_on_load) if root else _ctx # type: ignore # checked above
mut_data = {self._keys.to_model(key): value for key, value in data.items()}
if self._allow_missing:
for field in _fields_missing_from(mut_data, self._cls):
Expand All @@ -104,14 +111,14 @@ def load(self, data: Mapping, _ctx: Optional[Loading] = None) -> T:
if field in mut_data
}
result = self._cls(**init_kwargs) # type: ignore # not an object
validate(result)
if self._validate_on_load:
validate(result)
return result
except ValidationError:
raise
except Exception as e:
if root:
if isinstance(e, ValidationError):
raise
else:
raise LoadError(self._cls, loading.stack, data) from e
raise LoadError(self._cls, loading.stack, data) from e
raise

def dump(self, o: T, _ctx: Optional[Dumping] = None) -> Dict[str, Any]:
Expand All @@ -120,12 +127,16 @@ def dump(self, o: T, _ctx: Optional[Dumping] = None) -> Dict[str, Any]:
check_is_instance(o, self._cls)
root = _ctx is None
dumping: Dumping = Dumping(validating=False) if root else _ctx # type: ignore # checked above
_s = self._keys.to_serialized
try:
_s = self._keys.to_serialized
if self._validate_on_dump:
validate(o)
return {
_s(field): dumping.run(f'.{_s(field)}', serializer, getattr(o, field))
for field, serializer in self._serializers_by_field.items()
}
except ValidationError:
raise
except Exception as e:
if root:
raise DumpError(o, dumping.stack) from e
Expand All @@ -143,9 +154,13 @@ def child_model(self, descriptor: TypeDescriptor) -> SeriousModel:
new_model: SeriousModel = SeriousModel(
descriptor=descriptor,
serializers=self._serializers,
allow_any=self._allow_any,
allow_missing=self._allow_missing,
allow_unexpected=self._allow_unexpected,
allow_any=self._allow_any,
validate_on_load=self._validate_on_load,
validate_on_dump=self._validate_on_dump,
ensure_frozen=self._ensure_frozen,
key_mapper=self._keys,
_registry=self._serializer_registry
)
self._serializer_registry[descriptor] = new_model
Expand Down
10 changes: 1 addition & 9 deletions tests/entities.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
from dataclasses import dataclass, field
from typing import Dict, FrozenSet, List, Optional, Set, Tuple, TypeVar, Union, Type
from typing import Dict, FrozenSet, List, Optional, Set, Tuple, TypeVar, Union
from uuid import UUID

A = TypeVar('A')


def dataclass_of(type_: Type):
@dataclass(frozen=True)
class GenericDataclass:
value: type_

return GenericDataclass


@dataclass(frozen=True)
class DataclassWithList:
xs: List[int]
Expand Down
41 changes: 30 additions & 11 deletions tests/test_symmetry.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,41 @@
from typing import List, Set, Tuple, Optional, FrozenSet
from collections import namedtuple
from dataclasses import dataclass
from decimal import Decimal
from typing import List, Set, Tuple, Optional, FrozenSet, Type

from serious import JsonModel, DictModel
from serious.test_utils import assert_symmetric
from tests.entities import dataclass_of
from tests.utils import with_

Ex = namedtuple('Example', ['value_type', 'value'])

models = [JsonModel, DictModel]
objects = [
dataclass_of(List[int])([1, 2, 3]),
dataclass_of(Set[int])({1, 2, 3}),
dataclass_of(Tuple[int, Ellipsis])(tuple([1, 2, 3])),
dataclass_of(Tuple[int, str, int])(tuple([1, 'two', 3])),
dataclass_of(FrozenSet[int])(frozenset([1, 2, 3])),
dataclass_of(Optional[int])(None),
dataclass_of(Optional[int])(3),
Ex(int, 3),
Ex(float, 0.7),
Ex(str, '¡hellø!'),
Ex(Decimal, Decimal('3')),
Ex(List[int], [1, 2, 3]),
Ex(Set[int], {1, 2, 3}),
Ex(Tuple[int, Ellipsis], tuple([1, 2, 3])),
Ex(Tuple[int, str, int], tuple([1, 'two', 3])),
Ex(FrozenSet[int], frozenset([1, 2, 3])),
Ex(Optional[int], None),
Ex(Optional[int], 3),
]


@with_(models, objects)
def test_generic_encode_and_decode_are_inverses(new_model, dc):
assert_symmetric(new_model(type(dc)), dc)
def test_generic_encode_and_decode_are_inverses(new_model, example: Ex):
type_ = generic_dataclass(example.value_type)
model = new_model(type_)
instance = type_(example.value)
assert_symmetric(model, instance)


def generic_dataclass(type_: Type):
@dataclass(frozen=True)
class GenDataclass:
value: type_

return GenDataclass

0 comments on commit b42d066

Please sign in to comment.