Repository: mirumee/ariadne

Issue:
Create shortcut function for GraphQLMiddleware.make_simple_server
Getting started with Ariadne could be made even simpler by providing shortcut function abstracting the `GraphQLMiddleware` away on first contact, thus saving users possible confusion about what they really are doing.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e569de..de30277 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 - Removed support for Python 3.5 and added support for 3.7.
 - Moved to `GraphQL-core-next` that supports `async` resolvers, query execution and implements more recent version of GraphQL spec. If you are updating existing project, you will need to uninstall `graphql-core` before installing `graphql-core-next`, as both libraries use `graphql` namespace.
 - Added `gql()` utility that provides GraphQL string validation on declaration time, and enables use of [Apollo-GraphQL](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) plugin in Python code.
+- Added `start_simple_server()` shortcut for creating simple server with `GraphQLMiddleware.make_server()` and then running it with `serve_forever()`.
 - `Boolean` built-in scalar now checks the type of each serialized value. Returning values of type other than `bool`, `int` or `float` from a field resolver will result in a `Boolean cannot represent a non boolean value` error.
 - Redefining type in `type_defs` will now result in `TypeError` being raised. This is breaking change from previous behaviour where old type was simply replaced with new one.
 - Returning `None` from scalar `parse_literal` and `parse_value` function no longer results in GraphQL API producing default error message. Instead `None` will be passed further down to resolver or producing "value is required" error if its marked as such with `!` For old behaviour raise either `ValueError` or `TypeError`. See documentation for more details.
