Esempio n. 1
0
    def _build_union_des(self, context: BuildContext):
        func, annotation, namespace = (
            context.func,
            context.annotation,
            context.namespace,
        )
        # Get all types which we may coerce to.
        args = (*(a for a in annotation.args
                  if a not in {None, Ellipsis, type(None)}), )
        if not args:
            return
        # Add a type-check, but exclude str|bytes, since those are too permissive.
        types = {a for a in args if a not in {str, bytes}}
        if types:
            with func.b(f"if {self.VTYPE} in types:", types=types) as b:
                b.l(f"return {self.VNAME}")
        # Get all custom types, which may have discriminators
        targets = (*(a for a in args if not checks.isstdlibtype(a)), )
        # We can only build a tagged union deserializer if all args are valid
        if args != targets:
            return self._build_generic_union_des(context)

        # Try to collect the field which will be the discriminator.
        # First, get a mapping of Type -> Proto & Type -> Fields
        tagged = get_tag_for_types(targets)
        # Just bail out if we can't find a key.
        if not tagged:
            return self._build_generic_union_des(context)
        # If we got a key, re-map the protocols to the value for each type.
        deserializers = {
            value: self.resolver.resolve(t, namespace=namespace).transmute
            for value, t in tagged.types_by_values
        }
        # Finally, build the deserializer
        func.namespace.update(
            tag=tagged.tag,
            desers=deserializers,
            empty=_empty,
        )
        with func.b(f"if issubclass({self.VTYPE}, Mapping):",
                    Mapping=abc.Mapping) as b:
            b.l(f"tag_value = {self.VNAME}.get(tag, empty)")
        with func.b("else:") as b:
            b.l(f"tag_value = getattr({self.VNAME}, tag, empty)")
        with func.b("if tag_value in desers:") as b:
            b.l(f"{self.VNAME} = desers[tag_value]({self.VNAME})")
        with func.b("else:") as b:
            b.l("raise ValueError("
                'f"Value is missing field {tag!r} with one of '
                '{(*desers,)}: {val!r}"'
                ")")
Esempio n. 2
0
def get_tag_for_types(types: Tuple[Type, ...]) -> Optional[TaggedUnion]:
    if any(
        t in {None, ...} or not inspect.isclass(t) or checks.isstdlibtype(t)
        for t in types
    ):
        return None
    if len(types) > 1:
        root = types[0]
        root_hints = cached_type_hints(root)
        intersection = {*root_hints}
        fields_by_type = {root: root_hints}
        t: Type
        for t in types[1:]:
            hints = cached_type_hints(t)
            intersection &= hints.keys()
            fields_by_type[t] = hints
        tag = None
        literal = False
        # If we have an intersection, check if it's constant value we can use
        # TODO: This won't support Generics in this state.
        #  We don't support generics yet (#119), but when we do,
        #  we need to add a branch for tagged unions from generics.
        while intersection and tag is None:
            f = intersection.pop()
            v = getattr(root, f, empty)
            if v is not empty and not isinstance(v, MemberDescriptorType):
                tag = f
                continue
            rhint = root_hints[f]
            if checks.isliteral(rhint):
                tag, literal = f, True
        if tag:
            if literal:
                tbv = (
                    *((a, t) for t in types for a in get_args(fields_by_type[t][tag])),
                )
            else:
                tbv = (*((getattr(t, tag), t) for t in types),)
            return TaggedUnion(
                tag=tag, types=types, isliteral=literal, types_by_values=tbv
            )
    return None
Esempio n. 3
0
 def _build_union_des(self, func: gen.Block, annotation: "Annotation",
                      namespace):
     # Get all types which we may coerce to.
     args = (*(a for a in annotation.args
               if a not in {None, Ellipsis, type(None)}), )
     # Get all custom types, which may have discriminators
     targets = (*(a for a in args if not checks.isstdlibtype(a)), )
     # We can only build a tagged union deserializer if all args are valid
     if args and args == targets:
         # Try to collect the field which will be the discriminator.
         # First, get a mapping of Type -> Proto & Type -> Fields
         tagged = get_tag_for_types(targets)
         # Just bail out if we can't find a key.
         if not tagged:
             func.l("# No-op, couldn't locate a discriminator key.")
             return
         # If we got a key, re-map the protocols to the value for each type.
         deserializers = {
             value: self.resolver.resolve(t, namespace=namespace)
             for value, t in tagged.types_by_values
         }
         # Finally, build the deserializer
         func.namespace.update(
             tag=tagged.tag,
             desers=deserializers,
             empty=_empty,
         )
         with func.b(f"if issubclass({self.VTYPE}, Mapping):",
                     Mapping=abc.Mapping) as b:
             b.l(f"tag_value = {self.VNAME}.get(tag, empty)")
         with func.b("else:") as b:
             b.l(f"tag_value = getattr({self.VNAME}, tag, empty)")
         with func.b("if tag_value in desers:") as b:
             b.l(f"{self.VNAME} = desers[tag_value].transmute({self.VNAME})"
                 )
         with func.b("else:") as b:
             b.l("raise ValueError("
                 'f"Value is missing field {tag!r} with one of '
                 '{(*desers,)}: {val!r}"'
                 ")")
