Exemple #1
0
 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,
         )
Exemple #2
0
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"})
Exemple #3
0
 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
Exemple #4
0
 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
Exemple #5
0
 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)
Exemple #6
0
    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
Exemple #8
0
    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
Exemple #9
0
 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)
Exemple #13
0
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)
Exemple #15
0
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)