\ No newline at end of file
diff --git a/README.md b/README.md
index bea59e0..f139833 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ Ariadne can be installed with pip:
 Following example creates API defining `Person` type and single query field `people` returning list of two persons. It also starts local dev server with [GraphQL Playground](https://github.com/prisma/graphql-playground) available on the `http://127.0.0.1:8888` address.
 
 ```python
-from ariadne import GraphQLMiddleware, gql
+from ariadne import gql, start_simple_server
 
 # Define types using Schema Definition Language (https://graphql.org/learn/schema/)
 # Wrapping string in gql function provides validation and better error traceback
@@ -65,8 +65,7 @@ resolvers = {
 
 
 # Create and run dev server that provides api browser
-graphql_server = GraphQLMiddleware.make_simple_server(type_defs, resolvers)
-graphql_server.serve_forever()  # Visit http://127.0.0.1:8888 to see API browser!
+start_simple_server(type_defs, resolvers) # Visit http://127.0.0.1:8888 to see API browser!
 ```
 
 For more guides and examples, please see the [documentation](https://ariadne.readthedocs.io/en/latest/?badge=latest).
diff --git a/ariadne/__init__.py b/ariadne/__init__.py
index 5784542..a8738f0 100644
--- a/ariadne/__init__.py
+++ b/ariadne/__init__.py
@@ -1,6 +1,6 @@
 from .executable_schema import make_executable_schema
 from .resolvers import add_resolve_functions_to_schema, default_resolver, resolve_to
-from .utils import gql
+from .utils import gql, start_simple_server
 from .wsgi_middleware import GraphQLMiddleware
 
 __all__ = [
@@ -10,4 +10,5 @@ __all__ = [
     "make_executable_schema",
     "resolve_to",
     "gql",
+    "start_simple_server",
 ]
diff --git a/ariadne/utils.py b/ariadne/utils.py
index d10576e..4975d30 100644
--- a/ariadne/utils.py
+++ b/ariadne/utils.py
@@ -1,6 +1,26 @@
+from typing import List, Union
+
 from graphql import parse
 
+from .wsgi_middleware import GraphQLMiddleware
+
 
 def gql(value: str) -> str:
     parse(value)
     return value
+
+
+def start_simple_server(
+    type_defs: Union[str, List[str]],
+    resolvers: Union[dict, List[dict]],
+    host: str = "127.0.0.1",
+    port: int = 8888,
+):
+    try:
+        print("Simple GraphQL server is running on the http://%s:%s" % (host, port))
+        graphql_server = GraphQLMiddleware.make_simple_server(
+            type_defs, resolvers, host, port
+        )
+        graphql_server.serve_forever()
+    except KeyboardInterrupt:
+        pass
diff --git a/docs/introduction.rst b/docs/introduction.rst
index fd766f1..4192577 100644
--- a/docs/introduction.rst
+++ b/docs/introduction.rst
@@ -100,24 +100,17 @@ Testing the API
 
 Now we have everything we need to finish our API, with only piece missing being the http server that would receive the HTTP requests, execute GraphQL queries and return responses.
 
-This is where Ariadne comes into play. One of the utilities that Ariadne provides to developers is a WSGI middleware that can also be run as simple http server for developers to experiment with GraphQL locally.
+This is where Ariadne comes into play. One of the utilities that Ariadne provides is a ``start_simple_server`` that enables developers to experiment with GraphQL locally without need for full-fledged HTTP stack or web framework::
 
-.. warning::
-   Please never run ``GraphQLMiddleware`` in production without a proper WSGI container such as uWSGI or Gunicorn.
+    from ariadne import start_simple_server
 
-This middleware can be imported directly from ``ariadne`` package, so lets add an appropriate import at the beginning of our Python script::
+We will now call ``start_simple_server`` with ``type_defs`` and ``resolvers`` as its arguments to start a simple dev server::
 
-    from ariadne import GraphQLMiddleware
+    start_simple_server(type_defs, resolvers)
 
-We will now call ``GraphQLMiddleware.make_simple_server`` class method with ``type_defs`` and ``resolvers`` as its arguments to construct a simple dev server that we can then start::
+Run your script with ``python myscript.py`` (remember to replace ``myscript.py`` with name of your file!). If all is well, you will see a message telling you that simple GraphQL server is running on the http://127.0.0.1:8888. Open this link in your web browser.
 
-    print("Visit the http://127.0.0.1:8888 in the browser and say { hello }!")
-    my_api_server = GraphQLMiddleware.make_simple_server(type_defs, resolvers)
-    my_api_server.serve_forever()
-
-Run your script with ``python myscript.py`` (remember to replace ``myscript.py`` with name of your file!). If all is well, you will see a message telling you to visit the http://127.0.0.1:8888 and say ``{ hello }``.
-
-This the GraphQL Playground, the open source API explorer for GraphQL APIs. You can enter ``{ hello }`` query on the left, press the big bright "run" button, and see the result on the right:
+You will see the GraphQL Playground, the open source API explorer for GraphQL APIs. You can enter ``{ hello }`` query on the left, press the big bright "run" button, and see the result on the right:
 
 .. image:: _static/hello-world.png
    :alt: Your first Ariadne GraphQL in action!
@@ -131,7 +124,7 @@ Completed code
 
 For reference here is complete code of the API from this guide::
 
-    from ariadne import GraphQLMiddleware, gql
+    from ariadne import gql, start_simple_server
 
     type_defs = gql("""
         type Query {
@@ -152,6 +145,4 @@ For reference here is complete code of the API from this guide::
         }
     }
 
-    print("Visit the http://127.0.0.1:8888 in the browser and say { hello }!")
-    my_api_server = GraphQLMiddleware.make_simple_server(type_defs, resolvers)
-    my_api_server.serve_forever()
+    start_simple_server(type_defs, resolvers)
diff --git a/docs/wsgi-middleware.rst b/docs/wsgi-middleware.rst
index b9c5bd4..8eadf8f 100644
--- a/docs/wsgi-middleware.rst
+++ b/docs/wsgi-middleware.rst
@@ -92,4 +92,10 @@ The ``make_simple_server`` respects inheritance chain, so you can use it in cust
             return MyContext(environ, request_data)
 
     simple_server = MyGraphQLMiddleware(type_defs, resolvers)
-    simple_server.serve_forever()  # info.context will now be instance of MyContext
\ No newline at end of file
+    simple_server.serve_forever()  # info.context will now be instance of MyContext
+
+.. warning::
+   Please never run ``GraphQLMiddleware`` in production without a proper WSGI container such as uWSGI or Gunicorn.
+   
+.. note::
+  ``ariadne.start_simple_server`` is actually a simple shortcut that internally creates HTTP server with ``GraphQLMiddleware.make_simple_server``, starts it with `serve_forever`, displays instruction message and handles ``KeyboardInterrupt`` gracefully.


Test patch:
diff --git a/tests/test_start_simple_server.py b/tests/test_start_simple_server.py
new file mode 100644
index 0000000..2468984
--- /dev/null
+++ b/tests/test_start_simple_server.py
@@ -0,0 +1,78 @@
+import pytest
+
+from ariadne import GraphQLMiddleware, start_simple_server
+
+
+@pytest.fixture
+def simple_server_mock(mocker):
+    return mocker.Mock(serve_forever=mocker.Mock(return_value=True))
+
+
+@pytest.fixture
+def middleware_make_simple_server_mock(mocker):
+    return mocker.patch.object(
+        GraphQLMiddleware, "make_simple_server", new=mocker.Mock()
+    )
+
+
+@pytest.fixture
+def type_defs():
+    return """
+        type Query {
+            hello: String
+        }
+    """
+
+
+@pytest.fixture
+def resolvers():
+    return {}
+
+
+def test_wsgi_simple_server_serve_forever_is_called(
+    mocker, type_defs, resolvers, simple_server_mock
+):
+    mocker.patch("wsgiref.simple_server.make_server", return_value=simple_server_mock)
+    start_simple_server(type_defs, resolvers)
+    simple_server_mock.serve_forever.assert_called_once()
+
+
+def test_keyboard_interrupt_is_handled_gracefully(mocker, type_defs, resolvers):
+    mocker.patch(
+        "wsgiref.simple_server.make_server", side_effect=KeyboardInterrupt("test")
+    )
+    start_simple_server(type_defs, resolvers)
+
+
+def test_type_defs_resolvers_host_and_ip_are_passed_to_graphql_middleware(
+    middleware_make_simple_server_mock
+):
+    start_simple_server("type_defs", "resolvers", "0.0.0.0", 4444)
+    middleware_make_simple_server_mock.assert_called_once_with(
+        "type_defs", "resolvers", "0.0.0.0", 4444
+    )
+
+
+def test_default_host_and_ip_are_passed_to_graphql_middleware_if_not_set(
+    middleware_make_simple_server_mock
+):
+    start_simple_server("type_defs", "resolvers")
+    middleware_make_simple_server_mock.assert_called_once_with(
+        "type_defs", "resolvers", "127.0.0.1", 8888
+    )
+
+
+def test_default_host_and_ip_are_printed_on_server_creation(
+    middleware_make_simple_server_mock, capsys
+):  # pylint: disable=unused-argument
+    start_simple_server("", {})
+    captured = capsys.readouterr()
+    assert "http://127.0.0.1:8888" in captured.out
+
+
+def test_custom_host_and_ip_are_printed_on_server_creation(
+    middleware_make_simple_server_mock, capsys
+):  # pylint: disable=unused-argument
+    start_simple_server("", {}, "0.0.0.0", 4444)
+    captured = capsys.readouterr()
+    assert "http://0.0.0.0:4444" in captured.out