Esempio n. 4
0
    def annotation(
        self,
        annotation: Type[ObjectT],
        name: str = None,
        parameter: Optional[inspect.Parameter] = None,
        is_optional: bool = None,
        is_strict: StrictModeT = None,
        flags: "SerdeFlags" = None,
        default: Any = EMPTY,
        namespace: Type = None,
    ) -> AnnotationT:
        """Get a :py:class:`Annotation` for this type.

        Unlike a :py:class:`ResolvedAnnotation`, this does not provide access to a
        serializer/deserializer/validator protocol.
        """
        flags = cast(
            "SerdeFlags",
            getattr(annotation, SERDE_FLAGS_ATTR, flags or SerdeFlags()))
        if parameter is None:
            parameter = inspect.Parameter(
                name or "_",
                inspect.Parameter.POSITIONAL_OR_KEYWORD,
                annotation=annotation,
                default=default if checks.ishashable(default) else ...,
            )
        # Check for the super-type
        non_super = util.resolve_supertype(annotation)
        # Note, this may be a generic, like Union.
        orig = util.origin(annotation)
        use = non_super
        # Get the unfiltered args
        args = getattr(non_super, "__args__", None)
        # Set whether this is optional/strict
        is_optional = (is_optional or checks.isoptionaltype(non_super)
                       or parameter.default in self.OPTIONALS)
        is_strict = is_strict or checks.isstrict(non_super) or self.STRICT
        is_static = util.origin(use) not in self._DYNAMIC
        is_literal = checks.isliteral(use)
        # Determine whether we should use the first arg of the annotation
        while checks.should_unwrap(use) and args:
            is_optional = is_optional or checks.isoptionaltype(use)
            is_strict = is_strict or checks.isstrict(use)
            if is_optional and len(args) > 2:
                # We can't resolve this annotation.
                is_static = False
                use = Union[args[:-1]]
                break
            # Note that we don't re-assign `orig`.
            # This is intentional.
            # Special forms are needed for building the downstream validator.
            # Callers should be aware of this and perhaps use `util.origin` elsewhere.
            non_super = util.resolve_supertype(args[0])
            use = non_super
            args = util.get_args(use)
            is_static = util.origin(use) not in self._DYNAMIC
            is_literal = is_literal or checks.isliteral(use)

        # Only allow legal parameters at runtime, this has implementation implications.
        if is_literal:
            args = util.get_args(use)
            if any(not isinstance(a, self.LITERALS) for a in args):
                raise TypeError(
                    f"PEP 586: Unsupported parameters for 'Literal' type: {args}. "
                    "See https://www.python.org/dev/peps/pep-0586/"
                    "#legal-parameters-for-literal-at-type-check-time "
                    "for more information.")
        # The type definition doesn't exist yet.
        if use.__class__ is ForwardRef:
            module, localns = self.__module__, {}
            # Ideally we have a namespace from a parent class/function to the field
            if namespace:
                module = namespace.__module__
                localns = getattr(namespace, "__dict__", {})

            return ForwardDelayedAnnotation(
                ref=use,
                resolver=self,
                _name=name,
                parameter=parameter,
                is_optional=is_optional,
                is_strict=is_strict,
                flags=flags,
                default=default,
                module=module,
                localns=localns,
            )
        # The type definition is recursive or within a recursive loop.
        elif use is namespace or use in self.__stack:
            # If detected via stack, we can remove it now.
            # Otherwise we'll cause another recursive loop.
            if use in self.__stack:
                self.__stack.remove(use)
            return DelayedAnnotation(
                type=use,
                resolver=self,
                _name=name,
                parameter=parameter,
                is_optional=is_optional,
                is_strict=is_strict,
                flags=flags,
                default=default,
            )
        # Otherwise, add this type to the stack to prevent a recursive loop from elsewhere.
        if not checks.isstdlibtype(use):
            self.__stack.add(use)
        serde = (self._get_configuration(util.origin(use), flags)
                 if is_static and not is_literal else SerdeConfig(flags))

        anno = Annotation(
            resolved=use,
            origin=orig,
            un_resolved=annotation,
            parameter=parameter,
            optional=is_optional,
            strict=is_strict,
            static=is_static,
            serde=serde,
        )
        anno.translator = functools.partial(self.translator.factory,
                                            anno)  # type: ignore
        return anno