Skip to content

Commit

Permalink
Conversions + cache refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
wyfo committed Oct 1, 2020
1 parent 5c24a31 commit 2471fb6
Show file tree
Hide file tree
Showing 31 changed files with 270 additions and 219 deletions.
6 changes: 3 additions & 3 deletions apischema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"ValidationError",
"alias",
"check_types",
"conversion",
"conversions",
"deserialization",
"deserialize",
"deserializer",
Expand All @@ -25,7 +25,7 @@


from . import (
conversion,
conversions,
deserialization,
fields,
json_schema,
Expand All @@ -35,7 +35,7 @@
validation,
)
from .aliases import alias
from .conversion import deserializer, serializer
from .conversions import deserializer, serializer
from .deserialization import deserialize
from .json_schema.refs import schema_ref
from .json_schema.schema import schema
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__all__ = [
"conversions",
"dataclass_serializer",
"deserializer",
"extra_deserializer",
"extra_serializer",
"inherited_deserializer",
"raw_deserializer",
Expand All @@ -9,13 +10,14 @@
"serializer",
]

from .conversions import conversions
from .converters import (
deserializer,
extra_deserializer,
extra_serializer,
inherited_deserializer,
reset_deserializers,
self_deserializer,
serializer,
)
from .dataclass_serializers import dataclass_serializer
from .raw import raw_deserializer
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
overload,
)

