Skip to content

Commit

Permalink
Merge pull request #33 from mdrachuk/polish
Browse files Browse the repository at this point in the history
Polish
  • Loading branch information
mdrachuk committed Aug 15, 2019
2 parents b42d066 + 0c2d607 commit 4c43193
Show file tree
Hide file tree
Showing 31 changed files with 691 additions and 498 deletions.
6 changes: 3 additions & 3 deletions docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ Loading them will result in error:
>>> JsonModel(Message, allow_any=False)
Traceback (most recent call last):
File "<input>", line 1, in <module>
File ".../serious/json/api.py", line 42, in __init__
allow_unexpected=allow_unexpected,
File ".../serious/serialization.py", line 729, in __init__
File "..serious/serious/json/model.py", line 80, in __init__
key_mapper=JsonKeyMapper() if camel_case else None,
File "..serious/serious/serialization/model.py", line 71, in __init__
raise ModelContainsAny(descriptor.cls)
serious.errors.ModelContainsAny: <class '__main__.Message'>
```
Expand Down
17 changes: 16 additions & 1 deletion serious/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
from .descriptors import TypeDescriptor, describe
"""Serious is a Python dataclass model toolkit for serialization, validation, and more.
Core functionality is available for import from `serious`, and is listed here in `serious/__init__`:
models, errors, types and validation.
To provide custom field serialization use `serious.serialization`.
Test utils can be found in `serious.test_utils`.
`More on Read The Docs.`_
`Sources on GitHub.`_
.. _More on Read The Docs.: https://serious.readthedocs.io/en/latest/
.. _Sources on GitHub.: https://github.com/mdrachuk/serious
"""

from .dict import DictModel
from .errors import ModelError, ValidationError, LoadError, DumpError
from .json import JsonModel
Expand Down
File renamed without changes.
38 changes: 23 additions & 15 deletions serious/descriptors.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
from __future__ import annotations

__all__ = ['TypeDescriptor', 'describe', 'DescTypes', 'scan_types']
__doc__ = """Descriptors of types used by serious.
"""Descriptors of types used by Serious.
Descriptors are simplifying work with types, enriching them with more contextual information.
This allows to make decisions, like picking a serializer, easier.
Expand All @@ -11,6 +8,10 @@
The data is carried by `TypeDescriptor`s which are created by a call to `serious.descriptors.describe(cls)`.
"""
from __future__ import annotations

__all__ = ['TypeDescriptor', 'describe', 'DescTypes', 'scan_types']

from collections import ChainMap
from dataclasses import dataclass, fields, is_dataclass
from typing import Type, Any, TypeVar, get_type_hints, Dict, Mapping, List, Union, Iterable
Expand All @@ -24,6 +25,13 @@

@dataclass(frozen=True)
class TypeDescriptor:
"""A descriptor of a type unwrapping the aliases, optionals, separating generic parameters,
extracting the parameters from broader context, etc.
Type descriptors are mostly used for mapping serializers to particular objects.
A proper way of creating a `TypeDescriptor` is using the `serious.descriptors.describe(cls)` factory.
"""
_cls: Type
parameters: FrozenDict[Any, TypeDescriptor]
is_optional: bool = False
Expand All @@ -37,7 +45,7 @@ def cls(self): # Python fails when providing cls as a keyword parameter to data
def fields(self) -> Mapping[str, TypeDescriptor]:
"""A mapping of all dataclass field names to their corresponding Type Descriptors.
Returns an empty mapping if the object is not a dataclass."""
An empty mapping is returned if the object is not a dataclass."""
if not is_dataclass(self.cls):
return {}
types = get_type_hints(self.cls) # type: Dict[str, Type]
Expand Down Expand Up @@ -73,10 +81,10 @@ def describe(type_: Type, generic_params: GenericParams = None) -> TypeDescripto
def _get_default_generic_params(cls: Type, params: GenericParams) -> GenericParams:
"""Returns mapping of default generic params for the provided cls.
Examples:
- `dict` -> {0: <TypeDescriptor cls=Any>, 1: <TypeDescriptor cls=Any>};
- `list` -> {0: <TypeDescriptor cls=Any>};
- `tuple` -> {0: <TypeDescriptor cls=Any>, 1: <TypeDescriptor cls=Ellipses>}.
**Examples**:
- `dict` -> `{0: <TypeDescriptor cls=Any>, 1: <TypeDescriptor cls=Any>}`;
- `list` -> `{0: <TypeDescriptor cls=Any>}`;
- `tuple` -> `{0: <TypeDescriptor cls=Any>, 1: <TypeDescriptor cls=Ellipses>}`.
"""
for generic, default_params in _generic_params.items():
if issubclass(cls, generic):
Expand All @@ -87,9 +95,9 @@ def _get_default_generic_params(cls: Type, params: GenericParams) -> GenericPara
def _describe_generic(cls: Type, generic_params: GenericParams) -> TypeDescriptor:
"""Creates a TypeDescriptor for Python _GenericAlias, unwrapping it to its origin/
Examples:
- Tuple[str] -> <TypeDescriptor cls=tuple params={0: <TypeDescriptor cls=str>}>
- Optional[int] -> <TypeDescriptor cls=int is_optional=True>
**Examples**:
- `Tuple[str]` -> `<TypeDescriptor cls=tuple params={0: <TypeDescriptor cls=str>}>`;
- `Optional[int]` -> `<TypeDescriptor cls=int is_optional=True>`.
"""
params: GenericParams = {}
is_optional = _is_optional(cls)
Expand Down Expand Up @@ -162,14 +170,14 @@ def __contains__(self, item):


def scan_types(desc: TypeDescriptor) -> DescTypes:
"""Create a DescTypes object for the provided descriptor.
"""Create a `DescTypes` object for the provided descriptor.
DescTypes allow checks of the descriptor tree."""
`DescTypes` allow checks of the descriptor tree."""
return DescTypes.scan(desc, known=[])


def _is_optional(cls: Type) -> bool:
"""Returns True if the provided type is Optional."""
"""Returns True if the provided type is `Optional`."""
return getattr(cls, '__origin__', None) == Union \
and len(cls.__args__) == 2 \
and cls.__args__[1] == type(None)
4 changes: 3 additions & 1 deletion serious/dict/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""A module with the `DictModel` -- Serious model to transform between dataclasses and dictionaries."""

__all__ = ['DictModel']

from .api import DictModel
from .model import DictModel
78 changes: 0 additions & 78 deletions serious/dict/api.py

This file was deleted.

101 changes: 101 additions & 0 deletions serious/dict/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""A module with `DictModel` -- Serious model to transform between dataclasses and dictionaries."""
from __future__ import annotations

__all__ = ['DictModel']

from typing import TypeVar, Type, Generic, List, Collection, Dict, Iterable, Any, Union

from serious.descriptors import describe, TypeDescriptor
from serious.serialization import FieldSerializer, SeriousModel, field_serializers
from serious.utils import class_path

T = TypeVar('T')


class DictModel(Generic[T]):
"""A model converting dataclasses to dicts and back.
:Example:
from uuid import UUID
from dataclasses import dataclass
from serious import DictModel
@dataclass
class Robot:
serial: UUID
name: str
>>> model = DictModel(Robot)
>>> model.load({'serial': 'f3179d05-30f6-43ba-b6cb-7556af09330b', 'name': 'Caliban'})
Robot(serial=UUID('f3179d05-30f6-43ba-b6cb-7556af09330b'), name='Caliban')
>>> model.dump(Robot(UUID('00000000-0000-4000-0000-000002716057'), 'Bender'))
{'serial': '00000000-0000-4000-0000-000002716057', 'name': 'Bender'}
Check `__init__` parameters for a list of configuration options.
`More on models in docs <https://serious.readthedocs.io/en/latest/models/>`_.
"""
descriptor: TypeDescriptor
serious_model: SeriousModel

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,
):
"""Initialize a dictionary model.
:param cls: the dataclass type to load/dump.
:param serializers: field serializer classes in an order they will be tested for fitness for each field.
:param allow_any: `False` to raise if the model contains fields annotated with `Any`
(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_dump: 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.
"""
self.cls = cls
self.descriptor = describe(cls)
self.serious_model = 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,
)

def load(self, data: Dict[str, Any]) -> T:
"""Load dataclass from a dictionary."""
return self.serious_model.load(data)

def load_many(self, items: Iterable[Dict[str, Any]]) -> List[T]:
"""Load a list of dataclasses from a dictionary."""
return [self.load(each) for each in items]

def dump(self, o: T) -> Dict[str, Any]:
"""Dump a dataclasses to a dictionary."""
return self.serious_model.dump(o)

def dump_many(self, items: Collection[T]) -> List[Dict[str, Any]]:
"""Dump a list dataclasses to a dictionary."""
return [self.dump(o) for o in items]

def __repr__(self):
path = class_path(type(self))
if path == 'serious.dict.model.DictModel':
path = 'serious.DictModel'
return f'<{path}[{class_path(self.cls)}] at {hex(id(self))}>'
30 changes: 15 additions & 15 deletions serious/errors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
"""Errors raised by serious.
Errors are divided into 3 main groups:
1. Validation errors -- raised by serious or library users if an objects fails validation.
Contains the stack pointing to an object which raised the error.
2. Serialization errors -- wrap the exceptions which occur during serialization.
They also contain the stack pointing to a specific place which raised the error.
Can be one of `LoadError` or `DumpError`.
3. Model errors -- raised when traversing the dataclass and building the model.
When no serializer found for field, etc.
"""
from __future__ import annotations

__all__ = [
Expand All @@ -13,25 +25,13 @@
'MutableTypesInModel',
'ValidationError',
]
__doc__ = """Errors raised by serious.
Errors are divided into 3 main groups:
1. Validation errors -- raised by serious or library users if an objects fails validation.
Contains the stack pointing to an object which raised the error.
2. Serialization errors -- wrap the exceptions which occur during serialization.
They also contain the stack pointing to a specific place which raised the error.
Can be one of `LoadError` or `DumpError`.
3. Model errors -- raised when traversing the dataclass and building the model.
When no serializer found for field, etc.
"""

from typing import Type, Mapping, Collection, TYPE_CHECKING, Iterable

from .utils import class_path, Dataclass

if TYPE_CHECKING:
from .serialization.process import SerializationStep
from .serialization.context import SerializationStep
from .descriptors import TypeDescriptor


Expand Down Expand Up @@ -145,7 +145,7 @@ def __init__(self, cls: Type, desc: TypeDescriptor):
@property
def message(self):
return (f'{class_path(self.cls)} contains unserializable member: {self.desc}.'
f'Create a serializer fitting the descriptor and pass it to the model `serializers`.')
f'Create a serializer fitting the descriptor and pass it to the model ``serializers``.')


class ModelContainsAny(ModelError):
Expand All @@ -154,7 +154,7 @@ class ModelContainsAny(ModelError):
def message(self):
return (f'{class_path(self.cls)} contains fields annotated as Any or missing type annotation. '
f'Provide a type annotation or pass `allow_any=True` to the serializer. '
f'This may also be an ambiguous `Generic` definitions like `x: list`, `x: List` '
f'This may also be an ambiguous ``Generic`` definitions like `x: list`, `x: List` '
f'which are resolved as `List[Any]`. ')


Expand Down
3 changes: 2 additions & 1 deletion serious/json/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""A module with JSON model which serializes JSON strings to dataclasses."""
__all__ = ['JsonModel']

from .api import JsonModel
from .model import JsonModel

0 comments on commit 4c43193

Please sign in to comment.