def resolve_type_field(field: StrawberryField) -> None: # TODO: This should be handled by StrawberryType in the future if isinstance(field.type, str): module = sys.modules[field.origin.__module__] field.type = eval(field.type, module.__dict__) if isinstance(field.type, LazyType): field.type = field.type.resolve_type() if is_forward_ref(field.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 = field.type.__forward_arg__ module = sys.modules[field.origin.__module__] # TODO: we should probably raise an error if we can't find the type field.type = module.__dict__[type_name] return if is_async_generator(field.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.type = get_async_generator_annotation(field.type) return resolve_type_field(field) # check for Optional[A] which is represented as Union[A, None], we # have an additional check for proper unions below if is_optional(field.type) and len(field.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.is_optional = True and not field.is_list or field.is_optional field.is_child_optional = field.is_list field.type = get_optional_annotation(field.type) return resolve_type_field(field) elif is_list(field.type): child_field = StrawberryField( python_name=None, graphql_name=None, origin=field.origin, # type: ignore type_=get_list_annotation(field.type), ) resolve_type_field(child_field) field.is_list = True field.child = child_field # TODO: Fix StrawberryField.type typing field.type = typing.cast(type, None) 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(field.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.is_optional = is_optional(field.type) types = field.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, field.python_name) for t in types if t is not None.__class__) field.is_union = True # TODO: Fix StrawberryField.type typing strawberry_union = typing.cast( type, union(get_name_from_types(types), types)) field.type = strawberry_union # 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(field.type, "_type_definition") and field.type._type_definition.is_generic): args = get_args(field.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: raise MissingTypesForGenericError(field.python_name, field.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.type = copy_type_with(field.type, *args) if isinstance(field.type, StrawberryUnion): field.is_union = True
def copy_type_with( base: Type, *types: Type, params_to_type: Dict[Type, Union[Type, StrawberryUnion]] = None) -> Type: if params_to_type is None: params_to_type = {} if isinstance(base, StrawberryUnion): return copy_union_with(base.types, params_to_type=params_to_type, description=base.description) if hasattr(base, "_type_definition"): definition = cast(TypeDefinition, base._type_definition) if definition.type_params: fields = [] type_params = definition.type_params.values() for param, type_ in zip(type_params, types): if is_union(type_): params_to_type[param] = copy_union_with( type_.__args__, params_to_type=params_to_type) else: params_to_type[param] = type_ name = get_name_from_types( params_to_type.values()) + definition.name for field in definition.fields: # Copy federation information federation = FederationFieldParams(**field.federation.__dict__) new_field = StrawberryField( python_name=field.python_name, graphql_name=field.graphql_name, origin=field.origin, type_=field.type, default_value=field.default_value, base_resolver=field.base_resolver, child=field.child, is_child_optional=field.is_child_optional, is_list=field.is_list, is_optional=field.is_optional, is_subscription=field.is_subscription, is_union=field.is_union, federation=federation, permission_classes=field.permission_classes, ) if field.is_list: assert field.child is not None child_type = copy_type_with(field.child.type, params_to_type=params_to_type) new_field.child = StrawberryField( python_name=field.child.python_name, origin=field.child.origin, graphql_name=field.child.graphql_name, is_optional=field.child.is_optional, type_=child_type, ) else: new_field.type = copy_type_with( field.type, params_to_type=params_to_type) fields.append(new_field) 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, (base.__origin__, ) if hasattr(base, "__origin__") else (), {"_type_definition": type_definition}, ) if not hasattr(base, "_copies"): base._copies = {} base._copies[types] = copied_type return copied_type if is_type_var(base): # TODO: we ignore the type issue here as we'll improve how types # are represented internally (using StrawberryTypes) so we can improve # typings later return params_to_type[base] # type: ignore return base