Repository: strawberry-graphql/strawberry
Pull request #41 (2019-04-10 05:59:40)

Improve arguments conversion
Added tests and fixed some bugs :)


Patch:
diff --git a/README.md b/README.md
index f01fb74f..796d5105 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 
 > Python GraphQL library based on dataclasses
 
-[![CircleCI](https://img.shields.io/circleci/token/307b40d5e152e074d34f84d30d226376a15667d5/project/github/strawberry-graphql/strawberry/master.svg?style=for-the-badge)](https://circleci.com/gh/strawberry-graphql/strawberry/tree/master)
+[![CircleCI](https://img.shields.io/circleci/token/307b40d5e152e074d34f84d30d226376a15667d5/project/github/patrick91/strawberry/master.svg?style=for-the-badge)](https://circleci.com/gh/patrick91/strawberry/tree/master)
 
 ## Installation
 
@@ -79,8 +79,8 @@ pre-commit install
 ## Links
 
 - Project homepage: https://strawberry.rocks
-- Repository: https://github.com/strawberry-graphql/strawberry
-- Issue tracker: https://github.com/strawberry-graphql/strawberry/issues
+- Repository: https://github.com/patrick91/strawberry
+- Issue tracker: https://github.com/patrick91/strawberryissues
   - In case of sensitive bugs like security vulnerabilities, please contact
     patrick.arminio@gmail.com directly instead of using issue tracker. We value
     your effort to improve the security and privacy of this project!
diff --git a/strawberry/field.py b/strawberry/field.py
index e1690b8d..1576e2f5 100644
--- a/strawberry/field.py
+++ b/strawberry/field.py
@@ -7,6 +7,44 @@ from .exceptions import MissingArgumentsAnnotationsError, MissingReturnAnnotatio
 from .type_converter import get_graphql_type_for_annotation
 from .utils.dict_to_type import dict_to_type
 from .utils.inspect import get_func_args
+from .utils.typing import (
+    get_list_annotation,
+    get_optional_annotation,
+    is_list,
+    is_optional,
+)
+
+
+def convert_args(args, annotations):
+    """Converts a nested dictionary to a dictionary of strawberry input types."""
+
+    converted_args = {}
+
+    for key, value in args.items():
+        annotation = annotations[key]
+
+        # we don't need to check about unions here since they are not
+        # yet supported for arguments.
+        # see https://github.com/graphql/graphql-spec/issues/488
+
+        is_list_of_args = False
+
+        if is_optional(annotation):
+            annotation = get_optional_annotation(annotation)
+
+        if is_list(annotation):
+            annotation = get_list_annotation(annotation)
+            is_list_of_args = True
+
+        if getattr(annotation, IS_STRAWBERRY_INPUT, False):
+            if is_list_of_args:
+                converted_args[key] = [dict_to_type(x, annotation) for x in value]
+            else:
+                converted_args[key] = dict_to_type(value, annotation)
+        else:
+            converted_args[key] = value
+
+    return converted_args
 
 
 def field(wrap, *, is_subscription=False):
@@ -39,19 +77,8 @@ def field(wrap, *, is_subscription=False):
         for name, annotation in arguments_annotations.items()
     }
 
-    def convert_args(args):
-        converted_args = {}
-
-        for key, value in args.items():
-            if getattr(arguments_annotations[key], IS_STRAWBERRY_INPUT):
-                converted_args[key] = dict_to_type(value, arguments_annotations[key])
-            else:
-                converted_args[key] = value
-
-        return converted_args
-
     def resolver(source, info, **args):
-        args = convert_args(args)
+        args = convert_args(args, arguments_annotations)
 
         return wrap(source, info, **args)
 
diff --git a/strawberry/type_converter.py b/strawberry/type_converter.py
index e01a084a..2a378dce 100644
--- a/strawberry/type_converter.py
+++ b/strawberry/type_converter.py
@@ -12,6 +12,7 @@ from graphql import (
 )
 
 from .scalars import ID
+from .utils.typing import is_union
 
 
 REGISTRY = {
@@ -30,7 +31,7 @@ def get_graphql_type_for_annotation(
     annotation, field_name: str, force_optional: bool = False
 ):
     # TODO: this might lead to issues with types that have a field value
-    is_optional = force_optional
+    is_field_optional = force_optional
 
     if hasattr(annotation, "field"):
         graphql_type = annotation.field
@@ -51,21 +52,18 @@ def get_graphql_type_for_annotation(
             # https://docs.python.org/3/library/typing.html#typing.AsyncGenerator
             return get_graphql_type_for_annotation(annotation.__args__[0], field_name)
 
-        # for some reason _name is None for Optional and Union types, so we check if we
-        # have __args__ populated, there might be some edge cases where __args__ is
-        # populated but the type is not an Union, like in the above case with Lists
-        elif hasattr(annotation, "__args__"):
+        elif is_union(annotation):
             types = annotation.__args__
             non_none_types = [x for x in types if x != None.__class__]  # noqa:E721
 
             # optionals are represented as Union[type, None]
             if len(non_none_types) == 1:
-                is_optional = True
+                is_field_optional = True
                 graphql_type = get_graphql_type_for_annotation(
                     non_none_types[0], field_name, force_optional=True
                 )
             else:
-                is_optional = None.__class__ in types
+                is_field_optional = None.__class__ in types
 
                 # TODO: union types don't work with scalar types
                 # so we want to return a nice error
@@ -80,7 +78,7 @@ def get_graphql_type_for_annotation(
     if not graphql_type:
         raise ValueError(f"Unable to get GraphQL type for {annotation}")
 
-    if is_optional:
+    if is_field_optional:
         return graphql_type
 
     return GraphQLNonNull(graphql_type)
diff --git a/strawberry/utils/typing.py b/strawberry/utils/typing.py
new file mode 100644
index 00000000..ef401238
--- /dev/null
+++ b/strawberry/utils/typing.py
@@ -0,0 +1,42 @@
+import typing
+
+
+def is_list(annotation):
+    """Returns True if annotation is a typing.List"""
+
+    annotation_origin = getattr(annotation, "__origin__", None)
+
+    return annotation_origin == list
+
+
+def is_union(annotation):
+    """Returns True if annotation is a typing.Union"""
+
+    annotation_origin = getattr(annotation, "__origin__", None)
+
+    return annotation_origin == typing.Union
+
+
+def is_optional(annotation):
+    """Returns True if the annotation is typing.Optional[SomeType]"""
+
+    # Optionals are represented as unions
+
+    if not is_union(annotation):
+        return False
+
+    types = annotation.__args__
+
+    # A Union to be optional needs to have at least one None type
+    return any([x == None.__class__ for x in types])  # noqa:E721
+
+
+def get_optional_annotation(annotation):
+    types = annotation.__args__
+    non_none_types = [x for x in types if x != None.__class__]  # noqa:E721
+
+    return non_none_types[0]
+
+
+def get_list_annotation(annotation):
+    return annotation.__args__[0]


Test patch:
diff --git a/tests/test_arguments_converter.py b/tests/test_arguments_converter.py
new file mode 100644
index 00000000..e196b931
--- /dev/null
+++ b/tests/test_arguments_converter.py
@@ -0,0 +1,84 @@
+import typing
+
+import strawberry
+from strawberry.field import convert_args
+
+
+def test_simple_types():
+    args = {"integer": 1, "string": "abc", "float": 1.2}
+
+    annotations = {"integer": int, "string": str, "float": float}
+
+    assert convert_args(args, annotations) == {
+        "integer": 1,
+        "string": "abc",
+        "float": 1.2,
+    }
+
+
+def test_list():
+    args = {
+        "integer_list": [1, 2],
+        "string_list": ["abc", "cde"],
+        "float_list": [1.2, 2.3],
+    }
+
+    annotations = {
+        "integer_list": typing.List[int],
+        "string_list": typing.List[str],
+        "float_list": typing.List[float],
+    }
+
+    assert convert_args(args, annotations) == {
+        "integer_list": [1, 2],
+        "string_list": ["abc", "cde"],
+        "float_list": [1.2, 2.3],
+    }
+
+
+def test_input_types():
+    @strawberry.input
+    class MyInput:
+        abc: str
+
+    args = {"input": {"abc": "example"}}
+
+    annotations = {"input": MyInput}
+
+    assert convert_args(args, annotations) == {"input": MyInput(abc="example")}
+
+
+def test_optional_input_types():
+    @strawberry.input
+    class MyInput:
+        abc: str
+
+    args = {"input": {"abc": "example"}}
+
+    annotations = {"input": typing.Optional[MyInput]}
+
+    assert convert_args(args, annotations) == {"input": MyInput(abc="example")}
+
+
+def test_list_of_input_types():
+    @strawberry.input
+    class MyInput:
+        abc: str
+
+    args = {"input_list": [{"abc": "example"}]}
+
+    annotations = {"input_list": typing.List[MyInput]}
+
+    assert convert_args(args, annotations) == {"input_list": [MyInput(abc="example")]}
+
+
+def test_optional_list_of_input_types():
+    @strawberry.input
+    class MyInput:
+        abc: str
+
+    args = {"input_list": [{"abc": "example"}]}
+
+    annotations = {"input_list": typing.Optional[typing.List[MyInput]]}
+
+    assert convert_args(args, annotations) == {"input_list": [MyInput(abc="example")]}