def __init_subclass__(cls, **kwargs): # Deserializers stack directly as a Union deserializer(Conversion(identity, source=cls, target=Base)) # Only Base serializer must be registered (and updated for each subclass) as # a Union, and not be inherited Base._union = cls if Base._union is None else Union[Base._union, cls] serializer( Conversion(identity, source=Base, target=Base._union, inherited=False) )
def as_tagged_union(cls: Type): def serialization() -> Conversion: serialization_union = new_class( f"Tagged{cls.__name__}Union", (TaggedUnion, ), exec_body=lambda ns: ns.update({ "__annotations__": { sub.__name__: Tagged[sub] # type: ignore for sub in rec_subclasses(cls) } }), ) return Conversion( lambda obj: serialization_union(**{obj.__class__.__name__: obj}), source=cls, target=serialization_union, # Conversion must not be inherited because it would lead to infinite # recursion otherwise inherited=False, ) def deserialization() -> Conversion: annotations: Dict[str, Any] = {} deserialization_namespace: Dict[str, Any] = { "__annotations__": annotations } for sub in rec_subclasses(cls): annotations[sub.__name__] = Tagged[sub] # type: ignore # Add tagged fields for all its alternative constructors for constructor in _alternative_constructors.get(sub.__name__, ()): # Build the alias of the field alias = ("".join( map(str.capitalize, constructor.__name__.split("_"))) + sub.__name__) # object_deserialization uses get_type_hints, but the constructor # return type is stringified and the class not defined yet, # so it must be assigned manually constructor.__annotations__["return"] = sub # Add constructor tagged field with its conversion annotations[alias] = Tagged[sub] # type: ignore deserialization_namespace[alias] = Tagged( conversion( # Use object_deserialization to wrap constructor as deserializer deserialization=object_deserialization( constructor, type_name(alias)))) # Create the deserialization tagged union class deserialization_union = new_class( f"Tagged{cls.__name__}Union", (TaggedUnion, ), exec_body=lambda ns: ns.update(deserialization_namespace), ) return Conversion(lambda obj: get_tagged(obj)[1], source=deserialization_union, target=cls) deserializer(lazy=deserialization, target=cls) serializer(lazy=serialization, source=cls)
def __init_subclass__(cls, **kwargs): # Registers new subclasses automatically in the union cls._union. # Deserializers stack directly as a Union apischema.deserializer( apischema.conversions.Conversion(apischema.identity, source=cls, target=AsynPortBase)) # Only AsynPortBase serializer must be registered (and updated for each # subclass) as a Union, and not be inherited AsynPortBase._union = (cls if AsynPortBase._union is None else Union[AsynPortBase._union, cls]) apischema.serializer( apischema.conversions.Conversion( apischema.identity, source=AsynPortBase, target=AsynPortBase._union, inherited=False, ))
def as_tagged_union(cls: Cls) -> Cls: """ Tagged union decorator, to be used on base class. Supports generics as well, with names generated by way of `_get_generic_name_factory`. """ params = tuple(getattr(cls, "__parameters__", ())) tagged_union_bases: Tuple[type, ...] = (TaggedUnion, ) # Generic handling is here: if params: tagged_union_bases = (TaggedUnion, Generic[params]) generic_name(cls) prev_init_subclass = getattr(cls, "__init_subclass__", None) def __init_subclass__(cls, **kwargs): if prev_init_subclass is not None: prev_init_subclass(**kwargs) generic_name(cls) cls.__init_subclass__ = classmethod(__init_subclass__) def with_params(cls: type) -> Any: """Specify type of Generic if set.""" return cls[params] if params else cls def serialization() -> Conversion: """ Define the serializer Conversion for the tagged union. source is the base ``cls`` (or ``cls[T]``). target is the new tagged union class ``TaggedUnion`` which gets the dictionary {cls.__name__: obj} as its arguments. """ annotations = { # Assume that subclasses have same generic parameters than cls sub.__name__: Tagged[with_params(sub)] for sub in get_all_subclasses(cls) } namespace = {"__annotations__": annotations} tagged_union = new_class(cls.__name__, tagged_union_bases, exec_body=lambda ns: ns.update(namespace)) return Conversion( lambda obj: tagged_union(**{obj.__class__.__name__: obj}), source=with_params(cls), target=with_params(tagged_union), # Conversion must not be inherited because it would lead to # infinite recursion otherwise inherited=False, ) def deserialization() -> Conversion: """ Define the deserializer Conversion for the tagged union. Allows for alternative standalone constructors as per the apischema example. """ annotations: dict[str, Any] = {} namespace: dict[str, Any] = {"__annotations__": annotations} for sub in get_all_subclasses(cls): annotations[sub.__name__] = Tagged[with_params(sub)] for constructor in _alternative_constructors.get(sub, ()): # Build the alias of the field alias = to_pascal_case(constructor.__name__) # object_deserialization uses get_type_hints, but the constructor # return type is stringified and the class not defined yet, # so it must be assigned manually constructor.__annotations__["return"] = with_params(sub) # Use object_deserialization to wrap constructor as deserializer deserialization = object_deserialization( constructor, generic_name) # Add constructor tagged field with its conversion annotations[alias] = Tagged[with_params(sub)] namespace[alias] = Tagged( conversion(deserialization=deserialization)) # Create the deserialization tagged union class tagged_union = new_class(cls.__name__, tagged_union_bases, exec_body=lambda ns: ns.update(namespace)) return Conversion( lambda obj: get_tagged(obj)[1], source=with_params(tagged_union), target=with_params(cls), ) deserializer(lazy=deserialization, target=cls) serializer(lazy=serialization, source=cls) return cls
from dataclasses import dataclass from apischema import deserialize, deserializer, serialize, serializer from apischema.conversions import Conversion @dataclass class Foo: bar: int deserializer( lazy=lambda: Conversion(lambda bar: Foo(bar), source=int, target=Foo), target=Foo ) serializer( lazy=lambda: Conversion(lambda foo: foo.bar, source=Foo, target=int), source=Foo ) assert deserialize(Foo, 0) == Foo(0) assert serialize(Foo, Foo(0)) == 0
def __init__(self, value: Union[T, RecoverableRaw]): self._value = value @property def value(self) -> T: if isinstance(self._value, RecoverableRaw): raise self._value return self._value @value.setter def value(self, value: T): self._value = value deserializer(Recoverable) serializer(Recoverable.value) assert deserialize(Recoverable[int], 0).value == 0 with raises(RecoverableRaw) as err: _ = deserialize(Recoverable[int], "bad").value assert err.value.raw == "bad" assert serialize(Recoverable[int], Recoverable(0)) == 0 with raises(RecoverableRaw) as err: serialize(Recoverable[int], Recoverable(RecoverableRaw("bad"))) assert err.value.raw == "bad" assert (deserialization_schema(Recoverable[int]) == serialization_schema( Recoverable[int]) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "integer"
deserializer(Conversion(b64decode, source=str, target=bytes)) @serializer def to_base64(b: bytes) -> str: return b64encode(b).decode() type_name(graphql="Bytes")(bytes) schema(encoding="base64")(bytes) # ================ collections ================== deserializer(Conversion(deque, source=List[T], target=Deque[T])) serializer(Conversion(list, source=Deque[T], target=List[T])) if sys.version_info < (3, 7): deserializer(Conversion(deque, source=List, target=deque)) serializer(Conversion(list, source=deque, target=List)) # ================== datetime =================== if sys.version_info >= (3, 7): # pragma: no cover for cls, format in [(date, "date"), (datetime, "date-time"), (time, "time")]: deserializer(Conversion(cls.fromisoformat, source=str, target=cls)) # type: ignore serializer(Conversion(cls.isoformat, source=cls, target=str)) # type: ignore type_name(graphql=cls.__name__.capitalize())(cls) schema(format=format)(cls)
) def test_merge_results(results, origin, expected): assert list(merge_results( results, origin)) == [origin[tuple(exp)] for exp in expected] class Visitor(SerializationVisitor, WithConversionsResolver): def visit(self, tp: AnyType) -> Sequence[AnyType]: return self.resolve_conversion(tp) class A: pass serializer(Conversion(id, source=A, target=int)) tmp = None rec_conversion = Conversion(identity, A, Collection[A], LazyConversion(lambda: tmp)) tmp = rec_conversion @mark.parametrize( "tp, conversions, expected", [ (int, None, [int]), (int, Conversion(str, int), []), (List[int], None, [Collection[int]]), (List[int], Conversion(str, source=int), [Collection[str]]), (
from pytest import raises from apischema import ValidationError, deserialize, deserializer, serialize, serializer from apischema.json_schema import deserialization_schema, serialization_schema T = TypeVar("T") class Wrapper(Generic[T]): def __init__(self, wrapped: T): self.wrapped = wrapped # serializer methods of generic class are not handled in Python 3.6 def unwrap(self) -> T: return self.wrapped serializer(Wrapper.unwrap, Wrapper[T], T) deserializer(Wrapper, T, Wrapper[T]) assert deserialize(Wrapper[list[int]], [0, 1]).wrapped == [0, 1] with raises(ValidationError): deserialize(Wrapper[int], "wrapped") assert serialize(Wrapper("wrapped")) == "wrapped" assert ( deserialization_schema(Wrapper[int]) == {"$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "integer"} == serialization_schema(Wrapper[int]) )