Repository: strawberry-graphql/strawberry
Pull request #120 (2019-08-02 05:44:27)

Bump starlette from 0.12.4 to 0.12.7
Bumps [starlette](https://github.com/encode/starlette) from 0.12.4 to 0.12.7.
<details>
<summary>Commits</summary>

- [`9bed25c`](https://github.com/encode/starlette/commit/9bed25c91c0e1e8f77bd575af76d7bd763c40af9) Version 0.12.7
- [`d395a67`](https://github.com/encode/starlette/commit/d395a673d67cc60b7dfa447c3caf508979e018de) Merge pull request [#488](https://github-redirect.dependabot.com/encode/starlette/issues/488) from florimondmanca/fix/startup-error-handling
- [`eeb6e54`](https://github.com/encode/starlette/commit/eeb6e549b6bed325e69dc09d88040aed0050f2ea) Version 0.12.6
- [`cb99f5c`](https://github.com/encode/starlette/commit/cb99f5c0779789c162496b4261c770615b732e05) Merge branch 'master' of https://github.com/encode/starlette
- [`dee0299`](https://github.com/encode/starlette/commit/dee0299c3e6dd8056c245b785e0d3db1c1b8f24a) Merge pull request [#589](https://github-redirect.dependabot.com/encode/starlette/issues/589) from ciscorn/fix-multipart-encoding
- [`6071977`](https://github.com/encode/starlette/commit/60719777699f2ca0ea1631ace832b8680a78a181) multipart: fallback to latin-1 on decode error
- [`58016bd`](https://github.com/encode/starlette/commit/58016bd9187fb592eff4e21c431bb9fc8d7fb86a) utf-8 is better for default multipart encoding
- [`0a06013`](https://github.com/encode/starlette/commit/0a06013ef6d1ed98a107c871f61fa82a5571130b) Version 0.12.5
- [`274e570`](https://github.com/encode/starlette/commit/274e570553350e46feba1235eca4cb22c950b614) Merge pull request [#562](https://github-redirect.dependabot.com/encode/starlette/issues/562) from jaceksnet/multipart-encoding-fix
- See full diff in [compare view](https://github.com/encode/starlette/compare/0.12.4...0.12.7)
</details>
<br />

[![Dependabot compatibility score](https://api.dependabot.com/badges/compatibility_score?dependency-name=starlette&package-manager=pip&previous-version=0.12.4&new-version=0.12.7)](https://dependabot.com/compatibility-score.html?dependency-name=starlette&package-manager=pip&previous-version=0.12.4&new-version=0.12.7)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot ignore this [patch|minor|major] version` will close this PR and stop Dependabot creating any more for this minor/major version (unless you reopen the PR or upgrade to it). To ignore the version in this PR you can just close it
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language
- `@dependabot badge me` will comment on this PR with code to add a "Dependabot enabled" badge to your readme

Additionally, you can set the following in your Dependabot [dashboard](https://app.dependabot.com):
- Update frequency (including time of day and day of week)
- Pull request limits (per update run and/or open at any time)
- Out-of-range updates (receive only lockfile updates, if desired)
- Security updates (receive only security updates, if desired)

Finally, you can contact us by mentioning @dependabot.

</details>

codecov[bot]: # [Codecov](https://codecov.io/gh/strawberry-graphql/strawberry/pull/120?src=pr&el=h1) Report
> Merging [#120](https://codecov.io/gh/strawberry-graphql/strawberry/pull/120?src=pr&el=desc) into [master](https://codecov.io/gh/strawberry-graphql/strawberry/commit/4d6b78559bafc9dfa7139f8dca8adbcc8e0a0700?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.


```diff
@@           Coverage Diff           @@
##           master     #120   +/-   ##
=======================================
  Coverage   90.56%   90.56%           
=======================================
  Files          20       20           
  Lines         424      424           
=======================================
  Hits          384      384           
  Misses         40       40
```


Patch:
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 00000000..27e7c60a
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,20 @@
+Release type: minor
+
+Added support for defining query directives, example:
+
+```python
+import strawberry
+from strawberry.directives import DirectiveLocation
+
+@strawberry.type
+class Query:
+    cake: str = "made_in_switzerland"
+
+@strawberry.directive(
+    locations=[DirectiveLocation.FIELD], description="Make string uppercase"
+)
+def uppercase(value: str, example: str):
+    return value.upper()
+
+schema = strawberry.Schema(query=Query, directives=[uppercase])
+```
diff --git a/poetry.lock b/poetry.lock
index af46a693..cc8f4e3e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -398,7 +398,7 @@ description = "The little ASGI library that shines."
 name = "starlette"
 optional = false
 python-versions = ">=3.6"
-version = "0.12.4"
+version = "0.12.7"
 
 [[package]]
 category = "dev"
@@ -481,7 +481,7 @@ python-versions = ">=2.7"
 version = "0.5.1"
 
 [metadata]
-content-hash = "12b5a553041ff70d3aebc90c71505bca35fe9733eaf4a7b01f64d06ef5adf735"
+content-hash = "d6e0c4dddd8b9bf396de2253e7b6d4d710e0b83de4d0f5e97a2f8b6aefee6d1c"
 python-versions = "^3.7"
 
 [metadata.hashes]
@@ -525,7 +525,7 @@ pytest-cov = ["2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6"
 pytest-emoji = ["6e34ed21970fa4b80a56ad11417456bd873eb066c02315fe9df0fafe6d4d4436", "e1bd4790d87649c2d09c272c88bdfc4d37c1cc7c7a46583087d7c510944571e8"]
 requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"]
 six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
-starlette = ["23f1de058ffcc053e9bddd87f49bb8bcda709ed2787d038819f6d8561e71f8d4"]
+starlette = ["849553e6dc8dd4faac5d4383219bdf49b199f3f09d60ce374c2a00a42ab7c77d"]
 toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
 typing = ["38566c558a0a94d6531012c8e917b1b8518a41e418f7f15f00e129cc80162ad3", "53765ec4f83a2b720214727e319607879fec4acde22c4fbb54fa2604e79e44ce", "84698954b4e6719e912ef9a42a2431407fe3755590831699debda6fba92aac55"]
 urllib3 = ["b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", "dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"]
diff --git a/pyproject.toml b/pyproject.toml
index a909e618..76af86d5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,7 +20,7 @@ classifiers = [
 [tool.poetry.dependencies]
 python = "^3.7"
 graphql-core-next = "^1.0"
-starlette = "=0.12.4"
+starlette = "=0.12.7"
 click = "^7.0"
 hupper = "^1.5"
 pygments = "^2.3"
diff --git a/strawberry/__init__.py b/strawberry/__init__.py
index 7220b009..63379c82 100644
--- a/strawberry/__init__.py
+++ b/strawberry/__init__.py
@@ -8,3 +8,4 @@ from .scalars import ID  # noqa
 from .schema import Schema  # noqa
 from .type import input, type, interface  # noqa
 from .permission import BasePermission  # noqa
+from .directive import directive  # noqa
diff --git a/strawberry/asgi/__init__.py b/strawberry/asgi/__init__.py
index dcb1e23c..38ef0fa2 100644
--- a/strawberry/asgi/__init__.py
+++ b/strawberry/asgi/__init__.py
@@ -1,13 +1,13 @@
 import asyncio
 import typing
 
-from graphql import graphql, GraphQLSchema, GraphQLError
+from graphql import GraphQLSchema, GraphQLError
 from starlette.requests import Request
 from starlette.types import Receive, Scope, Send
 from starlette.websockets import WebSocket, WebSocketState
 from graphql.error import format_error as format_graphql_error
 
-from ..graphql import subscribe
+from ..graphql import execute, subscribe
 from ..utils.debug import pretty_print_graphql_operation
 from .constants import (
     GQL_COMPLETE,
@@ -146,7 +146,7 @@ class GraphQL:
         if self.debug:
             pretty_print_graphql_operation(operation_name, query, variables)
 
-        return await graphql(
+        return await execute(
             self.schema,
             query,
             variable_values=variables,
diff --git a/strawberry/directive.py b/strawberry/directive.py
new file mode 100644
index 00000000..e4bda09f
--- /dev/null
+++ b/strawberry/directive.py
@@ -0,0 +1,46 @@
+import typing
+from itertools import islice
+
+from graphql import DirectiveLocation, GraphQLArgument, GraphQLDirective
+
+from .type_converter import get_graphql_type_for_annotation
+from .utils.str_converters import to_camel_case
+
+
+DIRECTIVE_REGISTRY = {}
+
+
+def _get_arguments(func):
+    annotations = func.__annotations__
+    name = func.__name__
+
+    arguments = {}
+
+    for name, type_ in islice(annotations.items(), 1, None):
+        argument_type = get_graphql_type_for_annotation(type_, name)
+
+        name = to_camel_case(name)
+
+        arguments[name] = GraphQLArgument(argument_type)
+
+    return arguments
+
+
+def directive(
+    *, locations: typing.List[DirectiveLocation], description=None, name=None
+):
+    def _wrap(func):
+        directive_name = name or to_camel_case(func.__name__)
+
+        func.directive = GraphQLDirective(
+            name=directive_name,
+            locations=locations,
+            args=_get_arguments(func),
+            description=description,
+        )
+
+        DIRECTIVE_REGISTRY[directive_name] = func
+
+        return func
+
+    return _wrap
diff --git a/strawberry/field.py b/strawberry/field.py
index ddb513ed..80584049 100644
--- a/strawberry/field.py
+++ b/strawberry/field.py
@@ -55,7 +55,7 @@ class LazyFieldWrapper:
 
     def _check_has_annotations(self, func):
         # using annotations without passing from typing.get_type_hints
-        # as we don't the actually types for the annotations
+        # as we don't need the actually types for the annotations
         annotations = func.__annotations__
         name = func.__name__
 
diff --git a/strawberry/graphql.py b/strawberry/graphql.py
index b1969d1f..b76aec95 100644
--- a/strawberry/graphql.py
+++ b/strawberry/graphql.py
@@ -1,7 +1,51 @@
 import typing
 
-from graphql import ExecutionResult, GraphQLSchema, parse
+from graphql import (
+    ExecutionResult,
+    GraphQLError,
+    GraphQLSchema,
+    execute as graphql_excute,
+    parse,
+)
 from graphql.subscription import subscribe as graphql_subscribe
+from graphql.type import validate_schema
+from graphql.validation import validate
+
+from .middleware import DirectivesMiddleware
+
+
+async def execute(
+    schema: GraphQLSchema,
+    query: str,
+    context_value: typing.Any = None,
+    variable_values: typing.Dict[str, typing.Any] = None,
+    operation_name: str = None,
+):
+    schema_validation_errors = validate_schema(schema)
+    if schema_validation_errors:
+        return ExecutionResult(data=None, errors=schema_validation_errors)
+
+    try:
+        document = parse(query)
+    except GraphQLError as error:
+        return ExecutionResult(data=None, errors=[error])
+    except Exception as error:
+        error = GraphQLError(str(error), original_error=error)
+        return ExecutionResult(data=None, errors=[error])
+
+    validation_errors = validate(schema, document)
+
+    if validation_errors:
+        return ExecutionResult(data=None, errors=validation_errors)
+
+    return graphql_excute(
+        schema,
+        parse(query),
+        middleware=[DirectivesMiddleware()],
+        variable_values=variable_values,
+        operation_name=operation_name,
+        context_value=context_value,
+    )
 
 
 async def subscribe(
diff --git a/strawberry/middleware.py b/strawberry/middleware.py
new file mode 100644
index 00000000..cf357425
--- /dev/null
+++ b/strawberry/middleware.py
@@ -0,0 +1,20 @@
+from .directive import DIRECTIVE_REGISTRY
+
+
+class DirectivesMiddleware:
+    def resolve(self, next_, root, info, **kwargs):
+        result = next_(root, info, **kwargs)
+
+        for directive in info.field_nodes[0].directives:
+            directive_name = directive.name.value
+
+            func = DIRECTIVE_REGISTRY.get(directive_name)
+
+            arguments = {
+                argument.name.value: argument.value.value
+                for argument in directive.arguments
+            }
+
+            result = func(result, **arguments)
+
+        return result
diff --git a/strawberry/schema.py b/strawberry/schema.py
index a30c2ba7..400ebb3f 100644
--- a/strawberry/schema.py
+++ b/strawberry/schema.py
@@ -1,13 +1,20 @@
 from graphql import GraphQLSchema
+from graphql.type.directives import specified_directives
+from graphql.utilities.schema_printer import print_schema
 
 
 # TODO: typings
 
 
 class Schema(GraphQLSchema):
-    def __init__(self, query, mutation=None, subscription=None):
+    def __init__(self, query, mutation=None, subscription=None, directives=[]):
         super().__init__(
             query=query.field,
             mutation=mutation.field if mutation else None,
             subscription=subscription.field if subscription else None,
+            directives=specified_directives
+            + [directive.directive for directive in directives],
         )
+
+    def __repr__(self):
+        return print_schema(self)


Test patch:
diff --git a/tests/test_directives.py b/tests/test_directives.py
new file mode 100644
index 00000000..29b76693
--- /dev/null
+++ b/tests/test_directives.py
@@ -0,0 +1,14 @@
+import strawberry
+from graphql import DirectiveLocation, GraphQLDirective
+
+
+def test_declaring_executable_directive():
+    @strawberry.directive(
+        locations=[DirectiveLocation.FIELD], description="Sample description"
+    )
+    def uppercase(value: str) -> str:
+        return value.upper()
+
+    assert uppercase.directive.__class__ == GraphQLDirective
+    assert uppercase.directive.name == "uppercase"
+    assert uppercase.directive.description == "Sample description"
diff --git a/tests/test_execute.py b/tests/test_execute.py
new file mode 100644
index 00000000..8735222b
--- /dev/null
+++ b/tests/test_execute.py
@@ -0,0 +1,35 @@
+import pytest
+
+import strawberry
+from graphql import DirectiveLocation
+from strawberry.graphql import execute
+
+
+@pytest.mark.asyncio
+async def test_runs_directives():
+    @strawberry.type
+    class Query:
+        name: str = "Jess"
+
+    @strawberry.directive(
+        locations=[DirectiveLocation.FIELD], description="Make string uppercase"
+    )
+    def uppercase(value: str):
+        return value.upper()
+
+    @strawberry.directive(locations=[DirectiveLocation.FIELD])
+    def replace(value: str, old: str, new: str):
+        return value.replace(old, new)
+
+    schema = strawberry.Schema(query=Query, directives=[uppercase, replace])
+
+    query = """{
+        name @uppercase
+        jess: name @replace(old: "Jess", new: "Jessica")
+    }"""
+
+    result = await execute(schema, query)
+
+    assert not result.errors
+    assert result.data["name"] == "JESS"
+    assert result.data["jess"] == "Jessica"
diff --git a/tests/test_schema.py b/tests/test_schema.py
index bb0741e4..4e26d45d 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1,3 +1,4 @@
+import textwrap
 import typing
 from enum import Enum
 
@@ -5,7 +6,7 @@ import pytest
 
 import strawberry
 from dataclasses import InitVar
-from graphql import graphql, graphql_sync
+from graphql import DirectiveLocation, graphql, graphql_sync
 
 
 def test_simple_type():
@@ -435,3 +436,28 @@ def test_parent_class_fields_are_inherited():
     assert result.data["cake"] == "made_in_switzerland"
     assert result.data["friend"] == "food"
     assert result.data["helloThisIs"] == "patrick"
+
+
+def test_can_declare_directives():
+    @strawberry.type
+    class Query:
+        cake: str = "made_in_switzerland"
+
+    @strawberry.directive(
+        locations=[DirectiveLocation.FIELD], description="Make string uppercase"
+    )
+    def uppercase(value: str, example: str):
+        return value.upper()
+
+    schema = strawberry.Schema(query=Query, directives=[uppercase])
+
+    expected_schema = '''
+    """Make string uppercase"""
+    directive @uppercase(example: String!) on FIELD
+
+    type Query {
+      cake: String!
+    }
+    '''
+
+    assert repr(schema) == textwrap.dedent(expected_schema).lstrip()