Repository: strawberry-graphql/strawberry
Pull request #105 (2019-07-15 06:14:52)

Bump graphql-core-next from 1.0.5 to 1.1.0
Bumps [graphql-core-next](https://github.com/graphql-python/graphql-core-next) from 1.0.5 to 1.1.0.
<details>
<summary>Release notes</summary>

*Sourced from [graphql-core-next's releases](https://github.com/graphql-python/graphql-core-next/releases).*

> ## v1.1.0
> Public release of GraphQL-core-next, based on GraphQL.js v14.4.0.
> 
> In addition to incorporating the latest changes from GraphQL.js
> (see particularly the list of changes in GraphQL.js [v14.4.0](https://github.com/graphql/graphql-js/releases/tag/v14.4.0)),
> this release also contains the following improvements and changes:
> 
> * Made GraphQLError hashable ([#35](https://github-redirect.dependabot.com/graphql-python/graphql-core-next/issues/35))
> * Fixed online documentation for GraphQLError
> * Support transformation of argument names ([#41](https://github-redirect.dependabot.com/graphql-python/graphql-core-next/issues/41))
> * Support value transformation of input object types ([#42](https://github-redirect.dependabot.com/graphql-python/graphql-core-next/issues/42))
> * Support custom type map reducers in GraphQLSchema
> * Allow comments on the last line of the query ([#39](https://github-redirect.dependabot.com/graphql-python/graphql-core-next/issues/39))
> 
> Future versions of GraphQL-core-next will be released as GraphQL-core version 3 and up.
> 
> GraphQL-core-next has been discontinued as separate distribution.
</details>
<details>
<summary>Commits</summary>

- [`afa494e`](https://github.com/graphql-python/graphql-core-next/commit/afa494eb5036791e4a67135d8af772011ebc20a1) Add deprecation note to the docs.
- [`1a528b7`](https://github.com/graphql-python/graphql-core-next/commit/1a528b7830b805c85e0730728d091c1542a2a837) Prepare version 1.1.0
- [`5855dfc`](https://github.com/graphql-python/graphql-core-next/commit/5855dfca7c995c86c0b1cd5c4b0dfab5bec3c1b6) Update mypy and check tests
- [`eeaf9ce`](https://github.com/graphql-python/graphql-core-next/commit/eeaf9ce1a0010e135cf7a5ccc4f8291979640a63) Fix check for custom reducer
- [`753b927`](https://github.com/graphql-python/graphql-core-next/commit/753b927513b29012c49684c866b7c23d441e64e4) Don't check types & directives with custom reducer
- [`08b2957`](https://github.com/graphql-python/graphql-core-next/commit/08b295723afc3af74e165071f9d556bdfe0fab1f) Use src layout to make sure we test the right thing
- [`d7fa650`](https://github.com/graphql-python/graphql-core-next/commit/d7fa65068ac9d756ee533b2c15be8bc31464f16d) Don't put test requirements in setup.py
- [`d10ea84`](https://github.com/graphql-python/graphql-core-next/commit/d10ea8433c60a5ddbcffa58d7b7df5e754056cbb) Add some more stuff to .gitignore
- [`428f3d8`](https://github.com/graphql-python/graphql-core-next/commit/428f3d83eb146915dfd35c20fd4daf3517486b43) Allow comments on the last line of the query (fixes [#39](https://github-redirect.dependabot.com/graphql-python/graphql-core-next/issues/39))
- [`52818aa`](https://github.com/graphql-python/graphql-core-next/commit/52818aa78dc54c9714ec976e1fb06904df581491) * Up-to-date with GraphQL.js 14.4.0
- Additional commits viewable in [compare view](https://github.com/graphql-python/graphql-core-next/compare/v1.0.5...v1.1.0)
</details>
<br />

[![Dependabot compatibility score](https://api.dependabot.com/badges/compatibility_score?dependency-name=graphql-core-next&package-manager=pip&previous-version=1.0.5&new-version=1.1.0)](https://dependabot.com/compatibility-score.html?dependency-name=graphql-core-next&package-manager=pip&previous-version=1.0.5&new-version=1.1.0)

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/105?src=pr&el=h1) Report
> Merging [#105](https://codecov.io/gh/strawberry-graphql/strawberry/pull/105?src=pr&el=desc) into [master](https://codecov.io/gh/strawberry-graphql/strawberry/commit/a98d057a370bcd395934a28baa91a370ea58e053?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.


```diff
@@           Coverage Diff           @@
##           master     #105   +/-   ##
=======================================
  Coverage   89.55%   89.55%           
=======================================
  Files          19       19           
  Lines         383      383           
=======================================
  Hits          343      343           
  Misses         40       40
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f308e8bd..e77fca59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,49 @@
 CHANGELOG
 =========
 
+0.13.1 - 2019-07-17
+-------------------
+
+Fix missing fields when extending a class, now we can do this:
+
+
+```python
+@strawberry.type
+class Parent:
+    cheese: str = "swiss"
+
+    @strawberry.field
+    def friend(self, info) -> str:
+        return 'food'
+
+@strawberry.type
+class Schema(Parent):
+    cake: str = "made_in_swiss"
+```
+
+0.13.0 - 2019-07-16
+-------------------
+
+This release adds field support for permissions 
+
+```python
+import strawberry
+
+from strawberry.permission import BasePermission
+
+class IsAdmin(BasePermission):
+    message = "You are not authorized"
+
+    def has_permission(self, info):
+      return False
+
+@strawberry.type
+class Query:
+    @strawberry.field(permisson_classes=[IsAdmin])
+    def hello(self, info) -> str:
+      return "Hello"
+```
+
 0.12.0 - 2019-06-25
 -------------------
 
diff --git a/poetry.lock b/poetry.lock
index b200a16b..5bffb0ec 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -154,8 +154,8 @@ category = "main"
 description = "GraphQL-core-next is a Python port of GraphQL.js, the JavaScript reference implementation for GraphQL."
 name = "graphql-core-next"
 optional = false
-python-versions = ">=3.6"
-version = "1.0.5"
+python-versions = ">=3.6,<4"
+version = "1.1.0"
 
 [[package]]
 category = "dev"
@@ -500,7 +500,7 @@ curtsies = ["89c802ec051d01dec6fc983e9856a3706e4ea8265d2940b1f6d504a9e26ed3a9"]
 entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"]
 fancycompleter = ["d2522f1f3512371f295379c4c0d1962de06762eb586c199620a2a5d423539b12"]
 flake8 = ["859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", "a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8"]
-graphql-core-next = ["5a6cdf23a3b24fa738a903b9fcdaa1db2768a6552f8d1fb8fb79d9add16d090d", "c663ccc36d9a5ece606b5b4f5509835f0ec8d6e6b941992ced67384edcbfd045"]
+graphql-core-next = ["720b2ebbaa1ce5faa0876e2c2b9a36f77c984b5d5731c9963741bb947cd8ab04", "9b6d148280f8f3039bf2ae5e563e941c3d21b783e012209ab8d4cfaa6f206c1b"]
 greenlet = ["000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0", "0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28", "23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8", "37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304", "51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0", "8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214", "81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043", "853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6", "8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625", "9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc", "9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638", "99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163", "a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4", "a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490", "ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248", "bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939", "beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87", "d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720", "d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656"]
 h11 = ["acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208", "f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"]
 httptools = ["e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"]
diff --git a/pyproject.toml b/pyproject.toml
index 8a2abc7e..2741ab9c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
 [tool.poetry]
 name = "strawberry-graphql"
 packages = [ { include = "strawberry" } ]
-version = "0.12.0"
+version = "0.13.1"
 description = "A library for creating GraphQL APIs"
 authors = ["Patrick Arminio <patrick.arminio@gmail.com>"]
 license = "MIT"
diff --git a/strawberry/__init__.py b/strawberry/__init__.py
index 8cbd4ce5..7220b009 100644
--- a/strawberry/__init__.py
+++ b/strawberry/__init__.py
@@ -7,3 +7,4 @@ from .mutation import mutation, subscription  # noqa
 from .scalars import ID  # noqa
 from .schema import Schema  # noqa
 from .type import input, type, interface  # noqa
+from .permission import BasePermission  # noqa
diff --git a/strawberry/field.py b/strawberry/field.py
index 5b5a01ef..708d0d2d 100644
--- a/strawberry/field.py
+++ b/strawberry/field.py
@@ -40,6 +40,7 @@ class LazyFieldWrapper:
         resolver=None,
         name=None,
         description=None,
+        permission_classes=None
     ):
         self._wrapped_obj = obj
         self.is_subscription = is_subscription
@@ -47,6 +48,7 @@ class LazyFieldWrapper:
         self.field_name = name
         self.field_resolver = resolver
         self.field_description = description
+        self.permission_classes = permission_classes or []
 
         if callable(self._wrapped_obj):
             self._check_has_annotations(self._wrapped_obj)
@@ -85,6 +87,23 @@ class LazyFieldWrapper:
     def __call__(self, *args, **kwargs):
         return self._wrapped_obj(self, *args, **kwargs)
 
+    def _get_permissions(self):
+        """
+        Gets all permissions defined in the permission classes
+        >>> strawberry.field(permission_classes=[IsAuthenticated])
+        """
+        return (permission() for permission in self.permission_classes)
+
+    def _check_permissions(self, info):
+        """
+        Checks if the permission should be accepted and
+        raises an exception if not
+        """
+        for permission in self._get_permissions():
+            if not permission.has_permission(info):
+                message = getattr(permission, "message", None)
+                raise PermissionError(message)
+
     @lazy_property
     def field(self):
         return _get_field(
@@ -93,6 +112,7 @@ class LazyFieldWrapper:
             is_subscription=self.is_subscription,
             name=self.field_name,
             description=self.field_description,
+            check_permission=self._check_permissions,
         )
 
 
@@ -128,12 +148,14 @@ class strawberry_field(dataclasses.Field):
         name=None,
         description=None,
         metadata=None,
+        permission_classes=None
     ):
         self.field_name = name
         self.field_description = description
         self.field_resolver = resolver
         self.is_subscription = is_subscription
         self.is_input = is_input
+        self.field_permission_classes = permission_classes
 
         super().__init__(
             # TODO:
@@ -161,6 +183,7 @@ class strawberry_field(dataclasses.Field):
             resolver=self.field_resolver,
             name=self.field_name,
             description=self.field_description,
+            permission_classes=self.field_permission_classes,
         )
 
 
@@ -198,7 +221,13 @@ def convert_args(args, annotations):
 
 
 def _get_field(
-    wrap, *, is_input=False, is_subscription=False, name=None, description=None
+    wrap,
+    *,
+    is_input=False,
+    is_subscription=False,
+    name=None,
+    description=None,
+    check_permission=None
 ):
     name = wrap.__name__
 
@@ -220,8 +249,9 @@ def _get_field(
     }
 
     def resolver(source, info, **args):
+        if check_permission:
+            check_permission(info)
         args = convert_args(args, arguments_annotations)
-
         return wrap(source, info, **args)
 
     field_params = {}
@@ -232,6 +262,8 @@ def _get_field(
         if is_subscription:
 
             def _resolve(event, info):
+                if check_permission:
+                    check_permission(info)
                 return event
 
             field_params.update({"subscribe": resolver, "resolve": _resolve})
@@ -253,6 +285,7 @@ def field(
     resolver=None,
     is_input=False,
     is_subscription=False,
+    permission_classes=None
 ):
     """Annotates a method or property as a GraphQL field.
 
@@ -275,6 +308,7 @@ def field(
         resolver=resolver,
         is_input=is_input,
         is_subscription=is_subscription,
+        permission_classes=permission_classes,
     )
 
     # when calling this with parens we are going to return a strawberry_field
diff --git a/strawberry/permission.py b/strawberry/permission.py
new file mode 100644
index 00000000..f2143014
--- /dev/null
+++ b/strawberry/permission.py
@@ -0,0 +1,9 @@
+class BasePermission:
+    """
+    Base class for creating permissions
+    """
+
+    def has_permission(self, info):
+        raise NotImplementedError(
+            "Permission classes should override has_permission method"
+        )
diff --git a/strawberry/type.py b/strawberry/type.py
index 7297e040..4d3ef22b 100644
--- a/strawberry/type.py
+++ b/strawberry/type.py
@@ -60,11 +60,16 @@ def _process_type(cls, *, is_input=False, is_interface=False, description=None):
                 resolver, is_input=is_input, description=description
             ).field
 
-        strawberry_fields = {
-            key: value
-            for key, value in cls.__dict__.items()
-            if getattr(value, IS_STRAWBERRY_FIELD, False)
-        }
+        strawberry_fields = {}
+
+        for base in [cls, *cls.__bases__]:
+            strawberry_fields.update(
+                {
+                    key: value
+                    for key, value in base.__dict__.items()
+                    if getattr(value, IS_STRAWBERRY_FIELD, False)
+                }
+            )
 
         for key, value in strawberry_fields.items():
             name = getattr(value, "field_name", None) or to_camel_case(key)


Test patch:
diff --git a/tests/test_permission.py b/tests/test_permission.py
new file mode 100644
index 00000000..99682825
--- /dev/null
+++ b/tests/test_permission.py
@@ -0,0 +1,78 @@
+import typing
+
+import pytest
+
+import strawberry
+from graphql import graphql_sync
+from graphql.language import parse
+from graphql.subscription import subscribe
+from strawberry.permission import BasePermission
+
+
+def test_raises_graphql_error_when_permission_method_is_missing():
+    class IsAuthenticated(BasePermission):
+        pass
+
+    @strawberry.type
+    class Query:
+        @strawberry.field(permission_classes=[IsAuthenticated])
+        def user(self, info) -> str:
+            return "patrick"
+
+    schema = strawberry.Schema(query=Query)
+
+    query = "{ user }"
+
+    result = graphql_sync(schema, query)
+    assert (
+        result.errors[0].message
+        == "Permission classes should override has_permission method"
+    )
+
+
+def test_raises_graphql_error_when_permission_is_denied():
+    class IsAuthenticated(BasePermission):
+        message = "User is not authenticated"
+
+        def has_permission(self, info):
+            return False
+
+    @strawberry.type
+    class Query:
+        @strawberry.field(permission_classes=[IsAuthenticated])
+        def user(self, info) -> str:
+            return "patrick"
+
+    schema = strawberry.Schema(query=Query)
+
+    query = "{ user }"
+
+    result = graphql_sync(schema, query)
+    assert result.errors[0].message == "User is not authenticated"
+
+
+@pytest.mark.asyncio
+async def test_raises_permission_error_for_subscription():
+    class IsAdmin(BasePermission):
+        message = "You are not authorized"
+
+        def has_permission(self, info):
+            return False
+
+    @strawberry.type
+    class Query:
+        name: str = "Andrew"
+
+    @strawberry.type
+    class Subscription:
+        @strawberry.subscription(permission_classes=[IsAdmin])
+        async def user(self, info) -> typing.AsyncGenerator[str, None]:
+            yield "Hello"
+
+    schema = strawberry.Schema(query=Query, subscription=Subscription)
+
+    query = "subscription { user }"
+
+    result = await subscribe(schema, parse(query))
+
+    assert result.errors[0].message == "You are not authorized"
diff --git a/tests/test_schema.py b/tests/test_schema.py
index fa41b9bf..00b1a9d3 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -403,3 +403,34 @@ def test_enum_description():
     ]
 
     assert result.data["pizzas"]["description"] == "We also love pizza"
+
+
+def test_parent_class_fields_are_inherited():
+    @strawberry.type
+    class Parent:
+        cheese: str = "swiss"
+
+        @strawberry.field
+        def friend(self, info) -> str:
+            return "food"
+
+    @strawberry.type
+    class Schema(Parent):
+        cake: str = "made_in_switzerland"
+
+        @strawberry.field
+        def hello_this_is(self, info) -> str:
+            return "patrick"
+
+    schema = strawberry.Schema(query=Schema)
+
+    query = "{ cheese, cake, friend, helloThisIs }"
+
+    result = graphql_sync(schema, query)
+
+    assert not result.errors
+
+    assert result.data["cheese"] == "swiss"
+    assert result.data["cake"] == "made_in_switzerland"
+    assert result.data["friend"] == "food"
+    assert result.data["helloThisIs"] == "patrick"