Repository: mirumee/ariadne (python)

Issue:
make_executable_schema type_defs arg should accept str or list of str
The `make_executable_schema` utility should take string or list of strings with SDLs for type definitions and dict of dicts with resolvers as second argument, parse each one, merge them, then, if resolvers are given, add them to schema returned by `build_ast_schema` util from `graphql-core`, and finally return resulting executable schema that may then be passed to `graphql()`.

Pull request description:
Add build_schema_from_type_definitions
This PR:

- renames `build_schema` to `build_schema_from_type_definitions` (after apollo)
- adds support for definitions concatenating
- adds some tests and moves code around
- cut module names length and drop "function per module" approach

Fixes #7 

## TODO

* [x] Test happy path
* [x] Test what happens if type is redefined

Patch:
diff --git a/.pylintrc b/.pylintrc
index 8a605d8..4857df5 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -2,7 +2,7 @@
 load-plugins=pylint.extensions.bad_builtin, pylint.extensions.mccabe
 
 [MESSAGES CONTROL]
-disable=C0103, C0111, C0412, C1001, E1004, I0011, R0903, W0232, W0621
+disable=C0103, C0111, C0330, C0412, C1001, E1004, I0011, R0903, W0232, W0621
 
 [DESIGN]
 max-complexity=5
diff --git a/ariadne/__init__.py b/ariadne/__init__.py
index 94c6147..67b841b 100644
--- a/ariadne/__init__.py
+++ b/ariadne/__init__.py
@@ -1,11 +1,10 @@
-from .add_resolve_functions_to_schema import add_resolve_functions_to_schema
-from .default_resolver import default_resolver
-from .make_executable_schema import build_schema, make_executable_schema
-
+from .build_schema import build_schema_from_type_definitions
+from .executable_schema import make_executable_schema
+from .resolvers import add_resolve_functions_to_schema, default_resolver
 
 __all__ = [
     "add_resolve_functions_to_schema",
-    "build_schema",
+    "build_schema_from_type_definitions",
     "default_resolver",
     "make_executable_schema",
 ]
