def _add_type_check(self, func: gen.Function, annotation: Annotation): resolved_name = util.get_name(annotation.resolved) func.l(f"{self._FNAME} = name or {resolved_name!r}") line = "if not tcheck(o.__class__, t):" check: Callable[..., bool] = util.cached_issubclass t = annotation.generic if checks.isbuiltinsubtype(annotation.generic): line = "if not tcheck(o, t):" check = isinstance # type: ignore if checks.istypeddict(t): t = dict with func.b(line, tcheck=check) as b: msg = ( f"{{{self._FNAME}}}: type {{inst_tname!r}} " f"is not a subtype of type " f"{util.get_qualname(annotation.generic)!r}. " f"Perhaps this annotation should be " f"Union[{{inst_tname}}, {util.get_qualname(annotation.generic)}]?" ) b.l("inst_tname = qualname(o.__class__)") b.l( f"raise err(f{msg!r})", err=SerializationValueError, qualname=util.get_qualname, t=t, )
def isoptionaltype(obj: Type[ObjectT]) -> TypeGuard[Optional]: """Test whether an annotation is :py:class`typing.Optional`, or can be treated as. :py:class:`typing.Optional` is an alias for `typing.Union[<T>, None]`, so both are "optional". Parameters ---------- obj Examples -------- >>> import typic >>> from typing import Optional, Union, Dict >>> typic.isoptionaltype(Optional[str]) True >>> typic.isoptionaltype(Union[str, None]) True >>> typic.isoptionaltype(Dict[str, None]) False """ args = getattr(obj, "__args__", ()) return (len(args) > 1 and args[-1] in { type(None), None, } # noqa: E721 - we don't know what args[-1] is, so this is safer and util.get_name(util.origin(obj)) in {"Optional", "Union", "Literal"})
def _handle_union( self, anno: Annotation, ro: Optional[bool], wo: Optional[bool], name: Optional[str], parent: Optional[Type], ): fields: List[SchemaFieldT] = [] args = get_args(anno.un_resolved) for t in args: if t.__class__ is ForwardRef or t is parent: n = name or get_name(t) fields.append(Ref(f"#/definitions/{n}")) continue fields.append( self.get_field(resolver.resolve(t, namespace=parent), parent=parent)) schema = self._check_optional( anno, MultiSchemaField( title=name and self.defname(anno.resolved, name=name), anyOf=(*fields, ), ), ro, wo, name, ) self.__cache[anno] = schema return schema
def _build_generic_union_des(self, context: BuildContext): annotation, namespace, func = ( context.annotation, context.namespace, context.func, ) annos = { get_name(a): self.resolver.resolve(a, namespace=namespace) for a in annotation.args if a not in {None, Ellipsis, type(None)} } if annos: desers = {f"{n}_des": p.transmute for n, p in annos.items()} types = {n: p.annotation.resolved_origin for n, p in annos.items()} ctx: Mapping[str, Union[Type, DeserializerT]] = {**types, **desers} for name in annos: # Can't do subclass checks with these... if name in {"Literal", "Final"}: continue with func.b(f"if issubclass({name}, {self.VTYPE}):") as b: b.l(f"return {name}_des({self.VNAME})") for name in desers: with func.b("try:") as b: b.l(f"return {name}({self.VNAME})") with func.b("except (TypeError, ValueError, KeyError):") as b: b.l("pass") func.namespace.update(ctx) func.l( "raise ValueError(" f'f"Value could not be deserialized into one of {(*annos,)}: {{val!r}}"' ")", ) return False
def _compile_iterable_translator(self, source: Type, target: Type) -> TranslatorT: func_name = self._get_name(source, target) target_name = get_name(target) oname = "o" ismapping = ismappingtype(target) iterator = self.iterator(source, not ismapping) ctx = {"iterator": iterator, target_name: target} with Block(ctx) as main: with main.f(func_name, Block.p(oname)) as func: retval = f"iterator({oname})" if not isiteratortype(target): retval = f"{target_name}({retval})" func.l(f"{Keyword.RET} {retval}") return main.compile(name=func_name)
def register(self, t: Type[_ET], *, name: str = None): """Register a handler for the target type `t`.""" name = name or get_name(t) if name in self.__dict__: return self.__dict__[name] if not inspect.isclass(t): raise EnvironmentTypeError( f"Can't coerce to target {name!r} with t: {t!r}.") from None def get(var: str, *, ci: bool = True): return self.getenv(var, t=t, ci=ci) setattr(self, name, get) return get
def _get_serializer_proto(self, t: Type) -> SerdeProtocol: tname = util.get_name(t) if hasattr(t, SERDE_ATTR): return getattr(t, SERDE_ATTR) for attr, caller in self._DICT_FACTORY_METHODS: if hasattr(t, attr): def serializer( val, lazy: bool = False, name: util.ReprT = None, *, __prim=self.primitive, __call=caller, __tname=tname, __repr=util.joinedrepr, ): name = name or __tname return { __prim(x): __prim(y, lazy=lazy, name=__repr(name, x)) for x, y in __call(val).items() } return SerdeProtocol( self.annotation(t), # type: ignore deserializer=None, serializer=serializer, constraints=None, validator=None, ) if checks.ismappingtype(t): t = Mapping[Any, Any] elif checks.iscollectiontype(t) and not issubclass( t, (str, bytes, bytearray)): t = Iterable[Any] settings = getattr(t, SERDE_FLAGS_ATTR, None) serde: SerdeProtocol = self.resolve(t, flags=settings, _des=False) return serde
def register(self, t: Type[_ET], *aliases: str, name: str = None): """Register a handler for the target type `t`.""" anno = self.resolver.annotation(t) if isinstance( anno, (common.ForwardDelayedAnnotation, common.DelayedAnnotation) ): anno = anno.resolved.annotation # type: ignore name = name or get_name(anno.resolved) if name in self.__dict__: return self.__dict__[name] if not inspect.isclass(anno.resolved): raise EnvironmentTypeError( f"Can't coerce to target {name!r} with t: {t!r}." ) from None def get(var: str, *, ci: bool = True, default: _ET = ...): # type: ignore return self.getenv(var, default, *aliases, t=t, ci=ci) setattr(self, name, get) return get
def _build_key_serializer(self, name: str, kser: SerializerT, annotation: "Annotation") -> SerializerT: kser_name = util.get_name(kser) # Build the namespace ns: Dict[str, Any] = { kser_name: kser, } kvar = "k_" with gen.Block(ns) as main: with main.f(name, main.param(kvar), main.param("lazy", default=False)) as kf: k = f"{kser_name}({kvar})" # If there are args & field mapping, get the correct field name # AND serialize the key. if annotation.serde.fields_out: ns["fields_out"] = annotation.serde.fields_out k = f"{kser_name}(fields_out.get({kvar}, {kvar}))" # If there are only serializers, get the serialized value if annotation.serde.flags.case: ns.update(case=annotation.serde.flags.case.transformer) k = f"case({k})" kf.l(f"{gen.Keyword.RET} {k}") return main.compile(name=name, ns=ns)
class Resolver: """A type serializer/deserializer resolver.""" STRICT = st.STRICT_MODE _DICT_FACTORY_METHODS = frozenset({("asdict", methodcaller("asdict")), ("to_dict", methodcaller("to_dict"))}) _DYNAMIC = SerFactory._DYNAMIC OPTIONALS = (None, ...) LITERALS = (int, bytes, str, bool, Enum, type(None)) def __init__(self): self.des = DesFactory(self) self.ser = SerFactory(self) self.binder = Binder(self) self.translator = TranslatorFactory(self) self.bind = self.binder.bind self.__cache = {} self.__stack = set() for typ in checks.STDLIB_TYPES: self.resolve(typ) self.resolve(Optional[typ]) self.resolve(typ, is_optional=True) try: self.translator.iterator(typ) self.translator.iterator(typ, values=True) except TypeError: pass def transmute(self, annotation: Type[ObjectT], value: Any) -> ObjectT: """Convert a given value `into` the target annotation. Checks for: - :class:`datetime.date` - :class:`datetime.datetime` - builtin types - extended type annotations as described in the ``typing`` module. - User-defined classes (limited) Parameters ---------- annotation : The provided annotation for determining the coercion value : The value to be transmuted """ resolved: SerdeProtocol = self.resolve(annotation) transmuted: ObjectT = resolved.transmute(value) # type: ignore return transmuted def translate(self, value: ObjectT, target: Type[_T]) -> _T: """Translate an instance `from` its type `to` a target type. Notes ----- This provides a functional interface for translating one custom class instance to another custom class. This should not be confused with :py:func:`typic.transmute`, which is generally a more powerful functional interface for conversion between types, but this is provided as for api-completeness with the object-api. Parameters ---------- value The higher-order class instance to translate. target The higher-order class to translate into. """ resolved: SerdeProtocol = self.resolve(type(value)) return resolved.translate(value, target) # type: ignore def validate(self, annotation: Type[ObjectT], value: Any, *, transmute: bool = False) -> Union[ObjectT, Any]: """Validate an input against the type-constraints for the given annotation. Parameters ---------- annotation The type or annotation to validate against value The value to check transmute: (kw-only) Whether to transmute the value to the annotation after validation """ resolved: SerdeProtocol = self.resolve(annotation) value = resolved.validate(value) if transmute: return resolved.transmute(value) # type: ignore return value def iterate(self, obj, *, values: bool = False) -> Iterator[Union[Tuple[str, Any], Any]]: """Iterate over the fields of an object. Parameters ---------- obj The object to iterate over values Whether to only yield values of an object's fields. (defaults False) """ t = obj.__class__ # Extract the type of the enum value if this is an Enum. # Enums classes are iterable and will generate the wrong kind of iterator. if checks.isenumtype(t): obj = obj.value t = obj.__class__ iterator = self.translator.iterator(t, values=values) return iterator(obj) def coerce_value(self, value: Any, annotation: Type[ObjectT]) -> ObjectT: # pragma: nocover warnings.warn( "'typic.coerce' has been deprecated and will be removed in a future " "version. Use 'typic.transmute' instead.", DeprecationWarning, stacklevel=3, ) return self.transmute(annotation, value) def known(self, t: Type) -> bool: return hasattr(t, ORIG_SETTER_NAME) or hasattr(t, "__delayed__") def delayed(self, t: Type) -> bool: return getattr(t, "__delayed__", False) @lru_cache(maxsize=None) def _get_serializer_proto(self, t: Type) -> SerdeProtocol: tname = util.get_name(t) if hasattr(t, SERDE_ATTR): return getattr(t, SERDE_ATTR) for attr, caller in self._DICT_FACTORY_METHODS: if hasattr(t, attr): def serializer( val, lazy: bool = False, name: util.ReprT = None, *, __prim=self.primitive, __call=caller, __tname=tname, __repr=util.joinedrepr, ): name = name or __tname return { __prim(x): __prim(y, lazy=lazy, name=__repr(name, x)) for x, y in __call(val).items() } return SerdeProtocol( self.annotation(t), # type: ignore deserializer=None, serializer=serializer, constraints=None, validator=None, ) if checks.ismappingtype(t): t = Mapping[Any, Any] elif checks.iscollectiontype(t) and not issubclass( t, (str, bytes, bytearray)): t = Iterable[Any] settings = getattr(t, SERDE_FLAGS_ATTR, None) serde: SerdeProtocol = self.resolve(t, flags=settings, _des=False) return serde def primitive(self, obj: Any, lazy: bool = False, name: util.ReprT = None) -> Any: """A method for converting an object to its primitive equivalent. Useful for encoding data to JSON. Examples -------- >>> import typic >>> import datetime >>> import uuid >>> import ipaddress >>> import re >>> import dataclasses >>> typic.primitive("foo") 'foo' >>> typic.primitive(("foo",)) # containers are converted to lists/dicts ['foo'] >>> typic.primitive(datetime.datetime(1970, 1, 1)) '1970-01-01T00:00:00' >>> typic.primitive(b"foo") 'foo' >>> typic.primitive(ipaddress.IPv4Address("0.0.0.0")) '0.0.0.0' >>> typic.primitive(re.compile("[0-9]")) '[0-9]' >>> typic.primitive(uuid.UUID(int=0)) '00000000-0000-0000-0000-000000000000' >>> @dataclasses.dataclass ... class Foo: ... bar: str = 'bar' ... >>> typic.primitive(Foo()) {'bar': 'bar'} """ t = obj.__class__ if checks.isenumtype(t): obj = obj.value t = obj.__class__ proto: SerdeProtocol = self._get_serializer_proto(t) return proto.primitive(obj, lazy=lazy, name=name) # type: ignore def tojson(self, obj: Any, *, indent: int = 0, ensure_ascii: bool = False, **kwargs) -> str: """A method for dumping any object to a valid JSON string. Notes ----- If `ujson` is installed, we will default to that library for the final encoding, which can result in massive performance gains over the standard `json` library. Examples -------- >>> import typic >>> import datetime >>> import uuid >>> import ipaddress >>> import re >>> import dataclasses >>> import enum >>> typic.tojson("foo") '"foo"' >>> typic.tojson(("foo",)) '["foo"]' >>> typic.tojson(datetime.datetime(1970, 1, 1)) '"1970-01-01T00:00:00"' >>> typic.tojson(b"foo") '"foo"' >>> typic.tojson(ipaddress.IPv4Address("0.0.0.0")) '"0.0.0.0"' >>> typic.tojson(re.compile("[0-9]")) '"[0-9]"' >>> typic.tojson(uuid.UUID(int=0)) '"00000000-0000-0000-0000-000000000000"' >>> @dataclasses.dataclass ... class Foo: ... bar: str = 'bar' ... >>> typic.tojson(Foo()) '{"bar":"bar"}' >>> class Enum(enum.Enum): ... FOO = "foo" ... >>> typic.tojson(Enum.FOO) '"foo"' """ t = obj.__class__ if checks.isenumtype(t): obj = obj.value t = obj.__class__ proto: SerdeProtocol = self._get_serializer_proto(t) return proto.tojson(obj, indent=indent, ensure_ascii=ensure_ascii, **kwargs) @lru_cache(maxsize=None) def _get_configuration(self, origin: Type, flags: "SerdeFlags") -> "SerdeConfig": if hasattr(origin, SERDE_FLAGS_ATTR): flags = getattr(origin, SERDE_FLAGS_ATTR) # Get all the annotated fields params = util.safe_get_params(origin) # This is probably a builtin and has no signature fields: Dict[str, Union[Annotation, DelayedAnnotation, ForwardDelayedAnnotation]] = {} hints = util.cached_type_hints(origin) for name, t in hints.items(): fields[name] = self.annotation( t, flags=dataclasses.replace(flags, fields={}), default=getattr(origin, name, EMPTY), namespace=origin, ) # Filter out any annotations which aren't part of the object's signature. if flags.signature_only: fields = {x: fields[x] for x in fields.keys() & params.keys()} # Create a field-to-field mapping fields_out = {x: x for x in fields} # Make sure to include any fields explicitly listed include = flags.fields if include: if isinstance(include, Mapping): fields_out.update(include) else: fields_out.update({x: x for x in include}) # Transform the output fields to the correct case. if flags.case: case = Case(flags.case) fields_out = { x: case.transformer(y) for x, y in fields_out.items() } omit = flags.omit # Omit fields with explicitly omitted types & flag values to omit at dump value_omissions: Tuple[Any, ...] = () if omit: type_omissions = { o for o in omit if checks._type_check(o) or o is NotImplemented } type_name_omissions = {util.get_name(o) for o in type_omissions} value_omissions = (*(o for o in omit if o not in type_omissions), ) fields_out_final = {} anno: Union[Annotation, DelayedAnnotation, ForwardDelayedAnnotation] for name, out in fields_out.items(): anno = fields[name] default = anno.parameter.default if anno.parameter else EMPTY if isinstance(anno, ForwardDelayedAnnotation): if (not {util.get_name(anno.ref), util.get_name(default)} & type_name_omissions): fields_out_final[name] = out elif not {anno.origin, default} & type_omissions: fields_out_final[name] = out fields_out = fields_out_final fields_in = {y: x for x, y in fields_out.items()} if params: fields_in = {x: y for x, y in fields_in.items() if y in params} exclude = flags.exclude if exclude: fields_out = { x: y for x, y in fields_out.items() if x not in exclude } fields_getters = {x: attrgetter(x) for x in fields} return SerdeConfig( flags=flags, fields=fields, fields_out=fields_out, fields_in=fields_in, fields_getters=fields_getters, omit_values=value_omissions, )
def name(self) -> str: return util.get_name(self.type)
def name(self) -> str: return util.get_name(self.ref)
def isuniontype(obj: Type[ObjectT]) -> TypeGuard[Union]: return util.get_name(util.origin(obj)) in {"Union", "UnionType"}
def type_name(self): return util.get_name(self.ttype)
def _from_class( t: Type[VT], *, nullable: bool = False, name: str = None, cls: Type = None ) -> ConstraintsProtocolT[VT]: if not istypeddict(t) and not isnamedtuple(t) and isbuiltinsubtype(t): return cast( ConstraintsProtocolT, _from_strict_type(t, nullable=nullable, name=name) ) try: params: Dict[str, inspect.Parameter] = {**cached_signature(t).parameters} hints = cached_type_hints(t) for x in hints.keys() & params.keys(): p = params[x] params[x] = inspect.Parameter( p.name, p.kind, default=p.default, annotation=hints[x] ) for x in hints.keys() - params.keys(): hint = hints[x] if not isclassvartype(hint): continue # Hack in the classvars as "parameters" to allow for validation. default = getattr(t, x, empty) args = get_args(hint) if not args: hint = ClassVar[default.__class__] # type: ignore params[x] = inspect.Parameter( x, inspect.Parameter.KEYWORD_ONLY, default=default, annotation=hint ) except (ValueError, TypeError): return cast( ConstraintsProtocolT, _from_strict_type(t, nullable=nullable, name=name) ) name = name or get_name(t) items: Optional[frozendict.FrozenDict[Hashable, ConstraintsT]] = ( frozendict.FrozenDict(_resolve_params(t, **params)) or None ) required = frozenset( ( pname for pname, p in params.items() if ( p.kind not in {p.VAR_POSITIONAL, p.VAR_KEYWORD} and p.default is p.empty ) ) ) has_varargs = any( p.kind in {p.VAR_KEYWORD, p.VAR_POSITIONAL} for p in params.values() ) kwargs = { "type": t, "nullable": nullable, "name": name, "required_keys": required, "items": items, "total": not has_varargs, } cls = ObjectConstraints if istypeddict(t): cls = TypedDictConstraints kwargs.update(type=dict, ttype=t, total=getattr(t, "__total__", bool(required))) c = cls(**kwargs) # type: ignore return cast(ConstraintsProtocolT, c)