def copy_union_with( types: Tuple[Type, ...], params_to_type: Dict[Type, Union[Type, StrawberryUnion]] = None, description=None, ) -> StrawberryUnion: types = cast( Tuple[Type, ...], tuple(copy_type_with(t, params_to_type=params_to_type) for t in types), ) return union( name=get_name_from_types(types), types=types, description=description, )
def test_strawberry_union(): @strawberry.type class User: name: str @strawberry.type class Error: name: str cool_union = union(name="CoolUnion", types=(User, Error)) annotation = StrawberryAnnotation(cool_union) resolved = annotation.resolve() assert isinstance(resolved, StrawberryUnion) assert resolved.types == (User, Error) assert resolved == StrawberryUnion( name="CoolUnion", type_annotations=(StrawberryAnnotation(User), StrawberryAnnotation(Error)), ) assert resolved != Union[User, Error] # Name will be different
def resolve_type( field_definition: Union[FieldDefinition, ArgumentDefinition]) -> None: # convert a python type to include a strawberry definition, so for example # Union becomes a class with a UnionDefinition, Generics become an actual # type definition. This helps with making the code to convert the type definitions # to GraphQL types, as we only have to deal with Python's typings in one place. type = cast(Type, field_definition.type) origin_name = cast(str, field_definition.origin_name) if isinstance(type, LazyType): field_definition.type = type.resolve_type() if isinstance(type, str): module = sys.modules[field_definition.origin.__module__].__dict__ type = eval(type, module) field_definition.type = type if is_forward_ref(type): # if the type is a forward reference we try to resolve the type by # finding it in the global namespace of the module where the field # was initially declared. This will break when the type is not declared # in the main scope, but we don't want to support that use case # see https://mail.python.org/archives/list/[email protected]/thread/SNKJB2U5S74TWGDWVD6FMXOP63WVIGDR/ # noqa: E501 type_name = type.__forward_arg__ module = sys.modules[field_definition.origin.__module__] # TODO: we should probably raise an error if we can't find the type type = module.__dict__[type_name] field_definition.type = type return if is_async_generator(type): # TODO: shall we raise a warning if field is not used in a subscription? # async generators are used in subscription, we only need the yield type # https://docs.python.org/3/library/typing.html#typing.AsyncGenerator field_definition.type = get_async_generator_annotation(type) return resolve_type(field_definition) # check for Optional[A] which is represented as Union[A, None], we # have an additional check for proper unions below if is_optional(type) and len(type.__args__) == 2: # this logics works around List of optionals and Optional lists of Optionals: # >>> Optional[List[Str]] # >>> Optional[List[Optional[Str]]] # the field is only optional if it is not a list or if it was already optional # since we mark the child as optional when the field is a list field_definition.is_optional = (True and not field_definition.is_list or field_definition.is_optional) field_definition.is_child_optional = field_definition.is_list field_definition.type = get_optional_annotation(type) return resolve_type(field_definition) elif is_list(type): # TODO: maybe this should be an argument definition when it is argument # but doesn't matter much child_definition = FieldDefinition( origin=field_definition.origin, # type: ignore name=None, origin_name=None, type=get_list_annotation(type), ) resolve_type(child_definition) field_definition.type = None field_definition.is_list = True field_definition.child = child_definition return # case for Union[A, B, C], it also handles Optional[Union[A, B, C]] as optionals # type hints are represented as Union[..., None]. elif is_union(type): # Optional[Union[A, B]] is represented as Union[A, B, None] so we need # too check again if the field is optional as the check above only checks # for single Optionals field_definition.is_optional = is_optional(type) types = type.__args__ # we use a simplified version of resolve_type since unions in GraphQL # are simpler and cannot contain lists or optionals types = tuple( _resolve_generic_type(t, origin_name) for t in types if t is not None.__class__) field_definition.is_union = True field_definition.type = union(get_name_from_types(types), types) # case for Type[A], we want to convert generics to have the concrete types # when we pass them, so that we don't have to deal with generics when # generating the GraphQL types later on. elif hasattr(type, "_type_definition") and type._type_definition.is_generic: args = get_args(type) # raise an error when using generics without passing any type parameter, ie: # >>> class X(Generic[T]): ... # >>> a: X # instead of # >>> a: X[str] if len(args) == 0: name = cast(str, field_definition.origin_name) raise MissingTypesForGenericError(name, type) # we only make a copy when all the arguments are not type vars if not all(is_type_var(a) for a in args): field_definition.type = copy_type_with(type, *args) if isinstance(type, StrawberryUnion): field_definition.is_union = True
def copy_type_with( base: Type, *types: Type, params_to_type: Dict[Type, Type] = None ) -> Type: if params_to_type is None: params_to_type = {} if hasattr(base, "_union_definition"): types = cast( Tuple[Type], tuple( copy_type_with(t, params_to_type=params_to_type) for t in base._union_definition.types ), ) return union( name=get_name_from_types(types), types=types, description=base._union_definition.description, ) if hasattr(base, "_type_definition"): definition = cast(TypeDefinition, base._type_definition) if definition.type_params: fields = [] type_params = definition.type_params.values() params_to_type.update(dict(zip(type_params, types))) name = get_name_from_types(params_to_type.values()) + definition.name for field in definition.fields: kwargs = dataclasses.asdict(field) if field.is_list: child = cast(FieldDefinition, field.child) child_type = cast(Type, child.type) # TODO: nested list kwargs["child"] = FieldDefinition( name=child.name, origin=child.origin, origin_name=child.origin_name, is_optional=child.is_optional, type=copy_type_with(child_type, params_to_type=params_to_type), ) else: field_type = cast(Type, field.type) kwargs["type"] = copy_type_with( field_type, params_to_type=params_to_type ) federation_args = kwargs.pop("federation") kwargs["federation"] = FederationFieldParams(**federation_args) fields.append(FieldDefinition(**kwargs)) type_definition = TypeDefinition( name=name, is_input=definition.is_input, origin=definition.origin, is_interface=definition.is_interface, is_generic=False, federation=definition.federation, interfaces=definition.interfaces, description=definition.description, _fields=fields, ) type_definition._type_params = {} copied_type = builtins.type( name, (), {"_type_definition": type_definition}, ) if not hasattr(base, "_copies"): base._copies = {} base._copies[types] = copied_type return copied_type if is_type_var(base): return params_to_type[base] return base