Пример #1
0
 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)
     )
Пример #2
0
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)
Пример #3
0
    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,
            ))
Пример #4
0
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
Пример #5
0
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
Пример #6
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"
Пример #7
0
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)
Пример #8
0
)
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]]),
        (
Пример #9
0
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])
)