from apischema.conversion.utils import (
from apischema.conversions.utils import (
Conversions,
ConverterWithConversions,
check_converter,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dataclasses
from collections import ChainMap
from copy import copy
from dataclasses import Field, field, make_dataclass
from inspect import getmembers
from itertools import chain
from typing import (
Expand All @@ -16,20 +16,20 @@
Union,
)

from apischema import schema_ref, serializer
from apischema.conversion import conversions
from apischema.conversion.converters import extra_serializer
from apischema.conversion.utils import Conversions
from apischema.conversions.converters import extra_serializer, serializer
from apischema.conversions.metadata import conversions
from apischema.conversions.utils import Conversions
from apischema.dataclasses.cache import get_aggregate_serialization_fields
from apischema.fields import with_fields_set
from apischema.json_schema.refs import schema_ref
from apischema.typing import get_type_hints
from apischema.utils import MakeDataclassField

Metadata = Mapping[str, Any]


def _check_field(field: Any) -> str:
if isinstance(field, Field):
if isinstance(field, dataclasses.Field):
return field.name
elif isinstance(field, str):
return field
Expand All @@ -39,41 +39,39 @@ def _check_field(field: Any) -> str:
raise TypeError("Serialization fields must be Field or str or property")


Cls = TypeVar("Cls", bound=Type)
FieldFactory = Callable[[Cls], Any]
T = TypeVar("T")
FieldFactory = Callable[[T], Any]


def dataclass_serializer(
cls: Cls,
cls: Type[T],
ref: Optional[str] = None,
*,
include: Optional[Collection[Any]] = None,
include_properties: bool = False,
exclude: Collection[str] = (),
override: Optional[Mapping[str, Optional[Metadata]]] = None,
additional: Optional[
Mapping[str, Union[FieldFactory, Tuple[FieldFactory, Conversions]]]
Mapping[str, Union[FieldFactory[T], Tuple[FieldFactory[T], Conversions]]]
] = None,
as_default_serializer: bool = False,
) -> Type:
if override is None:
override = {}
override = {_check_field(field_): metadata for field_, metadata in override.items()}
override = {_check_field(field): metadata for field, metadata in override.items()}
if include is not None:
include = set(map(_check_field, include)) | override.keys()
exclude = set(map(_check_field, exclude))
fields, properties_fields = get_aggregate_serialization_fields(cls)
new_fields: Dict[str, MakeDataclassField] = {}
for field_ in chain(fields, properties_fields):
if field_.name in exclude or (
include is not None and field_.name not in include
):
for field in chain(fields, properties_fields):
if field.name in exclude or (include is not None and field.name not in include):
continue
new_field = copy(field_.base_field)
metadata = override.get(field_.name)
new_field = copy(field.base_field)
metadata = override.get(field.name)
if metadata is not None:
new_field.metadata = ChainMap(metadata, new_field.metadata)
new_fields[field_.name] = field_.name, field_.type, new_field
new_fields[field.name] = field.name, field.base_field.type, new_field
for name, prop in getmembers(cls, lambda m: isinstance(m, property)):
if name in exclude or (
not include_properties and include is not None and name not in include
Expand All @@ -95,15 +93,15 @@ def dataclass_serializer(
else:
conv = None
factories[name] = factory
new_field = field(metadata=conversions(serialization=conv))
new_field = dataclasses.field(metadata=conversions(serialization=conv))
types = get_type_hints(factory, include_extras=True)
try:
new_fields[name] = name, types["return"], new_field
except KeyError:
raise TypeError("Additional field factories must be typed") from None
attributes_list = list(attributes_set) # for iteration performance
serialized_class = with_fields_set(
make_dataclass(f"{cls.__name__}Serializer", new_fields.values())
dataclasses.make_dataclass(f"{cls.__name__}Serializer", new_fields.values())
)
if ref is ...:
raise TypeError("Dataclass serializer ref cannot be ...")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
from collections import ChainMap
from dataclasses import dataclass, fields
from types import MappingProxyType
from typing import (
Callable,
Mapping,
Optional,
Type,
TypeVar,
Union,
overload,
)

from apischema.conversion.raw import to_raw_deserializer
from apischema.conversion.utils import Conversions, Converter
from typing import Callable, Mapping, Optional, Type, TypeVar, overload

from apischema.conversions.raw import to_raw_deserializer
from apischema.conversions.utils import Conversions, Converter
from apischema.dataclasses import is_dataclass
from apischema.types import AnyType, MetadataMixin
from apischema.types import AnyType, Metadata, MetadataMixin

Cls = TypeVar("Cls", bound=Type)


@dataclass
class ConversionsMetadata(MetadataMixin):
both: Optional[AnyType] = None
deserialization: Optional[Conversions] = None
serialization: Optional[Conversions] = None
deserializer: Optional[Converter] = None
Expand All @@ -31,11 +22,6 @@ def __post_init__(self):
from apischema.metadata.keys import CONVERSIONS_METADATA

super().__init__(CONVERSIONS_METADATA)
if (
self.both is not None
and len([f for f in fields(self) if getattr(self, f.name) is not None]) > 1
):
raise ValueError("Cannot set both and (de)serialization/(de)serializers")

def __call__(self, dataclass: Cls) -> Cls:
from apischema.metadata.keys import CONVERSIONS_METADATA, MERGED_METADATA
Expand All @@ -46,7 +32,7 @@ def __call__(self, dataclass: Cls) -> Cls:
if conv is not None and not isinstance(conv, Mapping):
raise TypeError("Dataclasses conversions must be a Mapping instance")
for field in fields(dataclass):
if MERGED_METADATA in field.metadata:
if field.metadata.get(MERGED_METADATA):
continue
if CONVERSIONS_METADATA in field.metadata:
conversions: ConversionsMetadata = field.metadata[CONVERSIONS_METADATA]
Expand All @@ -67,11 +53,18 @@ def __call__(self, dataclass: Cls) -> Cls:
return dataclass


Conversions_ = Union[Mapping[AnyType, AnyType], AnyType]
@dataclass
class ConversionsMetadataFactory(MetadataMixin):
factory: Callable[[AnyType], ConversionsMetadata]

def __post_init__(self):
from apischema.metadata.keys import CONVERSIONS_METADATA

super().__init__(CONVERSIONS_METADATA)


@overload
def conversions(both: AnyType = None) -> ConversionsMetadata:
def conversions(model: AnyType) -> Metadata:
...


Expand All @@ -88,16 +81,21 @@ def conversions(


def conversions(
both: AnyType = None,
model: AnyType = None,
*,
deserialization: Conversions = None,
serialization: Conversions = None,
deserializer: Converter = None,
serializer: Converter = None,
raw_deserializer: Callable = None,
) -> ConversionsMetadata:
if raw_deserializer is not None:
deserializer = to_raw_deserializer(raw_deserializer)
return ConversionsMetadata(
both, deserialization, serialization, deserializer, serializer
)
) -> Metadata:
if model is not None:
return ConversionsMetadataFactory(
lambda cls: ConversionsMetadata({cls: model}, {cls: model})
)
else:
if raw_deserializer is not None:
deserializer = to_raw_deserializer(raw_deserializer)
return ConversionsMetadata(
deserialization, serialization, deserializer, serializer
)
4 changes: 2 additions & 2 deletions apischema/conversion/raw.py → apischema/conversions/raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from inspect import Parameter, signature
from typing import Any, Callable, List, Mapping, TypeVar

from apischema.conversion.converters import deserializer
from apischema.conversion.utils import Conversions, Converter
from apischema.conversions.converters import deserializer
from apischema.conversions.utils import Conversions, Converter
from apischema.fields import with_fields_set
from apischema.typing import get_type_hints
from apischema.utils import MakeDataclassField, as_dict, to_camel_case
Expand Down
48 changes: 5 additions & 43 deletions apischema/conversion/utils.py → apischema/conversions/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys
from inspect import Parameter, isclass, signature
from typing import (
Any,
Expand All @@ -13,8 +12,8 @@
cast,
)

from apischema.types import AnyType
from apischema.typing import _GenericAlias, get_type_hints
from apischema.types import AnyType, get_typed_origin
from apischema.typing import get_type_hints
from apischema.utils import type_name
from apischema.visitor import Visitor

Expand Down Expand Up @@ -72,43 +71,6 @@ def check_converter(
Cls = TypeVar("Cls", bound=Type)
Other = TypeVar("Other", bound=Type)

if sys.version_info >= (3, 7): # pragma: no cover
import collections.abc
import re
from typing import ( # noqa F811
List,
AbstractSet,
Set,
Dict,
Collection,
Sequence,
MutableSequence,
Pattern,
)

TYPED_ORIGINS = {
tuple: Tuple,
list: List,
frozenset: AbstractSet,
set: Set,
dict: Dict,
collections.abc.Collection: Collection,
collections.abc.Sequence: Sequence,
collections.abc.MutableSequence: MutableSequence,
collections.abc.Set: AbstractSet,
collections.abc.MutableSet: Set,
re.Pattern: Pattern,
}

def _get_origin(cls: _GenericAlias) -> Type: # type: ignore
return TYPED_ORIGINS.get(cls.__origin__, cls.__origin__) # type: ignore


else: # pragma: no cover

def _get_origin(cls: _GenericAlias) -> Type: # type: ignore
return cls.__origin__ # type: ignore


def substitute_type_vars(base: Cls, other: Other) -> Tuple[Cls, Other]:
if getattr(base, "__origin__", None) is None:
Expand All @@ -118,16 +80,16 @@ def substitute_type_vars(base: Cls, other: Other) -> Tuple[Cls, Other]:
f"Generic conversion doesn't support partial specialization,"
f" aka {type_name(base)}[{','.join(map(type_name, base.__args__))}]"
)
substitution = dict(zip(base.__args__, _get_origin(base).__parameters__))
substitution = dict(zip(base.__args__, get_typed_origin(base).__parameters__))
if isinstance(other, TypeVar): # type: ignore
new_other = substitution.get(other, other)
elif getattr(other, "__origin__", None) is not None:
new_other = _get_origin(other)[
new_other = get_typed_origin(other)[
tuple(substitution.get(arg, arg) for arg in other.__args__)
]
else:
new_other = other
return cast(Tuple[Cls, Other], (_get_origin(base), new_other))
return cast(Tuple[Cls, Other], (get_typed_origin(base), new_other))


class ConvertibleVisitor(Visitor):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
TypeVar,
)

from apischema.conversion.converters import (
from apischema.conversions.converters import (
_deserializers,
_extra_deserializers,
_extra_serializers,
_serializers,
)
from apischema.conversion.utils import Conversions, ConverterWithConversions
from apischema.conversions.utils import Conversions, ConverterWithConversions
from apischema.visitor import (
Arg,
Return,
Expand Down
Loading

0 comments on commit 2471fb6

Please sign in to comment.