def test_raw_with_kwargs(): deserializer(to_raw_deserializer(int_dict)) assert deserialize(IntDict, {"mandatory": 42, "a": 0}) == IntDict({"a": 0}) with raises(ValidationError): deserialize(IntDict, {"mandatory": 42, "a": "0"}) with raises(ValidationError): deserialize(IntDict, {"a": 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) )
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 test_raw(): deserializer(to_raw_deserializer(sfx_version)) assert deserialize(SuffixedVersion, {"version": 42}) == "42" assert deserialize(SuffixedVersion, { "version": 42, "suffix": "ok" }) == "42ok" deserializer(to_raw_deserializer(sfx_version)) with raises(ValidationError): deserialize(SuffixedVersion, {"version": "42"})
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 __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) # Register subclasses' conversion in __init_subclass__ deserializer(Conversion(cls.deserialize, target=cls))
from apischema import deserialize, deserializer, type_name from apischema.json_schema import deserialization_schema from apischema.objects import object_deserialization def create_range(start: int, stop: int, step: int = 1) -> range: return range(start, stop, step) range_conv = object_deserialization(create_range, type_name("Range")) # Conversion can be registered deserializer(range_conv) assert deserialize(range, {"start": 0, "stop": 10}) == range(0, 10) assert deserialization_schema(range) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": { "start": { "type": "integer" }, "stop": { "type": "integer" }, "step": { "type": "integer", "default": 1 }, }, "required": ["start", "stop"], "additionalProperties": False, }
def make_entity_class(definition: Definition, support: Support) -> Type[Entity]: """ We can get a set of Definitions by deserializing an ibek support module definition YAML file. This function then creates an Entity derived class from each Definition. See :ref:`entities` """ fields: List[Tuple[str, type, Field[Any]]] = [] # add in each of the arguments for arg in definition.args: # make_dataclass can cope with string types, so cast them here rather # than lookup metadata: Any = None arg_type: type if isinstance(arg, ObjectArg): def lookup_instance(id): try: return id_to_entity[id] except KeyError: raise ValidationError( f"{id} is not in {list(id_to_entity)}") metadata = conversion(deserialization=Conversion( lookup_instance, str, Entity)) | schema( extra={"vscode_ibek_plugin_type": "type_object"}) arg_type = Entity elif isinstance(arg, IdArg): arg_type = str metadata = schema(extra={"vscode_ibek_plugin_type": "type_id"}) else: # arg.type is str, int, float, etc. arg_type = getattr(builtins, arg.type) if arg.description: arg_type = A[arg_type, desc(arg.description)] if arg.default is Undefined: fld = field(metadata=metadata) else: fld = field(metadata=metadata, default=arg.default) fields.append((arg.name, arg_type, fld)) # put the literal name in as 'type' for this Entity this gives us # a unique key for each of the entity types we may instantiate full_name = f"{support.module}.{definition.name}" fields.append( ("type", Literal[full_name], field(default=cast(Any, full_name)))) # add a field so we can control rendering of the entity without having to delete # it fields.append(("entity_enabled", bool, field(default=cast(Any, True)))) namespace = dict(__definition__=definition) # make the Entity derived dataclass for this EntityClass, with a reference # to the Definition that created it entity_cls = make_dataclass(full_name, fields, bases=(Entity, ), namespace=namespace) deserializer(Conversion(identity, source=entity_cls, target=Entity)) return entity_cls
@dataclass class Foo: value: int @classmethod def deserialize(cls: type[Foo_], value: int) -> Foo_: return cls(value) def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) # Register subclasses' conversion in __init_subclass__ deserializer(Conversion(cls.deserialize, target=cls)) # Register main conversion after the class definition deserializer(Conversion(Foo.deserialize, target=Foo)) class Bar(Foo): pass assert deserialize(Foo, 0) == Foo(0) assert deserialize(Bar, 0) == Bar(0) # For external types (defines in imported library) @dataclass class ForeignType: value: int
@dataclass class Foo(Base): foo: int @dataclass class Bar(Base): bar: str @deserializer def from_foo(foo: Foo) -> Base: return foo deserializer(identity, Bar, Base) # Roughly equivalent to # @deserializer # def from_bar(bar: Bar) -> Base: # return bar # but identity is optimized by Apischema # You can even add deserializers which are not subclass @deserializer def from_list_of_int(ints: list[int]) -> Base: return Base() assert deserialization_schema(Base) == { "anyOf": [ {
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]) )
from base64 import b64decode from apischema import deserialize, deserializer from apischema.conversions import Conversion deserializer(Conversion(b64decode, source=str, target=bytes)) # Roughly equivalent to: # def decode_bytes(source: str) -> bytes: # return b64decode(source) # but saving a function call assert deserialize(bytes, "Zm9v") == b"foo"
def __init_subclass__(cls): # Deserializers stack directly as a Union deserializer(Conversion(identity, source=cls, target=Arg))
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
from typing import Annotated, Any, Generic, TypeVar, Union from pytest import raises from apischema import deserialize, deserializer, serialize, serializer from apischema.json_schema import deserialization_schema, serialization_schema from apischema.skip import Skip class RecoverableRaw(Exception): def __init__(self, raw): self.raw = raw deserializer(RecoverableRaw, Any, RecoverableRaw) T = TypeVar("T") class Recoverable(Generic[T]): def __init__(self, value: T): 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):
from pytest import raises from apischema import deserialize, deserializer, schema, serialize, serializer from apischema.json_schema import deserialization_schema, serialization_schema # Add a dummy placeholder comment in order to not have an empty schema # (because Union member with empty schema would "contaminate" whole Union schema) @schema(extra={"$comment": "recoverable"}) class RecoverableRaw(Exception): def __init__(self, raw: Any): self.raw = raw deserializer(RecoverableRaw) T = TypeVar("T") def remove_recoverable_schema(json_schema: Dict[str, Any]): if "anyOf" in json_schema: # deserialization schema value_schema, recoverable_comment = json_schema.pop("anyOf") assert recoverable_comment == {"$comment": "recoverable"} json_schema.update(value_schema) @schema(extra=remove_recoverable_schema) class Recoverable(Generic[T]): def __init__(self, value: Union[T, RecoverableRaw]): self._value = value
PurePath, PurePosixPath, PureWindowsPath, WindowsPath, ) from typing import Deque, List, TypeVar from uuid import UUID from apischema import deserializer, schema, serializer, type_name from apischema.conversions import Conversion, as_str T = TypeVar("T") # =================== bytes ===================== 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):