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}"' ")")
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
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}"' ")")
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