diff --git a/ariadne/make_executable_schema.py b/ariadne/build_schema.py
similarity index 76%
rename from ariadne/make_executable_schema.py
rename to ariadne/build_schema.py
index 1a979e6..adbe4e7 100644
--- a/ariadne/make_executable_schema.py
+++ b/ariadne/build_schema.py
@@ -1,3 +1,5 @@
+from typing import List, Union
+
 from graphql import GraphQLSchema, parse
 from graphql.language.ast import (
     Document,
@@ -9,7 +11,27 @@ from graphql.language.ast import (
 )
 from graphql.utils.build_ast_schema import build_ast_schema
 
-from .add_resolve_functions_to_schema import add_resolve_functions_to_schema
+
+def build_schema_from_type_definitions(
+    type_defs: Union[str, List[str]]
+) -> GraphQLSchema:
+    if isinstance(type_defs, list):
+        type_defs = concatenate_type_defs(type_defs)
+
+    document = parse(type_defs)
+
+    if not document_has_schema(document):
+        schema_definition = build_default_schema(document)
+        document.definitions.append(schema_definition)
+
+    return build_ast_schema(document)
+
+
+def concatenate_type_defs(type_defs: List[str]) -> str:
+    resolved_type_defs = []
+    for type_def in type_defs:
+        resolved_type_defs.append(type_def.strip())
+    return "\n\n".join(resolved_type_defs)
 
 
 def build_default_schema(document: Document) -> SchemaDefinition:
@@ -38,17 +60,3 @@ def build_default_schema(document: Document) -> SchemaDefinition:
 
 def document_has_schema(document: Document) -> bool:
     return any(isinstance(td, SchemaDefinition) for td in document.definitions)
-
-
-def build_schema(type_defs: str) -> GraphQLSchema:
-    document = parse(type_defs)
-    if not document_has_schema(document):
-        schema_definition = build_default_schema(document)
-        document.definitions.append(schema_definition)
-    return build_ast_schema(document)
-
-
-def make_executable_schema(type_defs: str, resolvers: dict) -> GraphQLSchema:
-    schema = build_schema(type_defs)
-    add_resolve_functions_to_schema(schema, resolvers)
-    return schema
diff --git a/ariadne/default_resolver.py b/ariadne/default_resolver.py
deleted file mode 100644
index d68fe53..0000000
--- a/ariadne/default_resolver.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from graphql.execution.base import ResolveInfo
-
-
-def default_resolver(context, info: ResolveInfo):
-    if isinstance(context, dict):
-        return context.get(info.field_name)
-    return getattr(context, info.field_name, None)
diff --git a/ariadne/executable_schema.py b/ariadne/executable_schema.py
new file mode 100644
index 0000000..87b127f
--- /dev/null
+++ b/ariadne/executable_schema.py
@@ -0,0 +1,14 @@
+from typing import List, Union
+
+from graphql import GraphQLSchema
+
+from .build_schema import build_schema_from_type_definitions
+from .resolvers import add_resolve_functions_to_schema
+
+
+def make_executable_schema(
+    type_defs: Union[str, List[str]], resolvers: dict
+) -> GraphQLSchema:
+    schema = build_schema_from_type_definitions(type_defs)
+    add_resolve_functions_to_schema(schema, resolvers)
+    return schema
diff --git a/ariadne/add_resolve_functions_to_schema.py b/ariadne/resolvers.py
similarity index 81%
rename from ariadne/add_resolve_functions_to_schema.py
rename to ariadne/resolvers.py
index cd3b3e0..c48be6b 100644
--- a/ariadne/add_resolve_functions_to_schema.py
+++ b/ariadne/resolvers.py
@@ -1,6 +1,11 @@
 from graphql import GraphQLObjectType, GraphQLScalarType, GraphQLSchema
+from graphql.execution.base import ResolveInfo
 
-from .default_resolver import default_resolver
+
+def default_resolver(parent, info: ResolveInfo):
+    if isinstance(parent, dict):
+        return parent.get(info.field_name)
+    return getattr(parent, info.field_name, None)
 
 
 def add_resolve_functions_to_schema(schema: GraphQLSchema, resolvers: dict):


Test patch:
diff --git a/tests/test_modularization.py b/tests/test_modularization.py
new file mode 100644
index 0000000..f70e36b
--- /dev/null
+++ b/tests/test_modularization.py
@@ -0,0 +1,53 @@
+from graphql import graphql
+
+from ariadne import make_executable_schema
+
+root_typedef = """
+    type Query {
+        user: User
+    }
+"""
+
+module_typedef = """
+    type User {
+        username: String
+    }
+"""
+
+overriding_typedef = """
+    type User {
+        firstName: String
+    }
+"""
+
+resolvers = {"Query": {"user": lambda *_: {"username": "Bob"}}}
+
+overriding_resolvers = {"Query": {"user": lambda *_: {"firstName": "Bob"}}}
+
+
+def test_concat_typedefs():
+    type_defs = [root_typedef, module_typedef]
+    schema = make_executable_schema(type_defs, resolvers)
+
+    result = graphql(schema, "{ user { username } }")
+    assert result.errors is None
+    assert result.data == {"user": {"username": "Bob"}}
+
+
+def test_override_typedef():
+    type_defs = [root_typedef, module_typedef, overriding_typedef]
+    schema = make_executable_schema(type_defs, overriding_resolvers)
+
+    result = graphql(schema, "{ user { firstName } }")
+    assert result.errors is None
+    assert result.data == {"user": {"firstName": "Bob"}}
+
+
+def test_override_typedef_outdated_field():
+    type_defs = [root_typedef, module_typedef, overriding_typedef]
+    schema = make_executable_schema(type_defs, overriding_resolvers)
+
+    result = graphql(schema, "{ user { username } }")
+    assert result.errors is not None
+    assert str(result.errors[0]) == 'Cannot query field "username" on type "User".'
+    assert result.data is None
diff --git a/tests/test_queries.py b/tests/test_queries.py
index f6a6a09..c80be6d 100644
--- a/tests/test_queries.py
+++ b/tests/test_queries.py
@@ -1,4 +1,5 @@
 from datetime import date
+from unittest.mock import Mock
 
 from graphql import graphql
 
@@ -62,6 +63,26 @@ def test_query_custom_type_default_resolver():
     assert result.data == {"test": {"node": "custom"}}
 
 
+def test_query_custom_type_object_default_resolver():
+    type_defs = """
+        type Query {
+            test: Custom
+        }
+
+        type Custom {
+            node: String
+        }
+    """
+
+    resolvers = {"Query": {"test": lambda *_: Mock(node="custom")}}
+
+    schema = make_executable_schema(type_defs, resolvers)
+
+    result = graphql(schema, "{ test { node } }")
+    assert result.errors is None
+    assert result.data == {"test": {"node": "custom"}}
+
+
 def test_query_custom_type_custom_resolver():
     type_defs = """
         type Query {