From 5c191b9062ed1da958f741042ec29eedc716492a Mon Sep 17 00:00:00 2001 From: sierreis <48896364+sierreis@users.noreply.github.com> Date: Sun, 24 Mar 2019 23:42:06 -0400 Subject: [PATCH 01/44] Add support for filterset_class meta parameter * Allow for use of either filter_fields or filterset_class * Add tests to check that the behavior is similar to filter_fields * Add documentation to show how to make use of the parameter --- docs/filtering.rst | 29 +++++++++++- graphene_django/converter.py | 5 +- graphene_django/filter/fields.py | 17 ++++--- graphene_django/filter/tests/test_fields.py | 52 +++++++++++++++++++++ graphene_django/types.py | 15 ++++-- 5 files changed, 105 insertions(+), 13 deletions(-) diff --git a/docs/filtering.rst b/docs/filtering.rst index feafd40..e27f8ce 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -100,7 +100,7 @@ features of ``django-filter``. This is done by transparently creating a ``filter_fields``. However, you may find this to be insufficient. In these cases you can -create your own ``Filterset`` as follows: +create your own ``FilterSet``. You can pass it directly as follows: .. code:: python @@ -127,6 +127,33 @@ create your own ``Filterset`` as follows: all_animals = DjangoFilterConnectionField(AnimalNode, filterset_class=AnimalFilter) +You can also specify the ``FilterSet`` class using the ``filerset_class`` +parameter when defining your ``DjangoObjectType``, however, this can't be used +in unison with the ``filter_fields`` parameter: + +.. code:: python + + class AnimalFilter(django_filters.FilterSet): + # Do case-insensitive lookups on 'name' + name = django_filters.CharFilter(lookup_expr=['iexact']) + + class Meta: + # Assume you have an Animal model defined with the following fields + model = Animal + fields = ['name', 'genus', 'is_domesticated'] + + + class AnimalNode(DjangoObjectType): + class Meta: + model = Animal + filterset_class = AnimalFilter + interfaces = (relay.Node, ) + + + class Query(ObjectType): + animal = relay.Node.Field(AnimalNode) + all_animals = DjangoFilterConnectionField(AnimalNode) + The context argument is passed on as the `request argument `__ in a ``django_filters.FilterSet`` instance. You can use this to customize your filters to be context-dependent. We could modify the ``AnimalFilter`` above to diff --git a/graphene_django/converter.py b/graphene_django/converter.py index c40313d..6fc1227 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -181,8 +181,9 @@ def convert_field_to_list_or_connection(field, registry=None): # into a DjangoConnectionField if _type._meta.connection: # Use a DjangoFilterConnectionField if there are - # defined filter_fields in the DjangoObjectType Meta - if _type._meta.filter_fields: + # defined filter_fields or a filterset_class in the + # DjangoObjectType Meta + if _type._meta.filter_fields or _type._meta.filterset_class: from .filter.fields import DjangoFilterConnectionField return DjangoFilterConnectionField(_type) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index cb42543..9aa629f 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -35,14 +35,17 @@ class DjangoFilterConnectionField(DjangoConnectionField): @property def filterset_class(self): if not self._filterset_class: - fields = self._fields or self.node_type._meta.filter_fields - meta = dict(model=self.model, fields=fields) - if self._extra_filter_meta: - meta.update(self._extra_filter_meta) + if not self.node_type._meta.filterset_class: + fields = self._fields or self.node_type._meta.filter_fields + meta = dict(model=self.model, fields=fields) + if self._extra_filter_meta: + meta.update(self._extra_filter_meta) - self._filterset_class = get_filterset_class( - self._provided_filterset_class, **meta - ) + self._filterset_class = get_filterset_class( + self._provided_filterset_class, **meta + ) + else: + self._filterset_class = self.node_type._meta.filterset_class return self._filterset_class diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index f9ef0ae..534ebb9 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -227,6 +227,58 @@ def test_filter_filterset_information_on_meta_related(): assert_not_orderable(articles_field) +def test_filter_filterset_class_information_on_meta(): + class ReporterFilter(FilterSet): + class Meta: + model = Reporter + fields = ["first_name", "articles"] + + class ReporterFilterNode(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + filterset_class = ReporterFilter + + field = DjangoFilterConnectionField(ReporterFilterNode) + assert_arguments(field, "first_name", "articles") + assert_not_orderable(field) + + +def test_filter_filterset_class_information_on_meta_related(): + class ReporterFilter(FilterSet): + class Meta: + model = Reporter + fields = ["first_name", "articles"] + + class ArticleFilter(FilterSet): + class Meta: + model = Article + fields = ["headline", "reporter"] + + class ReporterFilterNode(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + filterset_class = ReporterFilter + + class ArticleFilterNode(DjangoObjectType): + class Meta: + model = Article + interfaces = (Node,) + filterset_class = ArticleFilter + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterFilterNode) + all_articles = DjangoFilterConnectionField(ArticleFilterNode) + reporter = Field(ReporterFilterNode) + article = Field(ArticleFilterNode) + + schema = Schema(query=Query) + articles_field = ReporterFilterNode._meta.fields["articles"].get_type() + assert_arguments(articles_field, "headline", "reporter") + assert_not_orderable(articles_field) + + def test_filter_filterset_related_results(): class ReporterFilterNode(DjangoObjectType): class Meta: diff --git a/graphene_django/types.py b/graphene_django/types.py index 4441a9a..ef72b9b 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -44,6 +44,7 @@ class DjangoObjectTypeOptions(ObjectTypeOptions): connection = None # type: Type[Connection] filter_fields = () + filterset_class = None class DjangoObjectType(ObjectType): @@ -56,6 +57,7 @@ class DjangoObjectType(ObjectType): only_fields=(), exclude_fields=(), filter_fields=None, + filterset_class=None, connection=None, connection_class=None, use_connection=None, @@ -74,9 +76,15 @@ class DjangoObjectType(ObjectType): "The attribute registry in {} needs to be an instance of " 'Registry, received "{}".' ).format(cls.__name__, registry) - - if not DJANGO_FILTER_INSTALLED and filter_fields: - raise Exception("Can only set filter_fields if Django-Filter is installed") + + if filter_fields and filterset_class: + raise Exception("Can't set both filter_fields and filterset_class") + + if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class): + raise Exception(( + "Can only set filter_fields or filterset_class if " + "Django-Filter is installed" + )) django_fields = yank_fields_from_attrs( construct_fields(model, registry, only_fields, exclude_fields), _as=Field @@ -107,6 +115,7 @@ class DjangoObjectType(ObjectType): _meta.model = model _meta.registry = registry _meta.filter_fields = filter_fields + _meta.filterset_class = filterset_class _meta.fields = django_fields _meta.connection = connection From 4d905a46ac39a2f494f94ab177c84ba7c0c859cc Mon Sep 17 00:00:00 2001 From: sierreis <48896364+sierreis@users.noreply.github.com> Date: Mon, 25 Mar 2019 10:03:54 -0400 Subject: [PATCH 02/44] Fixed flake8 lint error --- graphene_django/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index ef72b9b..b33c6bf 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -76,10 +76,10 @@ class DjangoObjectType(ObjectType): "The attribute registry in {} needs to be an instance of " 'Registry, received "{}".' ).format(cls.__name__, registry) - + if filter_fields and filterset_class: raise Exception("Can't set both filter_fields and filterset_class") - + if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class): raise Exception(( "Can only set filter_fields or filterset_class if " From 132c4cb9d4174ced2ca716609e6f730f21d799ff Mon Sep 17 00:00:00 2001 From: sierreis <48896364+sierreis@users.noreply.github.com> Date: Mon, 25 Mar 2019 23:45:14 -0400 Subject: [PATCH 03/44] Fixed so that GrapheneFilterSetMixin is used with any provided filterset_class --- graphene_django/filter/fields.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index 9aa629f..7c85e9a 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -35,17 +35,16 @@ class DjangoFilterConnectionField(DjangoConnectionField): @property def filterset_class(self): if not self._filterset_class: - if not self.node_type._meta.filterset_class: - fields = self._fields or self.node_type._meta.filter_fields - meta = dict(model=self.model, fields=fields) - if self._extra_filter_meta: - meta.update(self._extra_filter_meta) + fields = self._fields or self.node_type._meta.filter_fields + meta = dict(model=self.model, fields=fields) + if self._extra_filter_meta: + meta.update(self._extra_filter_meta) - self._filterset_class = get_filterset_class( - self._provided_filterset_class, **meta - ) - else: - self._filterset_class = self.node_type._meta.filterset_class + filterset_class = self._provided_filterset_class or ( + self.node_type._meta.filterset_class) + self._filterset_class = get_filterset_class( + filterset_class, **meta + ) return self._filterset_class From 36ac5626e9e29d3fa2415caf154eb952fe12d9d6 Mon Sep 17 00:00:00 2001 From: Andrew Bettke Date: Wed, 27 Mar 2019 17:09:25 +1300 Subject: [PATCH 04/44] Adds enhanced support for proxy models. --- graphene_django/tests/models.py | 7 +++ graphene_django/tests/test_query.py | 93 ++++++++--------------------- graphene_django/types.py | 6 +- 3 files changed, 37 insertions(+), 69 deletions(-) diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index 4fe546d..b4eb3ce 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -65,6 +65,11 @@ class Reporter(models.Model): self.__class__ = CNNReporter +class CNNReporterManager(models.Manager): + def get_queryset(self): + return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2) + + class CNNReporter(Reporter): """ This class is a proxy model for Reporter, used for testing @@ -74,6 +79,8 @@ class CNNReporter(Reporter): class Meta: proxy = True + objects = CNNReporterManager() + class Article(models.Model): headline = models.CharField(max_length=100) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 1716034..82d7d75 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1,3 +1,4 @@ +import base64 import datetime import pytest @@ -895,8 +896,7 @@ def test_should_handle_inherited_choices(): def test_proxy_model_support(): """ - This test asserts that we can query for all Reporters, - even if some are of a proxy model type at runtime. + This test asserts that we can query for all Reporters and proxied Reporters. """ class ReporterType(DjangoObjectType): @@ -905,11 +905,17 @@ def test_proxy_model_support(): interfaces = (Node,) use_connection = True - reporter_1 = Reporter.objects.create( + class CNNReporterType(DjangoObjectType): + class Meta: + model = CNNReporter + interfaces = (Node,) + use_connection = True + + reporter = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) - reporter_2 = CNNReporter.objects.create( + cnn_reporter = CNNReporter.objects.create( first_name="Some", last_name="Guy", email="someguy@cnn.com", @@ -919,6 +925,7 @@ def test_proxy_model_support(): class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) + cnn_reporters = DjangoConnectionField(CNNReporterType) schema = graphene.Schema(query=Query) query = """ @@ -930,14 +937,26 @@ def test_proxy_model_support(): } } } + cnnReporters { + edges { + node { + id + } + } + } } """ expected = { "allReporters": { "edges": [ - {"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}, - {"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}}, + {"node": {"id": base64.b64encode("ReporterType:{}".format(reporter.id))}}, + {"node": {"id": base64.b64encode("ReporterType:{}".format(cnn_reporter.id))}}, + ] + }, + "cnnReporters": { + "edges": [ + {"node": {"id": base64.b64encode("CNNReporterType:{}".format(cnn_reporter.id))}} ] } } @@ -945,65 +964,3 @@ def test_proxy_model_support(): result = schema.execute(query) assert not result.errors assert result.data == expected - - -def test_proxy_model_fails(): - """ - This test asserts that if you try to query for a proxy model, - that query will fail with: - GraphQLError('Expected value of type "CNNReporterType" but got: - CNNReporter.',) - - This is because a proxy model has the identical model definition - to its superclass, and defines its behavior at runtime, rather than - at the database level. Currently, filtering objects of the proxy models' - type isn't supported. It would require a field on the model that would - represent the type, and it doesn't seem like there is a clear way to - enforce this pattern across all projects - """ - - class CNNReporterType(DjangoObjectType): - class Meta: - model = CNNReporter - interfaces = (Node,) - use_connection = True - - reporter_1 = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - - reporter_2 = CNNReporter.objects.create( - first_name="Some", - last_name="Guy", - email="someguy@cnn.com", - a_choice=1, - reporter_type=2, # set this guy to be CNN - ) - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(CNNReporterType) - - schema = graphene.Schema(query=Query) - query = """ - query ProxyModelQuery { - allReporters { - edges { - node { - id - } - } - } - } - """ - - expected = { - "allReporters": { - "edges": [ - {"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}, - {"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}}, - ] - } - } - - result = schema.execute(query) - assert result.errors diff --git a/graphene_django/types.py b/graphene_django/types.py index 4441a9a..6c386e0 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -130,7 +130,11 @@ class DjangoObjectType(ObjectType): if not is_valid_django_model(type(root)): raise Exception(('Received incompatible instance "{}".').format(root)) - model = root._meta.model._meta.concrete_model + if cls._meta.model._meta.proxy: + model = root._meta.model + else: + model = root._meta.model._meta.concrete_model + return model == cls._meta.model @classmethod From 980142dfcfb5411b39bbc95e1777475f81402558 Mon Sep 17 00:00:00 2001 From: Andrew Bettke Date: Wed, 27 Mar 2019 17:24:13 +1300 Subject: [PATCH 05/44] Fix linting. --- graphene_django/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index 6c386e0..2a402d7 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -134,7 +134,7 @@ class DjangoObjectType(ObjectType): model = root._meta.model else: model = root._meta.model._meta.concrete_model - + return model == cls._meta.model @classmethod From 83a2ad34cdb07de23038feefbe1167e45bbc8536 Mon Sep 17 00:00:00 2001 From: Andrew Bettke Date: Wed, 27 Mar 2019 17:28:56 +1300 Subject: [PATCH 06/44] Encode strings before passing to b64encode. --- graphene_django/tests/test_query.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 82d7d75..c99c8e1 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -950,13 +950,13 @@ def test_proxy_model_support(): expected = { "allReporters": { "edges": [ - {"node": {"id": base64.b64encode("ReporterType:{}".format(reporter.id))}}, - {"node": {"id": base64.b64encode("ReporterType:{}".format(cnn_reporter.id))}}, + {"node": {"id": base64.b64encode("ReporterType:{}".format(reporter.id).encode())}}, + {"node": {"id": base64.b64encode("ReporterType:{}".format(cnn_reporter.id).encode())}}, ] }, "cnnReporters": { "edges": [ - {"node": {"id": base64.b64encode("CNNReporterType:{}".format(cnn_reporter.id))}} + {"node": {"id": base64.b64encode("CNNReporterType:{}".format(cnn_reporter.id).encode())}} ] } } From a461e80ee461b7409f3728f747823dc75d56ce0e Mon Sep 17 00:00:00 2001 From: Andrew Bettke Date: Wed, 27 Mar 2019 17:56:06 +1300 Subject: [PATCH 07/44] Correctly encode / decode for python3+. --- graphene_django/tests/test_query.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index c99c8e1..5c38ce5 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -947,16 +947,19 @@ def test_proxy_model_support(): } """ + def str_to_node_id(val): + return base64.b64encode(val.encode()).decode() + expected = { "allReporters": { "edges": [ - {"node": {"id": base64.b64encode("ReporterType:{}".format(reporter.id).encode())}}, - {"node": {"id": base64.b64encode("ReporterType:{}".format(cnn_reporter.id).encode())}}, + {"node": {"id": str_to_node_id("ReporterType:{}".format(reporter.id))}}, + {"node": {"id": str_to_node_id("ReporterType:{}".format(cnn_reporter.id))}}, ] }, "cnnReporters": { "edges": [ - {"node": {"id": base64.b64encode("CNNReporterType:{}".format(cnn_reporter.id).encode())}} + {"node": {"id": str_to_node_id("CNNReporterType:{}".format(cnn_reporter.id))}} ] } } From d2f8bf730bbe571dbe568621e630c8f01dec9c55 Mon Sep 17 00:00:00 2001 From: sierreis <48896364+sierreis@users.noreply.github.com> Date: Wed, 27 Mar 2019 14:05:42 -0400 Subject: [PATCH 08/44] Test exception when both filterset_class and filter_fields are set --- graphene_django/filter/tests/test_fields.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 534ebb9..eb6581b 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -227,6 +227,21 @@ def test_filter_filterset_information_on_meta_related(): assert_not_orderable(articles_field) +def test_filter_filterset_class_filter_fields_exception(): + with pytest.raises(Exception): + class ReporterFilter(FilterSet): + class Meta: + model = Reporter + fields = ["first_name", "articles"] + + class ReporterFilterNode(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + filterset_class = ReporterFilter + filter_fields = ["first_name", "articles"] + + def test_filter_filterset_class_information_on_meta(): class ReporterFilter(FilterSet): class Meta: From 959e98eeb0c87295e2535b04f870ace2028a394b Mon Sep 17 00:00:00 2001 From: Andrew Bettke Date: Thu, 28 Mar 2019 09:56:10 +1300 Subject: [PATCH 09/44] Refactor to use formal to_global_id. --- graphene_django/tests/test_query.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 5c38ce5..e74b8d6 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -8,6 +8,7 @@ from py.test import raises from django.db.models import Q +from graphql_relay import to_global_id import graphene from graphene.relay import Node @@ -947,19 +948,16 @@ def test_proxy_model_support(): } """ - def str_to_node_id(val): - return base64.b64encode(val.encode()).decode() - expected = { "allReporters": { "edges": [ - {"node": {"id": str_to_node_id("ReporterType:{}".format(reporter.id))}}, - {"node": {"id": str_to_node_id("ReporterType:{}".format(cnn_reporter.id))}}, + {"node": {"id": to_global_id("ReporterType", reporter.id)}}, + {"node": {"id": to_global_id("ReporterType", cnn_reporter.id)}}, ] }, "cnnReporters": { "edges": [ - {"node": {"id": str_to_node_id("CNNReporterType:{}".format(cnn_reporter.id))}} + {"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}} ] } } From cb9eed6765f0ab706fdc1e6884d1c24c8123965b Mon Sep 17 00:00:00 2001 From: Anthony Monthe Date: Mon, 7 May 2018 00:22:34 +0100 Subject: [PATCH 10/44] Added tox.ini Updated Travis YML --- .gitignore | 2 ++ .travis.yml | 98 ++++++++++++++++++++++++++--------------------------- tox.ini | 31 +++++++++++++++++ 3 files changed, 81 insertions(+), 50 deletions(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 0b25625..150025a 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,5 @@ Session.vim *~ # auto-generated tag files tags +.tox/ +.pytest_cache/ diff --git a/.travis.yml b/.travis.yml index 07ee59f..5c4725f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,62 +1,60 @@ language: python sudo: required dist: xenial + python: -- 2.7 -- 3.4 -- 3.5 -- 3.6 -- 3.7 -install: -- | - if [ "$TEST_TYPE" = build ]; then - pip install -e .[test] - pip install psycopg2==2.8.2 # Required for Django postgres fields testing - pip install django==$DJANGO_VERSION - python setup.py develop - elif [ "$TEST_TYPE" = lint ]; then - pip install flake8==3.7.7 - fi -script: -- | - if [ "$TEST_TYPE" = lint ]; then - echo "Checking Python code lint." - flake8 graphene_django - exit - elif [ "$TEST_TYPE" = build ]; then - py.test --cov=graphene_django graphene_django examples - fi -after_success: -- | - if [ "$TEST_TYPE" = build ]; then - coveralls - fi + - 2.7 + - 3.4 + - 3.5 + - 3.6 + - 3.7 + env: matrix: - - TEST_TYPE=build DJANGO_VERSION=1.11 + - DJANGO=1.11 + - DJANGO=2.1 + - DJANGO=2.2 + - DJANGO=master + +install: + - TOX_ENV=py${TRAVIS_PYTHON_VERSION}-django${DJANGO} + - pip install tox + - tox -e $TOX_ENV --notest +script: + - tox -e $TOX_ENV + +after_success: + - tox -e $TOX_ENV -- pip install coveralls + - tox -e $TOX_ENV -- coveralls $COVERALLS_OPTION + matrix: fast_finish: true include: - - python: '3.4' - env: TEST_TYPE=build DJANGO_VERSION=2.0 - - python: '3.5' - env: TEST_TYPE=build DJANGO_VERSION=2.0 - - python: '3.6' - env: TEST_TYPE=build DJANGO_VERSION=2.0 - - python: '3.5' - env: TEST_TYPE=build DJANGO_VERSION=2.1 - - python: '3.6' - env: TEST_TYPE=build DJANGO_VERSION=2.1 - - python: '3.6' - env: TEST_TYPE=build DJANGO_VERSION=2.2 - - python: '3.7' - env: TEST_TYPE=build DJANGO_VERSION=2.2 - - python: '2.7' - env: TEST_TYPE=lint - - python: '3.6' - env: TEST_TYPE=lint - - python: '3.7' - env: TEST_TYPE=lint + - python: 3.5 + script: tox -e lint + exclude: + - python: 2.7 + env: DJANGO=2.1 + - python: 2.7 + env: DJANGO=2.2 + - python: 2.7 + env: DJANGO=master + - python: 3.4 + env: DJANGO=2.1 + - python: 3.4 + env: DJANGO=2.2 + - python: 3.4 + env: DJANGO=master + - python: 3.5 + env: DJANGO=master + - python: 3.7 + env: DJANGO=1.10 + - python: 3.7 + env: DJANGO=1.11 + allow_failures: + - python: 3.7 + - env: DJANGO=master + deploy: provider: pypi user: syrusakbary diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..8e21c74 --- /dev/null +++ b/tox.ini @@ -0,0 +1,31 @@ +[tox] +envlist = py{2.7,3.4,3.5,3.6,3.7,pypy,pypy3}-django{1.10,1.11,2.0,2.1,2.2,master},lint + +[testenv] +passenv = * +usedevelop = True +setenv = + DJANGO_SETTINGS_MODULE=django_test_settings +basepython = + py2.7: python2.7 + py3.4: python3.4 + py3.5: python3.5 + py3.6: python3.6 + py3.7: python3.7 + pypypy: pypy + pypypy3: pypy3 +deps = + -e.[test] + psycopg2 + django1.10: Django>=1.10,<1.11 + django1.11: Django>=1.11,<1.12 + django2.0: Django>=2.0 + django2.1: Django>=2.1 + djangomaster: https://github.com/django/django/archive/master.zip +commands = {posargs:py.test --cov=graphene_django graphene_django examples} + +[testenv:lint] +basepython = python +deps = + prospector +commands = prospector graphene_django -0 From ddf8d24bf5eb1911048482d04d046597f168c6be Mon Sep 17 00:00:00 2001 From: mvanlonden Date: Fri, 31 May 2019 14:38:34 -0700 Subject: [PATCH 11/44] increment version to match release tag --- graphene_django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py index 4538cb3..51acfd2 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,6 +1,6 @@ from .types import DjangoObjectType from .fields import DjangoConnectionField -__version__ = "2.2.0" +__version__ = "2.3.0" __all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"] From fc49a50cc3bceda9ac578c1746de35e2422cf99d Mon Sep 17 00:00:00 2001 From: Richard Sween Date: Wed, 5 Jun 2019 19:43:51 -0500 Subject: [PATCH 12/44] Update mutations.rst I believe the `[1]` was ommitted from the `from_global_id` call as that method returns a tuple of type and id, of which we're only interested in the id here. Took me half a day to figure out why this code wasn't working today. See function def here: https://github.com/graphql-python/graphql-relay-py/blob/master/graphql_relay/node/node.py#L67 --- docs/mutations.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/mutations.rst b/docs/mutations.rst index f6c6f14..15bef1d 100644 --- a/docs/mutations.rst +++ b/docs/mutations.rst @@ -214,7 +214,7 @@ You can use relay with mutations. A Relay mutation must inherit from @classmethod def mutate_and_get_payload(cls, root, info, text, id): - question = Question.objects.get(pk=from_global_id(id)) + question = Question.objects.get(pk=from_global_id(id)[1]) question.text = text question.save() return QuestionMutation(question=question) @@ -226,4 +226,4 @@ Relay ClientIDMutation accept a ``clientIDMutation`` argument. This argument is also sent back to the client with the mutation result (you do not have to do anything). For services that manage a pool of many GraphQL requests in bulk, the ``clientIDMutation`` -allows you to match up a specific mutation with the response. \ No newline at end of file +allows you to match up a specific mutation with the response. From d06217d2033375d19ad0cdaf4b0afb74c0eba408 Mon Sep 17 00:00:00 2001 From: Richard Sween Date: Thu, 6 Jun 2019 13:53:16 -0500 Subject: [PATCH 13/44] Fix Mutations Relay example imports Per comment here: https://github.com/graphql-python/graphene-django/pull/657#issuecomment-499618785 --- docs/mutations.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/mutations.rst b/docs/mutations.rst index 15bef1d..6610151 100644 --- a/docs/mutations.rst +++ b/docs/mutations.rst @@ -199,7 +199,9 @@ You can use relay with mutations. A Relay mutation must inherit from .. code:: python - import graphene import relay, DjangoObjectType + import graphene + from graphene import relay + from graphene_django import DjangoObjectType from graphql_relay import from_global_id from .queries import QuestionType From 67b21cb36f9ed5319a3eb15cb438cbc3762b2299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Roberto=20Meza=20Cabrera?= Date: Sun, 9 Jun 2019 14:08:31 -0500 Subject: [PATCH 14/44] Revert "Drop old Django compatibility code" This reverts commit 6acd917cf7076397009d0ba77901f4c1c8e190fe. --- graphene_django/utils/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphene_django/utils/utils.py b/graphene_django/utils/utils.py index 02c47ee..c2a3b09 100644 --- a/graphene_django/utils/utils.py +++ b/graphene_django/utils/utils.py @@ -18,7 +18,8 @@ def get_reverse_fields(model, local_field_names): if name in local_field_names: continue - related = getattr(attr, "rel", None) + # Django =>1.9 uses 'rel', django <1.9 uses 'related' + related = getattr(attr, "rel", None) or getattr(attr, "related", None) if isinstance(related, models.ManyToOneRel): yield (name, related) elif isinstance(related, models.ManyToManyRel) and not related.symmetrical: From ce6e6dd6e1668631f35c464f4f96818046437c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Roberto=20Meza=20Cabrera?= Date: Sun, 9 Jun 2019 14:15:46 -0500 Subject: [PATCH 15/44] Fixes O2O relations --- graphene_django/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/utils/utils.py b/graphene_django/utils/utils.py index c2a3b09..b8aaba0 100644 --- a/graphene_django/utils/utils.py +++ b/graphene_django/utils/utils.py @@ -18,7 +18,7 @@ def get_reverse_fields(model, local_field_names): if name in local_field_names: continue - # Django =>1.9 uses 'rel', django <1.9 uses 'related' + # "rel" for FK and M2M relations and "related" for O2O Relations related = getattr(attr, "rel", None) or getattr(attr, "related", None) if isinstance(related, models.ManyToOneRel): yield (name, related) From 94602c77c6ebae74c418222291d6b2ca108a2f4d Mon Sep 17 00:00:00 2001 From: mvanlonden Date: Sun, 9 Jun 2019 12:41:04 -0700 Subject: [PATCH 16/44] add reverse relation one to one query test --- graphene_django/debug/tests/test_query.py | 4 +- graphene_django/tests/test_query.py | 56 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/graphene_django/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py index 592899b..af69715 100644 --- a/graphene_django/debug/tests/test_query.py +++ b/graphene_django/debug/tests/test_query.py @@ -50,9 +50,7 @@ def test_should_query_field(): """ expected = { "reporter": {"lastName": "ABA"}, - "_debug": { - "sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}] - }, + "_debug": {"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]}, } schema = graphene.Schema(query=Query) result = schema.execute( diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 58f46c7..36fad9b 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -226,6 +226,62 @@ def test_should_node(): assert result.data == expected +def test_should_query_onetoone_fields(): + film = Film(id=1) + film_details = FilmDetails(id=1, film=film) + + class FilmNode(DjangoObjectType): + class Meta: + model = Film + interfaces = (Node,) + + class FilmDetailsNode(DjangoObjectType): + class Meta: + model = FilmDetails + interfaces = (Node,) + + class Query(graphene.ObjectType): + film = graphene.Field(FilmNode) + film_details = graphene.Field(FilmDetailsNode) + + def resolve_film(root, info): + return film + + def resolve_film_details(root, info): + return film_details + + query = """ + query FilmQuery { + filmDetails { + id + film { + id + } + } + film { + id + details { + id + } + } + } + """ + expected = { + "filmDetails": { + "id": "RmlsbURldGFpbHNOb2RlOjE=", + "film": {"id": "RmlsbU5vZGU6MQ=="}, + }, + "film": { + "id": "RmlsbU5vZGU6MQ==", + "details": {"id": "RmlsbURldGFpbHNOb2RlOjE="}, + }, + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + def test_should_query_connectionfields(): class ReporterType(DjangoObjectType): class Meta: From c90c27f3649c31b434e8220775a93e1334e6aeaa Mon Sep 17 00:00:00 2001 From: kamilkijak Date: Mon, 10 Jun 2019 09:25:34 +1000 Subject: [PATCH 17/44] Add support for write_only fields in SerializerMutation (#555) --- graphene_django/rest_framework/models.py | 5 +++ graphene_django/rest_framework/mutation.py | 5 ++- .../rest_framework/tests/test_mutation.py | 43 ++++++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/graphene_django/rest_framework/models.py b/graphene_django/rest_framework/models.py index 848837b..06d9b60 100644 --- a/graphene_django/rest_framework/models.py +++ b/graphene_django/rest_framework/models.py @@ -4,3 +4,8 @@ from django.db import models class MyFakeModel(models.Model): cool_name = models.CharField(max_length=50) created = models.DateTimeField(auto_now_add=True) + + +class MyFakeModelWithPassword(models.Model): + cool_name = models.CharField(max_length=50) + password = models.CharField(max_length=50) diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index b8025f6..0fe9a02 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -27,6 +27,8 @@ def fields_for_serializer(serializer, only_fields, exclude_fields, is_input=Fals name in exclude_fields # or # name in already_created_fields + ) or ( + field.write_only and not is_input # don't show write_only fields in Query ) if is_not_in_only or is_excluded: @@ -138,6 +140,7 @@ class SerializerMutation(ClientIDMutation): kwargs = {} for f, field in serializer.fields.items(): - kwargs[f] = field.get_attribute(obj) + if not field.write_only: + kwargs[f] = field.get_attribute(obj) return cls(errors=None, **kwargs) diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index 4dccc18..a0c861d 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -7,7 +7,7 @@ from py.test import mark from rest_framework import serializers from ...types import DjangoObjectType -from ..models import MyFakeModel +from ..models import MyFakeModel, MyFakeModelWithPassword from ..mutation import SerializerMutation @@ -86,6 +86,47 @@ def test_exclude_fields(): assert "created" not in MyMutation.Input._meta.fields +@mark.django_db +def test_write_only_field(): + class WriteOnlyFieldModelSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True) + + class Meta: + model = MyFakeModelWithPassword + fields = ["cool_name", "password"] + + class MyMutation(SerializerMutation): + class Meta: + serializer_class = WriteOnlyFieldModelSerializer + + result = MyMutation.mutate_and_get_payload( + None, mock_info(), **{"cool_name": "New Narf", "password": "admin"} + ) + + assert hasattr(result, "cool_name") + assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible" + + +@mark.django_db +def test_write_only_field_using_extra_kwargs(): + class WriteOnlyFieldModelSerializer(serializers.ModelSerializer): + class Meta: + model = MyFakeModelWithPassword + fields = ["cool_name", "password"] + extra_kwargs = {"password": {"write_only": True}} + + class MyMutation(SerializerMutation): + class Meta: + serializer_class = WriteOnlyFieldModelSerializer + + result = MyMutation.mutate_and_get_payload( + None, mock_info(), **{"cool_name": "New Narf", "password": "admin"} + ) + + assert hasattr(result, "cool_name") + assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible" + + def test_nested_model(): class MyFakeModelGrapheneType(DjangoObjectType): class Meta: From 3cde872e2873197ba99de5004d59a8888d5f2223 Mon Sep 17 00:00:00 2001 From: Emil Goldsmith Olesen Date: Mon, 10 Jun 2019 01:30:48 +0200 Subject: [PATCH 18/44] Stop enforcing csrf checks in GraphQLTestCase (#658) --- graphene_django/utils/testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/utils/testing.py b/graphene_django/utils/testing.py index 47f8d04..db3e9f4 100644 --- a/graphene_django/utils/testing.py +++ b/graphene_django/utils/testing.py @@ -22,7 +22,7 @@ class GraphQLTestCase(TestCase): "Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase." ) - cls._client = Client(cls.GRAPHQL_SCHEMA) + cls._client = Client() def query(self, query, op_name=None, input_data=None): """ From f617b2a9c2706202e281be05361834547df7c9fe Mon Sep 17 00:00:00 2001 From: Abraham Toriz Cruz Date: Sun, 9 Jun 2019 19:33:57 -0400 Subject: [PATCH 19/44] django 1.11.19 is not available, probably for security reasons (#652) --- examples/cookbook/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt index 3fed30f1..fe0527a 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -1,5 +1,5 @@ graphene graphene-django graphql-core>=2.1rc1 -django==1.11.19 +django==1.11.20 django-filter>=2 From fcc491fffbf35b506918983019b337c0265b2bc1 Mon Sep 17 00:00:00 2001 From: Emil Goldsmith Olesen Date: Mon, 10 Jun 2019 02:06:50 +0200 Subject: [PATCH 20/44] Add watch option to graphql_schema (#656) * Add watch option to graphql_schema * add documentation for grapql_schema --watch --- docs/introspection.rst | 2 + .../management/commands/graphql_schema.py | 37 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/docs/introspection.rst b/docs/introspection.rst index 0fc6776..c1d6ede 100644 --- a/docs/introspection.rst +++ b/docs/introspection.rst @@ -35,6 +35,8 @@ Advanced Usage The ``--indent`` option can be used to specify the number of indentation spaces to be used in the output. Defaults to `None` which displays all data on a single line. +The ``--watch`` option can be used to run ``./manage.py graphql_schema`` in watch mode, where it will automatically output a new schema every time there are file changes in your project + To simplify the command to ``./manage.py graphql_schema``, you can specify the parameters in your settings.py: diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index 9f8689e..1e8baf6 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -1,7 +1,9 @@ import importlib import json +import functools from django.core.management.base import BaseCommand, CommandError +from django.utils import autoreload from graphene_django.settings import graphene_settings @@ -32,6 +34,14 @@ class CommandArguments(BaseCommand): help="Output file indent (default: None)", ) + parser.add_argument( + "--watch", + dest="watch", + default=False, + action="store_true", + help="Updates the schema on file changes (default: False)", + ) + class Command(CommandArguments): help = "Dump Graphene schema JSON to file" @@ -41,6 +51,18 @@ class Command(CommandArguments): with open(out, "w") as outfile: json.dump(schema_dict, outfile, indent=indent, sort_keys=True) + def get_schema(self, schema, out, indent): + schema_dict = {"data": schema.introspect()} + if out == "-": + self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True)) + else: + self.save_file(out, schema_dict, indent) + + style = getattr(self, "style", None) + success = getattr(style, "SUCCESS", lambda x: x) + + self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out)) + def handle(self, *args, **options): options_schema = options.get("schema") @@ -63,13 +85,10 @@ class Command(CommandArguments): ) indent = options.get("indent") - schema_dict = {"data": schema.introspect()} - if out == "-": - self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True)) + watch = options.get("watch") + if watch: + autoreload.run_with_reloader( + functools.partial(self.get_schema, schema, out, indent) + ) else: - self.save_file(out, schema_dict, indent) - - style = getattr(self, "style", None) - success = getattr(style, "SUCCESS", lambda x: x) - - self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out)) + self.get_schema(schema, out, indent) From 96934c46141de56c4c607625f56f785ea5276387 Mon Sep 17 00:00:00 2001 From: Alexandre Kirszenberg Date: Mon, 10 Jun 2019 02:19:05 +0200 Subject: [PATCH 21/44] Correctly propagate help_text as description for many-to-* relations (#579) * Correctly propagate help_text as description for many-to-* relations * Trigger build --- graphene_django/converter.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 6fc1227..158355a 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -177,6 +177,8 @@ def convert_field_to_list_or_connection(field, registry=None): if not _type: return + description = field.help_text if isinstance(field, models.ManyToManyField) else field.field.help_text + # If there is a connection, we should transform the field # into a DjangoConnectionField if _type._meta.connection: @@ -186,11 +188,11 @@ def convert_field_to_list_or_connection(field, registry=None): if _type._meta.filter_fields or _type._meta.filterset_class: from .filter.fields import DjangoFilterConnectionField - return DjangoFilterConnectionField(_type) + return DjangoFilterConnectionField(_type, description=description) - return DjangoConnectionField(_type) + return DjangoConnectionField(_type, description=description) - return DjangoListField(_type) + return DjangoListField(_type, description=description) return Dynamic(dynamic_type) From 44e9b0d0c584fde3028acdf132b4e70de7e9e7f1 Mon Sep 17 00:00:00 2001 From: Mel van Londen Date: Mon, 10 Jun 2019 11:08:41 -0700 Subject: [PATCH 22/44] Add stale bot (#661) --- .github/stale.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..dc90e5a --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false From 775d2e35233f14123cea127dbd7b79d53e8e4420 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Mon, 10 Jun 2019 20:54:30 -0700 Subject: [PATCH 23/44] Update travis and tox (#667) * Update travis and tox * Use xenial distribution * Don't install coveralls twice * Add black and flake8 tox commands * Remove Python 3.5 test for Django master * Fix indent * Ignore migrations * Remove black for now * Run black formatting (#668) * Run black format * Update makefile * Add black to travis build --- .travis.yml | 80 +++++++-------- Makefile | 4 +- .../cookbook/ingredients/admin.py | 4 +- .../cookbook/ingredients/apps.py | 6 +- .../cookbook/ingredients/models.py | 7 +- .../cookbook/ingredients/schema.py | 12 +-- .../cookbook/ingredients/tests.py | 1 - .../cookbook/ingredients/views.py | 1 - .../cookbook-plain/cookbook/recipes/apps.py | 6 +- .../cookbook-plain/cookbook/recipes/models.py | 22 +++-- .../cookbook-plain/cookbook/recipes/schema.py | 9 +- .../cookbook-plain/cookbook/recipes/tests.py | 1 - .../cookbook-plain/cookbook/recipes/views.py | 1 - examples/cookbook-plain/cookbook/schema.py | 10 +- examples/cookbook-plain/cookbook/settings.py | 95 ++++++++---------- examples/cookbook-plain/cookbook/urls.py | 4 +- .../cookbook/cookbook/ingredients/admin.py | 4 +- .../cookbook/cookbook/ingredients/apps.py | 6 +- .../cookbook/cookbook/ingredients/models.py | 2 +- .../cookbook/cookbook/ingredients/schema.py | 16 ++- .../cookbook/cookbook/ingredients/tests.py | 1 - .../cookbook/cookbook/ingredients/views.py | 1 - examples/cookbook/cookbook/recipes/apps.py | 6 +- examples/cookbook/cookbook/recipes/models.py | 19 ++-- examples/cookbook/cookbook/recipes/schema.py | 15 ++- examples/cookbook/cookbook/recipes/tests.py | 1 - examples/cookbook/cookbook/recipes/views.py | 1 - examples/cookbook/cookbook/schema.py | 10 +- examples/cookbook/cookbook/settings.py | 97 +++++++++---------- examples/cookbook/cookbook/urls.py | 4 +- examples/starwars/data.py | 73 +++----------- examples/starwars/models.py | 10 +- examples/starwars/schema.py | 17 ++-- examples/starwars/tests/test_connections.py | 35 ++----- examples/starwars/tests/test_mutation.py | 58 +++-------- .../tests/test_objectidentification.py | 51 +++------- graphene_django/converter.py | 6 +- graphene_django/filter/fields.py | 5 +- graphene_django/filter/tests/test_fields.py | 1 + .../rest_framework/tests/test_mutation.py | 8 +- graphene_django/tests/test_query.py | 4 +- graphene_django/types.py | 10 +- tox.ini | 46 +++++---- 43 files changed, 331 insertions(+), 439 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5c4725f..871d4e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,58 +1,58 @@ language: python -sudo: required +cache: pip dist: xenial -python: - - 2.7 - - 3.4 - - 3.5 - - 3.6 - - 3.7 - -env: - matrix: - - DJANGO=1.11 - - DJANGO=2.1 - - DJANGO=2.2 - - DJANGO=master - install: - - TOX_ENV=py${TRAVIS_PYTHON_VERSION}-django${DJANGO} - - pip install tox - - tox -e $TOX_ENV --notest -script: - - tox -e $TOX_ENV + - pip install tox tox-travis -after_success: - - tox -e $TOX_ENV -- pip install coveralls - - tox -e $TOX_ENV -- coveralls $COVERALLS_OPTION +script: + - tox + +after_success: + - pip install coveralls + - coveralls matrix: fast_finish: true include: - - python: 3.5 - script: tox -e lint - exclude: - python: 2.7 - env: DJANGO=2.1 - - python: 2.7 - env: DJANGO=2.2 - - python: 2.7 - env: DJANGO=master - - python: 3.4 - env: DJANGO=2.1 - - python: 3.4 - env: DJANGO=2.2 - - python: 3.4 - env: DJANGO=master + env: DJANGO=1.11 + - python: 3.5 + env: DJANGO=1.11 + - python: 3.5 + env: DJANGO=2.0 + - python: 3.5 + env: DJANGO=2.1 + - python: 3.5 + env: DJANGO=2.2 + + - python: 3.6 + env: DJANGO=1.11 + - python: 3.6 + env: DJANGO=2.0 + - python: 3.6 + env: DJANGO=2.1 + - python: 3.6 + env: DJANGO=2.2 + - python: 3.6 env: DJANGO=master - - python: 3.7 - env: DJANGO=1.10 + - python: 3.7 env: DJANGO=1.11 - allow_failures: - python: 3.7 + env: DJANGO=2.0 + - python: 3.7 + env: DJANGO=2.1 + - python: 3.7 + env: DJANGO=2.2 + - python: 3.7 + env: DJANGO=master + + - python: 3.7 + env: TOXENV=black,flake8 + + allow_failures: - env: DJANGO=master deploy: diff --git a/Makefile b/Makefile index 061ad4e..70badcb 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ tests: py.test graphene_django --cov=graphene_django -vv format: - black graphene_django + black --exclude "/migrations/" graphene_django examples lint: - flake8 graphene_django + flake8 graphene_django examples diff --git a/examples/cookbook-plain/cookbook/ingredients/admin.py b/examples/cookbook-plain/cookbook/ingredients/admin.py index b57cbc3..042682f 100644 --- a/examples/cookbook-plain/cookbook/ingredients/admin.py +++ b/examples/cookbook-plain/cookbook/ingredients/admin.py @@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient @admin.register(Ingredient) class IngredientAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'category') - list_editable = ('name', 'category') + list_display = ("id", "name", "category") + list_editable = ("name", "category") admin.site.register(Category) diff --git a/examples/cookbook-plain/cookbook/ingredients/apps.py b/examples/cookbook-plain/cookbook/ingredients/apps.py index 21b4b08..3ad0143 100644 --- a/examples/cookbook-plain/cookbook/ingredients/apps.py +++ b/examples/cookbook-plain/cookbook/ingredients/apps.py @@ -2,6 +2,6 @@ from django.apps import AppConfig class IngredientsConfig(AppConfig): - name = 'cookbook.ingredients' - label = 'ingredients' - verbose_name = 'Ingredients' + name = "cookbook.ingredients" + label = "ingredients" + verbose_name = "Ingredients" diff --git a/examples/cookbook-plain/cookbook/ingredients/models.py b/examples/cookbook-plain/cookbook/ingredients/models.py index 5836949..5d88785 100644 --- a/examples/cookbook-plain/cookbook/ingredients/models.py +++ b/examples/cookbook-plain/cookbook/ingredients/models.py @@ -3,7 +3,8 @@ from django.db import models class Category(models.Model): class Meta: - verbose_name_plural = 'Categories' + verbose_name_plural = "Categories" + name = models.CharField(max_length=100) def __str__(self): @@ -13,7 +14,9 @@ class Category(models.Model): class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField(null=True, blank=True) - category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE) + category = models.ForeignKey( + Category, related_name="ingredients", on_delete=models.CASCADE + ) def __str__(self): return self.name diff --git a/examples/cookbook-plain/cookbook/ingredients/schema.py b/examples/cookbook-plain/cookbook/ingredients/schema.py index e7ef688..1a54c4b 100644 --- a/examples/cookbook-plain/cookbook/ingredients/schema.py +++ b/examples/cookbook-plain/cookbook/ingredients/schema.py @@ -15,14 +15,12 @@ class IngredientType(DjangoObjectType): class Query(object): - category = graphene.Field(CategoryType, - id=graphene.Int(), - name=graphene.String()) + category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) all_categories = graphene.List(CategoryType) - ingredient = graphene.Field(IngredientType, - id=graphene.Int(), - name=graphene.String()) + ingredient = graphene.Field( + IngredientType, id=graphene.Int(), name=graphene.String() + ) all_ingredients = graphene.List(IngredientType) def resolve_all_categories(self, context): @@ -30,7 +28,7 @@ class Query(object): def resolve_all_ingredients(self, context): # We can easily optimize query count in the resolve method - return Ingredient.objects.select_related('category').all() + return Ingredient.objects.select_related("category").all() def resolve_category(self, context, id=None, name=None): if id is not None: diff --git a/examples/cookbook-plain/cookbook/ingredients/tests.py b/examples/cookbook-plain/cookbook/ingredients/tests.py index 4929020..a39b155 100644 --- a/examples/cookbook-plain/cookbook/ingredients/tests.py +++ b/examples/cookbook-plain/cookbook/ingredients/tests.py @@ -1,2 +1 @@ - # Create your tests here. diff --git a/examples/cookbook-plain/cookbook/ingredients/views.py b/examples/cookbook-plain/cookbook/ingredients/views.py index b8e4ee0..60f00ef 100644 --- a/examples/cookbook-plain/cookbook/ingredients/views.py +++ b/examples/cookbook-plain/cookbook/ingredients/views.py @@ -1,2 +1 @@ - # Create your views here. diff --git a/examples/cookbook-plain/cookbook/recipes/apps.py b/examples/cookbook-plain/cookbook/recipes/apps.py index 1f24f13..f1e4dde 100644 --- a/examples/cookbook-plain/cookbook/recipes/apps.py +++ b/examples/cookbook-plain/cookbook/recipes/apps.py @@ -2,6 +2,6 @@ from django.apps import AppConfig class RecipesConfig(AppConfig): - name = 'cookbook.recipes' - label = 'recipes' - verbose_name = 'Recipes' + name = "cookbook.recipes" + label = "recipes" + verbose_name = "Recipes" diff --git a/examples/cookbook-plain/cookbook/recipes/models.py b/examples/cookbook-plain/cookbook/recipes/models.py index 382b88e..f6e955e 100644 --- a/examples/cookbook-plain/cookbook/recipes/models.py +++ b/examples/cookbook-plain/cookbook/recipes/models.py @@ -6,17 +6,23 @@ from ..ingredients.models import Ingredient class Recipe(models.Model): title = models.CharField(max_length=100) instructions = models.TextField() + def __str__(self): return self.title class RecipeIngredient(models.Model): - recipe = models.ForeignKey(Recipe, related_name='amounts', on_delete=models.CASCADE) - ingredient = models.ForeignKey(Ingredient, related_name='used_by', on_delete=models.CASCADE) + recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE) + ingredient = models.ForeignKey( + Ingredient, related_name="used_by", on_delete=models.CASCADE + ) amount = models.FloatField() - unit = models.CharField(max_length=20, choices=( - ('unit', 'Units'), - ('kg', 'Kilograms'), - ('l', 'Litres'), - ('st', 'Shots'), - )) + unit = models.CharField( + max_length=20, + choices=( + ("unit", "Units"), + ("kg", "Kilograms"), + ("l", "Litres"), + ("st", "Shots"), + ), + ) diff --git a/examples/cookbook-plain/cookbook/recipes/schema.py b/examples/cookbook-plain/cookbook/recipes/schema.py index 74692f8..b029570 100644 --- a/examples/cookbook-plain/cookbook/recipes/schema.py +++ b/examples/cookbook-plain/cookbook/recipes/schema.py @@ -15,13 +15,10 @@ class RecipeIngredientType(DjangoObjectType): class Query(object): - recipe = graphene.Field(RecipeType, - id=graphene.Int(), - title=graphene.String()) + recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String()) all_recipes = graphene.List(RecipeType) - recipeingredient = graphene.Field(RecipeIngredientType, - id=graphene.Int()) + recipeingredient = graphene.Field(RecipeIngredientType, id=graphene.Int()) all_recipeingredients = graphene.List(RecipeIngredientType) def resolve_recipe(self, context, id=None, title=None): @@ -43,5 +40,5 @@ class Query(object): return Recipe.objects.all() def resolve_all_recipeingredients(self, context): - related = ['recipe', 'ingredient'] + related = ["recipe", "ingredient"] return RecipeIngredient.objects.select_related(*related).all() diff --git a/examples/cookbook-plain/cookbook/recipes/tests.py b/examples/cookbook-plain/cookbook/recipes/tests.py index 4929020..a39b155 100644 --- a/examples/cookbook-plain/cookbook/recipes/tests.py +++ b/examples/cookbook-plain/cookbook/recipes/tests.py @@ -1,2 +1 @@ - # Create your tests here. diff --git a/examples/cookbook-plain/cookbook/recipes/views.py b/examples/cookbook-plain/cookbook/recipes/views.py index b8e4ee0..60f00ef 100644 --- a/examples/cookbook-plain/cookbook/recipes/views.py +++ b/examples/cookbook-plain/cookbook/recipes/views.py @@ -1,2 +1 @@ - # Create your views here. diff --git a/examples/cookbook-plain/cookbook/schema.py b/examples/cookbook-plain/cookbook/schema.py index f91d62c..bde9372 100644 --- a/examples/cookbook-plain/cookbook/schema.py +++ b/examples/cookbook-plain/cookbook/schema.py @@ -5,10 +5,12 @@ import graphene from graphene_django.debug import DjangoDebug -class Query(cookbook.ingredients.schema.Query, - cookbook.recipes.schema.Query, - graphene.ObjectType): - debug = graphene.Field(DjangoDebug, name='_debug') +class Query( + cookbook.ingredients.schema.Query, + cookbook.recipes.schema.Query, + graphene.ObjectType, +): + debug = graphene.Field(DjangoDebug, name="_debug") schema = graphene.Schema(query=Query) diff --git a/examples/cookbook-plain/cookbook/settings.py b/examples/cookbook-plain/cookbook/settings.py index bce2bab..7eb9d56 100644 --- a/examples/cookbook-plain/cookbook/settings.py +++ b/examples/cookbook-plain/cookbook/settings.py @@ -21,7 +21,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4' +SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -32,64 +32,61 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'graphene_django', - - 'cookbook.ingredients.apps.IngredientsConfig', - 'cookbook.recipes.apps.RecipesConfig', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "graphene_django", + "cookbook.ingredients.apps.IngredientsConfig", + "cookbook.recipes.apps.RecipesConfig", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] GRAPHENE = { - 'SCHEMA': 'cookbook.schema.schema', - 'SCHEMA_INDENT': 2, - 'MIDDLEWARE': ( - 'graphene_django.debug.DjangoDebugMiddleware', - ) + "SCHEMA": "cookbook.schema.schema", + "SCHEMA_INDENT": 2, + "MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",), } -ROOT_URLCONF = 'cookbook.urls' +ROOT_URLCONF = "cookbook.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ] }, - }, + } ] -WSGI_APPLICATION = 'cookbook.wsgi.application' +WSGI_APPLICATION = "cookbook.wsgi.application" # Database # https://docs.djangoproject.com/en/1.9/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } @@ -99,26 +96,20 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" }, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] # Internationalization # https://docs.djangoproject.com/en/1.9/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -130,4 +121,4 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" diff --git a/examples/cookbook-plain/cookbook/urls.py b/examples/cookbook-plain/cookbook/urls.py index 4f87da0..a64a875 100644 --- a/examples/cookbook-plain/cookbook/urls.py +++ b/examples/cookbook-plain/cookbook/urls.py @@ -5,6 +5,6 @@ from graphene_django.views import GraphQLView urlpatterns = [ - path('admin/', admin.site.urls), - path('graphql/', GraphQLView.as_view(graphiql=True)), + path("admin/", admin.site.urls), + path("graphql/", GraphQLView.as_view(graphiql=True)), ] diff --git a/examples/cookbook/cookbook/ingredients/admin.py b/examples/cookbook/cookbook/ingredients/admin.py index b57cbc3..042682f 100644 --- a/examples/cookbook/cookbook/ingredients/admin.py +++ b/examples/cookbook/cookbook/ingredients/admin.py @@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient @admin.register(Ingredient) class IngredientAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'category') - list_editable = ('name', 'category') + list_display = ("id", "name", "category") + list_editable = ("name", "category") admin.site.register(Category) diff --git a/examples/cookbook/cookbook/ingredients/apps.py b/examples/cookbook/cookbook/ingredients/apps.py index 21b4b08..3ad0143 100644 --- a/examples/cookbook/cookbook/ingredients/apps.py +++ b/examples/cookbook/cookbook/ingredients/apps.py @@ -2,6 +2,6 @@ from django.apps import AppConfig class IngredientsConfig(AppConfig): - name = 'cookbook.ingredients' - label = 'ingredients' - verbose_name = 'Ingredients' + name = "cookbook.ingredients" + label = "ingredients" + verbose_name = "Ingredients" diff --git a/examples/cookbook/cookbook/ingredients/models.py b/examples/cookbook/cookbook/ingredients/models.py index 2f0eba3..6426dab 100644 --- a/examples/cookbook/cookbook/ingredients/models.py +++ b/examples/cookbook/cookbook/ingredients/models.py @@ -11,7 +11,7 @@ class Category(models.Model): class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField(null=True, blank=True) - category = models.ForeignKey(Category, related_name='ingredients') + category = models.ForeignKey(Category, related_name="ingredients") def __str__(self): return self.name diff --git a/examples/cookbook/cookbook/ingredients/schema.py b/examples/cookbook/cookbook/ingredients/schema.py index 5ad92e8..5e5da80 100644 --- a/examples/cookbook/cookbook/ingredients/schema.py +++ b/examples/cookbook/cookbook/ingredients/schema.py @@ -7,24 +7,22 @@ from graphene_django.types import DjangoObjectType # Graphene will automatically map the Category model's fields onto the CategoryNode. # This is configured in the CategoryNode's Meta class (as you can see below) class CategoryNode(DjangoObjectType): - class Meta: model = Category - interfaces = (Node, ) - filter_fields = ['name', 'ingredients'] + interfaces = (Node,) + filter_fields = ["name", "ingredients"] class IngredientNode(DjangoObjectType): - class Meta: model = Ingredient # Allow for some more advanced filtering here - interfaces = (Node, ) + interfaces = (Node,) filter_fields = { - 'name': ['exact', 'icontains', 'istartswith'], - 'notes': ['exact', 'icontains'], - 'category': ['exact'], - 'category__name': ['exact'], + "name": ["exact", "icontains", "istartswith"], + "notes": ["exact", "icontains"], + "category": ["exact"], + "category__name": ["exact"], } diff --git a/examples/cookbook/cookbook/ingredients/tests.py b/examples/cookbook/cookbook/ingredients/tests.py index 4929020..a39b155 100644 --- a/examples/cookbook/cookbook/ingredients/tests.py +++ b/examples/cookbook/cookbook/ingredients/tests.py @@ -1,2 +1 @@ - # Create your tests here. diff --git a/examples/cookbook/cookbook/ingredients/views.py b/examples/cookbook/cookbook/ingredients/views.py index b8e4ee0..60f00ef 100644 --- a/examples/cookbook/cookbook/ingredients/views.py +++ b/examples/cookbook/cookbook/ingredients/views.py @@ -1,2 +1 @@ - # Create your views here. diff --git a/examples/cookbook/cookbook/recipes/apps.py b/examples/cookbook/cookbook/recipes/apps.py index 1f24f13..f1e4dde 100644 --- a/examples/cookbook/cookbook/recipes/apps.py +++ b/examples/cookbook/cookbook/recipes/apps.py @@ -2,6 +2,6 @@ from django.apps import AppConfig class RecipesConfig(AppConfig): - name = 'cookbook.recipes' - label = 'recipes' - verbose_name = 'Recipes' + name = "cookbook.recipes" + label = "recipes" + verbose_name = "Recipes" diff --git a/examples/cookbook/cookbook/recipes/models.py b/examples/cookbook/cookbook/recipes/models.py index ca12fac..b98664c 100644 --- a/examples/cookbook/cookbook/recipes/models.py +++ b/examples/cookbook/cookbook/recipes/models.py @@ -10,12 +10,15 @@ class Recipe(models.Model): class RecipeIngredient(models.Model): - recipe = models.ForeignKey(Recipe, related_name='amounts') - ingredient = models.ForeignKey(Ingredient, related_name='used_by') + recipe = models.ForeignKey(Recipe, related_name="amounts") + ingredient = models.ForeignKey(Ingredient, related_name="used_by") amount = models.FloatField() - unit = models.CharField(max_length=20, choices=( - ('unit', 'Units'), - ('kg', 'Kilograms'), - ('l', 'Litres'), - ('st', 'Shots'), - )) + unit = models.CharField( + max_length=20, + choices=( + ("unit", "Units"), + ("kg", "Kilograms"), + ("l", "Litres"), + ("st", "Shots"), + ), + ) diff --git a/examples/cookbook/cookbook/recipes/schema.py b/examples/cookbook/cookbook/recipes/schema.py index 8018322..fbbedd8 100644 --- a/examples/cookbook/cookbook/recipes/schema.py +++ b/examples/cookbook/cookbook/recipes/schema.py @@ -3,24 +3,23 @@ from graphene import Node from graphene_django.filter import DjangoFilterConnectionField from graphene_django.types import DjangoObjectType -class RecipeNode(DjangoObjectType): +class RecipeNode(DjangoObjectType): class Meta: model = Recipe - interfaces = (Node, ) - filter_fields = ['title','amounts'] + interfaces = (Node,) + filter_fields = ["title", "amounts"] class RecipeIngredientNode(DjangoObjectType): - class Meta: model = RecipeIngredient # Allow for some more advanced filtering here - interfaces = (Node, ) + interfaces = (Node,) filter_fields = { - 'ingredient__name': ['exact', 'icontains', 'istartswith'], - 'recipe': ['exact'], - 'recipe__title': ['icontains'], + "ingredient__name": ["exact", "icontains", "istartswith"], + "recipe": ["exact"], + "recipe__title": ["icontains"], } diff --git a/examples/cookbook/cookbook/recipes/tests.py b/examples/cookbook/cookbook/recipes/tests.py index 4929020..a39b155 100644 --- a/examples/cookbook/cookbook/recipes/tests.py +++ b/examples/cookbook/cookbook/recipes/tests.py @@ -1,2 +1 @@ - # Create your tests here. diff --git a/examples/cookbook/cookbook/recipes/views.py b/examples/cookbook/cookbook/recipes/views.py index b8e4ee0..60f00ef 100644 --- a/examples/cookbook/cookbook/recipes/views.py +++ b/examples/cookbook/cookbook/recipes/views.py @@ -1,2 +1 @@ - # Create your views here. diff --git a/examples/cookbook/cookbook/schema.py b/examples/cookbook/cookbook/schema.py index f91d62c..bde9372 100644 --- a/examples/cookbook/cookbook/schema.py +++ b/examples/cookbook/cookbook/schema.py @@ -5,10 +5,12 @@ import graphene from graphene_django.debug import DjangoDebug -class Query(cookbook.ingredients.schema.Query, - cookbook.recipes.schema.Query, - graphene.ObjectType): - debug = graphene.Field(DjangoDebug, name='_debug') +class Query( + cookbook.ingredients.schema.Query, + cookbook.recipes.schema.Query, + graphene.ObjectType, +): + debug = graphene.Field(DjangoDebug, name="_debug") schema = graphene.Schema(query=Query) diff --git a/examples/cookbook/cookbook/settings.py b/examples/cookbook/cookbook/settings.py index 0b3207e..ed41a65 100644 --- a/examples/cookbook/cookbook/settings.py +++ b/examples/cookbook/cookbook/settings.py @@ -21,7 +21,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4' +SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -32,65 +32,62 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'graphene_django', - - 'cookbook.ingredients.apps.IngredientsConfig', - 'cookbook.recipes.apps.RecipesConfig', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "graphene_django", + "cookbook.ingredients.apps.IngredientsConfig", + "cookbook.recipes.apps.RecipesConfig", ] MIDDLEWARE_CLASSES = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.auth.middleware.SessionAuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] GRAPHENE = { - 'SCHEMA': 'cookbook.schema.schema', - 'SCHEMA_INDENT': 2, - 'MIDDLEWARE': ( - 'graphene_django.debug.DjangoDebugMiddleware', - ) + "SCHEMA": "cookbook.schema.schema", + "SCHEMA_INDENT": 2, + "MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",), } -ROOT_URLCONF = 'cookbook.urls' +ROOT_URLCONF = "cookbook.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ] }, - }, + } ] -WSGI_APPLICATION = 'cookbook.wsgi.application' +WSGI_APPLICATION = "cookbook.wsgi.application" # Database # https://docs.djangoproject.com/en/1.9/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } @@ -100,26 +97,20 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" }, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] # Internationalization # https://docs.djangoproject.com/en/1.9/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -131,4 +122,4 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" diff --git a/examples/cookbook/cookbook/urls.py b/examples/cookbook/cookbook/urls.py index 4bf6003..6f8a302 100644 --- a/examples/cookbook/cookbook/urls.py +++ b/examples/cookbook/cookbook/urls.py @@ -5,6 +5,6 @@ from graphene_django.views import GraphQLView urlpatterns = [ - url(r'^admin/', admin.site.urls), - url(r'^graphql$', GraphQLView.as_view(graphiql=True)), + url(r"^admin/", admin.site.urls), + url(r"^graphql$", GraphQLView.as_view(graphiql=True)), ] diff --git a/examples/starwars/data.py b/examples/starwars/data.py index 9b52006..6bdbf57 100644 --- a/examples/starwars/data.py +++ b/examples/starwars/data.py @@ -2,97 +2,50 @@ from .models import Character, Faction, Ship def initialize(): - human = Character( - name='Human' - ) + human = Character(name="Human") human.save() - droid = Character( - name='Droid' - ) + droid = Character(name="Droid") droid.save() - rebels = Faction( - id='1', - name='Alliance to Restore the Republic', - hero=human - ) + rebels = Faction(id="1", name="Alliance to Restore the Republic", hero=human) rebels.save() - empire = Faction( - id='2', - name='Galactic Empire', - hero=droid - ) + empire = Faction(id="2", name="Galactic Empire", hero=droid) empire.save() - xwing = Ship( - id='1', - name='X-Wing', - faction=rebels, - ) + xwing = Ship(id="1", name="X-Wing", faction=rebels) xwing.save() human.ship = xwing human.save() - ywing = Ship( - id='2', - name='Y-Wing', - faction=rebels, - ) + ywing = Ship(id="2", name="Y-Wing", faction=rebels) ywing.save() - awing = Ship( - id='3', - name='A-Wing', - faction=rebels, - ) + awing = Ship(id="3", name="A-Wing", faction=rebels) awing.save() # Yeah, technically it's Corellian. But it flew in the service of the rebels, # so for the purposes of this demo it's a rebel ship. - falcon = Ship( - id='4', - name='Millenium Falcon', - faction=rebels, - ) + falcon = Ship(id="4", name="Millenium Falcon", faction=rebels) falcon.save() - homeOne = Ship( - id='5', - name='Home One', - faction=rebels, - ) + homeOne = Ship(id="5", name="Home One", faction=rebels) homeOne.save() - tieFighter = Ship( - id='6', - name='TIE Fighter', - faction=empire, - ) + tieFighter = Ship(id="6", name="TIE Fighter", faction=empire) tieFighter.save() - tieInterceptor = Ship( - id='7', - name='TIE Interceptor', - faction=empire, - ) + tieInterceptor = Ship(id="7", name="TIE Interceptor", faction=empire) tieInterceptor.save() - executor = Ship( - id='8', - name='Executor', - faction=empire, - ) + executor = Ship(id="8", name="Executor", faction=empire) executor.save() def create_ship(ship_name, faction_id): - new_ship = Ship( - name=ship_name, - faction_id=faction_id - ) + new_ship = Ship(name=ship_name, faction_id=faction_id) new_ship.save() return new_ship diff --git a/examples/starwars/models.py b/examples/starwars/models.py index 45741da..03e06a2 100644 --- a/examples/starwars/models.py +++ b/examples/starwars/models.py @@ -5,7 +5,13 @@ from django.db import models class Character(models.Model): name = models.CharField(max_length=50) - ship = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True, related_name='characters') + ship = models.ForeignKey( + "Ship", + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="characters", + ) def __str__(self): return self.name @@ -21,7 +27,7 @@ class Faction(models.Model): class Ship(models.Model): name = models.CharField(max_length=50) - faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name='ships') + faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name="ships") def __str__(self): return self.name diff --git a/examples/starwars/schema.py b/examples/starwars/schema.py index 492918e..fb22840 100644 --- a/examples/starwars/schema.py +++ b/examples/starwars/schema.py @@ -2,18 +2,16 @@ import graphene from graphene import Schema, relay, resolve_only_args from graphene_django import DjangoConnectionField, DjangoObjectType -from .data import (create_ship, get_empire, get_faction, get_rebels, get_ship, - get_ships) +from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships from .models import Character as CharacterModel from .models import Faction as FactionModel from .models import Ship as ShipModel class Ship(DjangoObjectType): - class Meta: model = ShipModel - interfaces = (relay.Node, ) + interfaces = (relay.Node,) @classmethod def get_node(cls, info, id): @@ -22,16 +20,14 @@ class Ship(DjangoObjectType): class Character(DjangoObjectType): - class Meta: model = CharacterModel class Faction(DjangoObjectType): - class Meta: model = FactionModel - interfaces = (relay.Node, ) + interfaces = (relay.Node,) @classmethod def get_node(cls, info, id): @@ -39,7 +35,6 @@ class Faction(DjangoObjectType): class IntroduceShip(relay.ClientIDMutation): - class Input: ship_name = graphene.String(required=True) faction_id = graphene.String(required=True) @@ -48,7 +43,9 @@ class IntroduceShip(relay.ClientIDMutation): faction = graphene.Field(Faction) @classmethod - def mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None): + def mutate_and_get_payload( + cls, root, info, ship_name, faction_id, client_mutation_id=None + ): ship = create_ship(ship_name, faction_id) faction = get_faction(faction_id) return IntroduceShip(ship=ship, faction=faction) @@ -58,7 +55,7 @@ class Query(graphene.ObjectType): rebels = graphene.Field(Faction) empire = graphene.Field(Faction) node = relay.Node.Field() - ships = DjangoConnectionField(Ship, description='All the ships.') + ships = DjangoConnectionField(Ship, description="All the ships.") @resolve_only_args def resolve_ships(self): diff --git a/examples/starwars/tests/test_connections.py b/examples/starwars/tests/test_connections.py index d266df3..425dce5 100644 --- a/examples/starwars/tests/test_connections.py +++ b/examples/starwars/tests/test_connections.py @@ -8,7 +8,7 @@ pytestmark = pytest.mark.django_db def test_correct_fetch_first_ship_rebels(): initialize() - query = ''' + query = """ query RebelsShipsQuery { rebels { name, @@ -24,22 +24,12 @@ def test_correct_fetch_first_ship_rebels(): } } } - ''' + """ expected = { - 'rebels': { - 'name': 'Alliance to Restore the Republic', - 'hero': { - 'name': 'Human' - }, - 'ships': { - 'edges': [ - { - 'node': { - 'name': 'X-Wing' - } - } - ] - } + "rebels": { + "name": "Alliance to Restore the Republic", + "hero": {"name": "Human"}, + "ships": {"edges": [{"node": {"name": "X-Wing"}}]}, } } result = schema.execute(query) @@ -49,7 +39,7 @@ def test_correct_fetch_first_ship_rebels(): def test_correct_list_characters(): initialize() - query = ''' + query = """ query RebelsShipsQuery { node(id: "U2hpcDox") { ... on Ship { @@ -60,15 +50,8 @@ def test_correct_list_characters(): } } } - ''' - expected = { - 'node': { - 'name': 'X-Wing', - 'characters': [{ - 'name': 'Human' - }], - } - } + """ + expected = {"node": {"name": "X-Wing", "characters": [{"name": "Human"}]}} result = schema.execute(query) assert not result.errors assert result.data == expected diff --git a/examples/starwars/tests/test_mutation.py b/examples/starwars/tests/test_mutation.py index aa312ff..e24bf8a 100644 --- a/examples/starwars/tests/test_mutation.py +++ b/examples/starwars/tests/test_mutation.py @@ -9,7 +9,7 @@ pytestmark = pytest.mark.django_db def test_mutations(): initialize() - query = ''' + query = """ mutation MyMutation { introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) { ship { @@ -29,49 +29,23 @@ def test_mutations(): } } } - ''' + """ expected = { - 'introduceShip': { - 'ship': { - 'id': 'U2hpcDo5', - 'name': 'Peter' - }, - 'faction': { - 'name': 'Alliance to Restore the Republic', - 'ships': { - 'edges': [{ - 'node': { - 'id': 'U2hpcDox', - 'name': 'X-Wing' - } - }, { - 'node': { - 'id': 'U2hpcDoy', - 'name': 'Y-Wing' - } - }, { - 'node': { - 'id': 'U2hpcDoz', - 'name': 'A-Wing' - } - }, { - 'node': { - 'id': 'U2hpcDo0', - 'name': 'Millenium Falcon' - } - }, { - 'node': { - 'id': 'U2hpcDo1', - 'name': 'Home One' - } - }, { - 'node': { - 'id': 'U2hpcDo5', - 'name': 'Peter' - } - }] + "introduceShip": { + "ship": {"id": "U2hpcDo5", "name": "Peter"}, + "faction": { + "name": "Alliance to Restore the Republic", + "ships": { + "edges": [ + {"node": {"id": "U2hpcDox", "name": "X-Wing"}}, + {"node": {"id": "U2hpcDoy", "name": "Y-Wing"}}, + {"node": {"id": "U2hpcDoz", "name": "A-Wing"}}, + {"node": {"id": "U2hpcDo0", "name": "Millenium Falcon"}}, + {"node": {"id": "U2hpcDo1", "name": "Home One"}}, + {"node": {"id": "U2hpcDo5", "name": "Peter"}}, + ] }, - } + }, } } result = schema.execute(query) diff --git a/examples/starwars/tests/test_objectidentification.py b/examples/starwars/tests/test_objectidentification.py index fad1958..6e04a7b 100644 --- a/examples/starwars/tests/test_objectidentification.py +++ b/examples/starwars/tests/test_objectidentification.py @@ -8,19 +8,16 @@ pytestmark = pytest.mark.django_db def test_correctly_fetches_id_name_rebels(): initialize() - query = ''' + query = """ query RebelsQuery { rebels { id name } } - ''' + """ expected = { - 'rebels': { - 'id': 'RmFjdGlvbjox', - 'name': 'Alliance to Restore the Republic' - } + "rebels": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"} } result = schema.execute(query) assert not result.errors @@ -29,7 +26,7 @@ def test_correctly_fetches_id_name_rebels(): def test_correctly_refetches_rebels(): initialize() - query = ''' + query = """ query RebelsRefetchQuery { node(id: "RmFjdGlvbjox") { id @@ -38,12 +35,9 @@ def test_correctly_refetches_rebels(): } } } - ''' + """ expected = { - 'node': { - 'id': 'RmFjdGlvbjox', - 'name': 'Alliance to Restore the Republic' - } + "node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"} } result = schema.execute(query) assert not result.errors @@ -52,20 +46,15 @@ def test_correctly_refetches_rebels(): def test_correctly_fetches_id_name_empire(): initialize() - query = ''' + query = """ query EmpireQuery { empire { id name } } - ''' - expected = { - 'empire': { - 'id': 'RmFjdGlvbjoy', - 'name': 'Galactic Empire' - } - } + """ + expected = {"empire": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}} result = schema.execute(query) assert not result.errors assert result.data == expected @@ -73,7 +62,7 @@ def test_correctly_fetches_id_name_empire(): def test_correctly_refetches_empire(): initialize() - query = ''' + query = """ query EmpireRefetchQuery { node(id: "RmFjdGlvbjoy") { id @@ -82,13 +71,8 @@ def test_correctly_refetches_empire(): } } } - ''' - expected = { - 'node': { - 'id': 'RmFjdGlvbjoy', - 'name': 'Galactic Empire' - } - } + """ + expected = {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}} result = schema.execute(query) assert not result.errors assert result.data == expected @@ -96,7 +80,7 @@ def test_correctly_refetches_empire(): def test_correctly_refetches_xwing(): initialize() - query = ''' + query = """ query XWingRefetchQuery { node(id: "U2hpcDox") { id @@ -105,13 +89,8 @@ def test_correctly_refetches_xwing(): } } } - ''' - expected = { - 'node': { - 'id': 'U2hpcDox', - 'name': 'X-Wing' - } - } + """ + expected = {"node": {"id": "U2hpcDox", "name": "X-Wing"}} result = schema.execute(query) assert not result.errors assert result.data == expected diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 158355a..1bb16f4 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -177,7 +177,11 @@ def convert_field_to_list_or_connection(field, registry=None): if not _type: return - description = field.help_text if isinstance(field, models.ManyToManyField) else field.field.help_text + description = ( + field.help_text + if isinstance(field, models.ManyToManyField) + else field.field.help_text + ) # If there is a connection, we should transform the field # into a DjangoConnectionField diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index 7c85e9a..62f4b1a 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -41,10 +41,9 @@ class DjangoFilterConnectionField(DjangoConnectionField): meta.update(self._extra_filter_meta) filterset_class = self._provided_filterset_class or ( - self.node_type._meta.filterset_class) - self._filterset_class = get_filterset_class( - filterset_class, **meta + self.node_type._meta.filterset_class ) + self._filterset_class = get_filterset_class(filterset_class, **meta) return self._filterset_class diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index eb6581b..4d8d597 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -229,6 +229,7 @@ def test_filter_filterset_information_on_meta_related(): def test_filter_filterset_class_filter_fields_exception(): with pytest.raises(Exception): + class ReporterFilter(FilterSet): class Meta: model = Reporter diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index a0c861d..9621ee3 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -104,7 +104,9 @@ def test_write_only_field(): ) assert hasattr(result, "cool_name") - assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible" + assert not hasattr( + result, "password" + ), "'password' is write_only field and shouldn't be visible" @mark.django_db @@ -124,7 +126,9 @@ def test_write_only_field_using_extra_kwargs(): ) assert hasattr(result, "cool_name") - assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible" + assert not hasattr( + result, "password" + ), "'password' is write_only field and shouldn't be visible" def test_nested_model(): diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 9ef217e..484a225 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1015,13 +1015,13 @@ def test_proxy_model_support(): "edges": [ {"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}} ] - } + }, } result = schema.execute(query) assert not result.errors assert result.data == expected - + def test_should_resolve_get_queryset_connectionfields(): reporter_1 = Reporter.objects.create( diff --git a/graphene_django/types.py b/graphene_django/types.py index ded8a15..a1e17b3 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -82,10 +82,12 @@ class DjangoObjectType(ObjectType): raise Exception("Can't set both filter_fields and filterset_class") if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class): - raise Exception(( - "Can only set filter_fields or filterset_class if " - "Django-Filter is installed" - )) + raise Exception( + ( + "Can only set filter_fields or filterset_class if " + "Django-Filter is installed" + ) + ) django_fields = yank_fields_from_attrs( construct_fields(model, registry, only_fields, exclude_fields), _as=Field diff --git a/tox.ini b/tox.ini index 8e21c74..58f283a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,31 +1,39 @@ [tox] -envlist = py{2.7,3.4,3.5,3.6,3.7,pypy,pypy3}-django{1.10,1.11,2.0,2.1,2.2,master},lint +envlist = + py{27,35,36,37}-django{111,20,21,22,master}, + black,flake8 + +[travis:env] +DJANGO = + 1.11: django111 + 2.0: django20 + 2.1: django21 + 2.2: django22 + master: djangomaster [testenv] passenv = * usedevelop = True -setenv = +setenv = DJANGO_SETTINGS_MODULE=django_test_settings -basepython = - py2.7: python2.7 - py3.4: python3.4 - py3.5: python3.5 - py3.6: python3.6 - py3.7: python3.7 - pypypy: pypy - pypypy3: pypy3 deps = -e.[test] psycopg2 - django1.10: Django>=1.10,<1.11 - django1.11: Django>=1.11,<1.12 - django2.0: Django>=2.0 - django2.1: Django>=2.1 + django111: Django>=1.11,<2.0 + django20: Django>=2.0,<2.1 + django21: Django>=2.1,<2.2 + django22: Django>=2.2,<3.0 djangomaster: https://github.com/django/django/archive/master.zip commands = {posargs:py.test --cov=graphene_django graphene_django examples} -[testenv:lint] -basepython = python -deps = - prospector -commands = prospector graphene_django -0 +[testenv:black] +basepython = python3.7 +deps = black +commands = + black --exclude "/migrations/" graphene_django examples --check + +[testenv:flake8] +basepython = python3.7 +deps = flake8 +commands = + flake8 graphene_django examples From 6e8dce95ae184cc3f8c3c9202ba92f8fd92c09d1 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 14 Jun 2019 12:33:37 +0100 Subject: [PATCH 24/44] Update doc setup (#673) * Expose doc commands in root makefile and add autobuild * Fix some errors * Alias some commands and add PHONY --- Makefile | 18 ++++++++++++++++++ docs/Makefile | 8 ++++++++ docs/_static/.gitkeep | 0 docs/authorization.rst | 3 ++- docs/requirements.txt | 3 ++- docs/settings.rst | 10 +++++----- 6 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 docs/_static/.gitkeep diff --git a/Makefile b/Makefile index 70badcb..39a0f31 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,29 @@ +.PHONY: dev-setup ## Install development dependencies dev-setup: pip install -e ".[dev]" +.PHONY: install-dev +install-dev: dev-setup # Alias install-dev -> dev-setup + +.PHONY: tests tests: py.test graphene_django --cov=graphene_django -vv +.PHONY: test +test: tests # Alias test -> tests + +.PHONY: format format: black --exclude "/migrations/" graphene_django examples +.PHONY: lint lint: flake8 graphene_django examples + +.PHONY: docs ## Generate docs +docs: dev-setup + cd docs && make install && make html + +.PHONY: docs-live ## Generate docs with live reloading +docs-live: dev-setup + cd docs && make install && make livehtml diff --git a/docs/Makefile b/docs/Makefile index 7da67c3..4ae2962 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -48,12 +48,20 @@ help: clean: rm -rf $(BUILDDIR)/* +.PHONY: install ## to install all documentation related requirements +install: + pip install -r requirements.txt + .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." +.PHONY: livehtml ## to build and serve live-reloading documentation +livehtml: + sphinx-autobuild -b html --watch ../graphene_django $(ALLSPHINXOPTS) $(BUILDDIR)/html + .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/authorization.rst b/docs/authorization.rst index 3d0bb8a..2c38fa4 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -154,7 +154,8 @@ Adding Login Required To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.: .. code:: python - #views.py + + # views.py from django.contrib.auth.mixins import LoginRequiredMixin from graphene_django.views import GraphQLView diff --git a/docs/requirements.txt b/docs/requirements.txt index 220b7cf..7c89926 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ -sphinx +Sphinx==1.5.3 +sphinx-autobuild==0.7.1 # Docs template http://graphene-python.org/sphinx_graphene_theme.zip diff --git a/docs/settings.rst b/docs/settings.rst index 547e77f..4d37a99 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -30,7 +30,7 @@ Default: ``None`` ``SCHEMA_OUTPUT`` ----------- +----------------- The name of the file where the GraphQL schema output will go. @@ -44,7 +44,7 @@ Default: ``schema.json`` ``SCHEMA_INDENT`` ----------- +----------------- The indentation level of the schema output. @@ -58,7 +58,7 @@ Default: ``2`` ``MIDDLEWARE`` ----------- +-------------- A tuple of middleware that will be executed for each GraphQL query. @@ -76,7 +76,7 @@ Default: ``()`` ``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST`` ----------- +------------------------------------------ Enforces relay queries to have the ``first`` or ``last`` argument. @@ -90,7 +90,7 @@ Default: ``False`` ``RELAY_CONNECTION_MAX_LIMIT`` ----------- +------------------------------ The maximum size of objects that can be requested through a relay connection. From 6169346776854c055f6349509e4e02d64b00863e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 17 Jun 2019 17:08:51 +0100 Subject: [PATCH 25/44] Bump django from 1.11.20 to 1.11.21 in /examples/cookbook (#670) Bumps [django](https://github.com/django/django) from 1.11.20 to 1.11.21. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/1.11.20...1.11.21) Signed-off-by: dependabot[bot] --- examples/cookbook/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt index fe0527a..9d13a82 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -1,5 +1,5 @@ graphene graphene-django graphql-core>=2.1rc1 -django==1.11.20 +django==1.11.21 django-filter>=2 From 894b1053a2bb40e7f52601f70d65e3ebd7c51fe5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 17 Jun 2019 18:48:15 +0100 Subject: [PATCH 26/44] Bump django from 2.1.6 to 2.1.9 in /examples/cookbook-plain (#669) Bumps [django](https://github.com/django/django) from 2.1.6 to 2.1.9. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/2.1.6...2.1.9) Signed-off-by: dependabot[bot] --- examples/cookbook-plain/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt index 2154fd8..ea1f4ba 100644 --- a/examples/cookbook-plain/requirements.txt +++ b/examples/cookbook-plain/requirements.txt @@ -1,4 +1,4 @@ graphene graphene-django graphql-core>=2.1rc1 -django==2.1.6 +django==2.1.9 From 612ba5a4eaea0336a5dffcba3dbe7909b9d94646 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Mon, 17 Jun 2019 18:48:29 +0100 Subject: [PATCH 27/44] Add `convert_choices_to_enum` option on DjangoObjectType Meta class (#674) * Add convert_choices_to_enum meta option * Add tests * Run black * Update documentation * Add link to Django choices documentation * Add test and documentation note That setting to an empty list is the same as setting the value as False * Fix Django warning in tests * rst is not markdown --- docs/queries.rst | 65 ++++++++++++++ graphene_django/converter.py | 6 +- graphene_django/tests/test_converter.py | 17 ++++ graphene_django/tests/test_types.py | 115 +++++++++++++++++++++++- graphene_django/types.py | 23 ++++- 5 files changed, 220 insertions(+), 6 deletions(-) diff --git a/docs/queries.rst b/docs/queries.rst index 0edd1dd..7aff572 100644 --- a/docs/queries.rst +++ b/docs/queries.rst @@ -92,6 +92,71 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType return 'hello!' +Choices to Enum conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default Graphene-Django will convert any Django fields that have `choices`_ +defined into a GraphQL enum type. + +.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices + +For example the following ``Model`` and ``DjangoObjectType``: + +.. code:: python + + class PetModel(models.Model): + kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog'))) + + class Pet(DjangoObjectType): + class Meta: + model = PetModel + +Results in the following GraphQL schema definition: + +.. code:: + + type Pet { + id: ID! + kind: PetModelKind! + } + + enum PetModelKind { + CAT + DOG + } + +You can disable this automatic conversion by setting +``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType`` +``Meta`` class. + +.. code:: python + + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = False + +.. code:: + + type Pet { + id: ID! + kind: String! + } + +You can also set ``convert_choices_to_enum`` to a list of fields that should be +automatically converted into enums: + +.. code:: python + + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = ['kind'] + +**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to +``False``. + + Related models -------------- diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 1bb16f4..4d0b45f 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -52,13 +52,15 @@ def get_choices(choices): yield name, value, description -def convert_django_field_with_choices(field, registry=None): +def convert_django_field_with_choices( + field, registry=None, convert_choices_to_enum=True +): if registry is not None: converted = registry.get_converted_field(field) if converted: return converted choices = getattr(field, "choices", None) - if choices: + if choices and convert_choices_to_enum: meta = field.model._meta name = to_camel_case("{}_{}".format(meta.object_name, field.name)) choices = list(get_choices(choices)) diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index bb176b3..5542c90 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -196,6 +196,23 @@ def test_field_with_choices_collision(): convert_django_field_with_choices(field) +def test_field_with_choices_convert_enum_false(): + field = models.CharField( + help_text="Language", choices=(("es", "Spanish"), ("en", "English")) + ) + + class TranslatedModel(models.Model): + language = field + + class Meta: + app_label = "test" + + graphene_type = convert_django_field_with_choices( + field, convert_choices_to_enum=False + ) + assert isinstance(graphene_type, graphene.String) + + def test_should_float_convert_float(): assert_conversion(models.FloatField, graphene.Float) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 8a8643b..c1ac6c2 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -1,6 +1,11 @@ +from collections import OrderedDict, defaultdict +from textwrap import dedent + +import pytest +from django.db import models from mock import patch -from graphene import Interface, ObjectType, Schema, Connection, String +from graphene import Connection, Field, Interface, ObjectType, Schema, String from graphene.relay import Node from .. import registry @@ -224,3 +229,111 @@ def test_django_objecttype_exclude_fields(): fields = list(Reporter._meta.fields.keys()) assert "email" not in fields + + +class TestDjangoObjectType: + @pytest.fixture + def PetModel(self): + class PetModel(models.Model): + kind = models.CharField(choices=(("cat", "Cat"), ("dog", "Dog"))) + cuteness = models.IntegerField( + choices=((1, "Kind of cute"), (2, "Pretty cute"), (3, "OMG SO CUTE!!!")) + ) + + yield PetModel + + # Clear Django model cache so we don't get warnings when creating the + # model multiple times + PetModel._meta.apps.all_models = defaultdict(OrderedDict) + + def test_django_objecttype_convert_choices_enum_false(self, PetModel): + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = False + + class Query(ObjectType): + pet = Field(Pet) + + schema = Schema(query=Query) + + assert str(schema) == dedent( + """\ + schema { + query: Query + } + + type Pet { + id: ID! + kind: String! + cuteness: Int! + } + + type Query { + pet: Pet + } + """ + ) + + def test_django_objecttype_convert_choices_enum_list(self, PetModel): + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = ["kind"] + + class Query(ObjectType): + pet = Field(Pet) + + schema = Schema(query=Query) + + assert str(schema) == dedent( + """\ + schema { + query: Query + } + + type Pet { + id: ID! + kind: PetModelKind! + cuteness: Int! + } + + enum PetModelKind { + CAT + DOG + } + + type Query { + pet: Pet + } + """ + ) + + def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel): + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = [] + + class Query(ObjectType): + pet = Field(Pet) + + schema = Schema(query=Query) + + assert str(schema) == dedent( + """\ + schema { + query: Query + } + + type Pet { + id: ID! + kind: String! + cuteness: Int! + } + + type Query { + pet: Pet + } + """ + ) diff --git a/graphene_django/types.py b/graphene_django/types.py index a1e17b3..005300d 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -18,7 +18,9 @@ if six.PY3: from typing import Type -def construct_fields(model, registry, only_fields, exclude_fields): +def construct_fields( + model, registry, only_fields, exclude_fields, convert_choices_to_enum +): _model_fields = get_model_fields(model) fields = OrderedDict() @@ -33,7 +35,18 @@ def construct_fields(model, registry, only_fields, exclude_fields): # in there. Or when we exclude this field in exclude_fields. # Or when there is no back reference. continue - converted = convert_django_field_with_choices(field, registry) + + _convert_choices_to_enum = convert_choices_to_enum + if not isinstance(_convert_choices_to_enum, bool): + # then `convert_choices_to_enum` is a list of field names to convert + if name in _convert_choices_to_enum: + _convert_choices_to_enum = True + else: + _convert_choices_to_enum = False + + converted = convert_django_field_with_choices( + field, registry, convert_choices_to_enum=_convert_choices_to_enum + ) fields[name] = converted return fields @@ -63,6 +76,7 @@ class DjangoObjectType(ObjectType): connection_class=None, use_connection=None, interfaces=(), + convert_choices_to_enum=True, _meta=None, **options ): @@ -90,7 +104,10 @@ class DjangoObjectType(ObjectType): ) django_fields = yank_fields_from_attrs( - construct_fields(model, registry, only_fields, exclude_fields), _as=Field + construct_fields( + model, registry, only_fields, exclude_fields, convert_choices_to_enum + ), + _as=Field, ) if use_connection is None and interfaces: From 91c1278d1a25e35c08c47d24e6ac39ecc0ab78e2 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Wed, 19 Jun 2019 15:59:19 +0500 Subject: [PATCH 28/44] Make cookbook example working on django 2 (#680) --- examples/cookbook/cookbook/ingredients/models.py | 4 +++- examples/cookbook/cookbook/recipes/models.py | 6 ++++-- examples/cookbook/cookbook/settings.py | 3 +-- examples/cookbook/requirements.txt | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/cookbook/cookbook/ingredients/models.py b/examples/cookbook/cookbook/ingredients/models.py index 6426dab..1e97226 100644 --- a/examples/cookbook/cookbook/ingredients/models.py +++ b/examples/cookbook/cookbook/ingredients/models.py @@ -11,7 +11,9 @@ class Category(models.Model): class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField(null=True, blank=True) - category = models.ForeignKey(Category, related_name="ingredients") + category = models.ForeignKey( + Category, related_name="ingredients", on_delete=models.CASCADE + ) def __str__(self): return self.name diff --git a/examples/cookbook/cookbook/recipes/models.py b/examples/cookbook/cookbook/recipes/models.py index b98664c..0bfb434 100644 --- a/examples/cookbook/cookbook/recipes/models.py +++ b/examples/cookbook/cookbook/recipes/models.py @@ -10,8 +10,10 @@ class Recipe(models.Model): class RecipeIngredient(models.Model): - recipe = models.ForeignKey(Recipe, related_name="amounts") - ingredient = models.ForeignKey(Ingredient, related_name="used_by") + recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE) + ingredient = models.ForeignKey( + Ingredient, related_name="used_by", on_delete=models.CASCADE + ) amount = models.FloatField() unit = models.CharField( max_length=20, diff --git a/examples/cookbook/cookbook/settings.py b/examples/cookbook/cookbook/settings.py index ed41a65..7eb9d56 100644 --- a/examples/cookbook/cookbook/settings.py +++ b/examples/cookbook/cookbook/settings.py @@ -43,13 +43,12 @@ INSTALLED_APPS = [ "cookbook.recipes.apps.RecipesConfig", ] -MIDDLEWARE_CLASSES = [ +MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.auth.middleware.SessionAuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt index 9d13a82..ccece5c 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -1,5 +1,5 @@ graphene graphene-django graphql-core>=2.1rc1 -django==1.11.21 +django==2.2.2 django-filter>=2 From 692540cc782e52364f01c14523bcd551dff6cd3e Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Mon, 24 Jun 2019 18:55:44 +0100 Subject: [PATCH 29/44] Update flake8 (#688) * Include setup.py in black formatting * Add new flake8 plugins and update errors to look for * Fix duplicate test name * Don't use mutable data structure * Install all dev dependencies for flake8 and black tox envs --- Makefile | 2 +- graphene_django/filter/tests/test_fields.py | 4 ++- graphene_django/rest_framework/mutation.py | 2 +- setup.cfg | 27 ++++++++++++++++++++- setup.py | 8 +++++- tox.ini | 6 ++--- 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 39a0f31..b850ae8 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ test: tests # Alias test -> tests .PHONY: format format: - black --exclude "/migrations/" graphene_django examples + black --exclude "/migrations/" graphene_django examples setup.py .PHONY: lint lint: diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 4d8d597..b9bc599 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -321,12 +321,14 @@ def test_filter_filterset_related_results(): pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r1, + editor=r1, ) Article.objects.create( headline="a2", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r2, + editor=r2, ) query = """ @@ -450,7 +452,7 @@ def test_global_id_multiple_field_explicit_reverse(): assert multiple_filter.field_class == GlobalIDMultipleChoiceField -def test_filter_filterset_related_results(): +def test_filter_filterset_related_results_with_filter(): class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 0fe9a02..b5e7160 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -52,7 +52,7 @@ class SerializerMutation(ClientIDMutation): lookup_field=None, serializer_class=None, model_class=None, - model_operations=["create", "update"], + model_operations=("create", "update"), only_fields=(), exclude_fields=(), **options diff --git a/setup.cfg b/setup.cfg index 546ad67..7d93d3e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,8 +5,33 @@ test=pytest universal=1 [flake8] -exclude = setup.py,docs/*,examples/*,tests,graphene_django/debug/sql/* +exclude = docs,graphene_django/debug/sql/*,migrations max-line-length = 120 +select = + # Dictionary key repeated + F601, + # Ensure use of ==/!= to compare with str, bytes and int literals + F632, + # Redefinition of unused name + F811, + # Using an undefined variable + F821, + # Defining an undefined variable in __all__ + F822, + # Using a variable before it is assigned + F823, + # Duplicate argument in function declaration + F831, + # Black would format this line + BLK, + # Do not use bare except + B001, + # Don't allow ++n. You probably meant n += 1 + B002, + # Do not use mutable structures for argument defaults + B006, + # Do not perform calls in argument defaults + B008 [coverage:run] omit = */tests/* diff --git a/setup.py b/setup.py index e622a71..bc7dcd3 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,8 @@ tests_require = [ dev_requires = [ "black==19.3b0", "flake8==3.7.7", + "flake8-black==0.1.0", + "flake8-bugbear==19.3.0", ] + tests_require setup( @@ -64,7 +66,11 @@ setup( setup_requires=["pytest-runner"], tests_require=tests_require, rest_framework_require=rest_framework_require, - extras_require={"test": tests_require, "rest_framework": rest_framework_require, "dev": dev_requires}, + extras_require={ + "test": tests_require, + "rest_framework": rest_framework_require, + "dev": dev_requires, + }, include_package_data=True, zip_safe=False, platforms="any", diff --git a/tox.ini b/tox.ini index 58f283a..a1b599a 100644 --- a/tox.ini +++ b/tox.ini @@ -28,12 +28,12 @@ commands = {posargs:py.test --cov=graphene_django graphene_django examples} [testenv:black] basepython = python3.7 -deps = black +deps = -e.[dev] commands = - black --exclude "/migrations/" graphene_django examples --check + black --exclude "/migrations/" graphene_django examples setup.py --check [testenv:flake8] basepython = python3.7 -deps = flake8 +deps = -e.[dev] commands = flake8 graphene_django examples From e2e496f505bad4d45a1616baa176a53732766bd1 Mon Sep 17 00:00:00 2001 From: Konstantin Alekseev Date: Tue, 25 Jun 2019 11:40:29 +0300 Subject: [PATCH 30/44] Apply camel case converter to field names in DRF errors (#514) * Apply camel case converter to field names in DRF errors * Implement recursive error camelize, add setting. --- graphene_django/forms/mutation.py | 7 ++--- graphene_django/forms/tests/test_mutation.py | 20 +++++++++++++- graphene_django/rest_framework/mutation.py | 9 +++---- .../rest_framework/tests/test_mutation.py | 14 +++++++--- graphene_django/settings.py | 1 + graphene_django/tests/test_utils.py | 22 +++++++++++++++- graphene_django/types.py | 21 ++++++++++++--- graphene_django/utils/__init__.py | 10 ++++--- graphene_django/utils/utils.py | 26 +++++++++++++++++++ 9 files changed, 107 insertions(+), 23 deletions(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 0851a75..f5921e8 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -13,8 +13,8 @@ from graphene.types.mutation import MutationOptions from graphene.types.utils import yank_fields_from_attrs from graphene_django.registry import get_global_registry -from .converter import convert_form_field from ..types import ErrorType +from .converter import convert_form_field def fields_for_form(form, only_fields, exclude_fields): @@ -45,10 +45,7 @@ class BaseDjangoFormMutation(ClientIDMutation): if form.is_valid(): return cls.perform_mutate(form, info) else: - errors = [ - ErrorType(field=key, messages=value) - for key, value in form.errors.items() - ] + errors = ErrorType.from_errors(form.errors) return cls(errors=errors) diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 543e89e..4c46702 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -2,7 +2,9 @@ from django import forms from django.test import TestCase from py.test import raises -from graphene_django.tests.models import Pet, Film, FilmDetails +from graphene_django.tests.models import Film, FilmDetails, Pet + +from ...settings import graphene_settings from ..mutation import DjangoFormMutation, DjangoModelFormMutation @@ -41,6 +43,22 @@ def test_has_input_fields(): assert "text" in MyMutation.Input._meta.fields +def test_mutation_error_camelcased(): + class ExtraPetForm(PetForm): + test_field = forms.CharField(required=True) + + class PetMutation(DjangoModelFormMutation): + class Meta: + form_class = ExtraPetForm + + result = PetMutation.mutate_and_get_payload(None, None) + assert {f.field for f in result.errors} == {"name", "age", "test_field"} + graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = True + result = PetMutation.mutate_and_get_payload(None, None) + assert {f.field for f in result.errors} == {"name", "age", "testField"} + graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = False + + class ModelFormMutationTests(TestCase): def test_default_meta_fields(self): class PetMutation(DjangoModelFormMutation): diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index b5e7160..d9c695e 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -3,13 +3,13 @@ from collections import OrderedDict from django.shortcuts import get_object_or_404 import graphene +from graphene.relay.mutation import ClientIDMutation from graphene.types import Field, InputField from graphene.types.mutation import MutationOptions -from graphene.relay.mutation import ClientIDMutation from graphene.types.objecttype import yank_fields_from_attrs -from .serializer_converter import convert_serializer_field from ..types import ErrorType +from .serializer_converter import convert_serializer_field class SerializerMutationOptions(MutationOptions): @@ -127,10 +127,7 @@ class SerializerMutation(ClientIDMutation): if serializer.is_valid(): return cls.perform_mutate(serializer, info) else: - errors = [ - ErrorType(field=key, messages=value) - for key, value in serializer.errors.items() - ] + errors = ErrorType.from_errors(serializer.errors) return cls(errors=errors) diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index 9621ee3..0dd5ad3 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -1,11 +1,12 @@ import datetime +from py.test import mark, raises +from rest_framework import serializers + from graphene import Field, ResolveInfo from graphene.types.inputobjecttype import InputObjectType -from py.test import raises -from py.test import mark -from rest_framework import serializers +from ...settings import graphene_settings from ...types import DjangoObjectType from ..models import MyFakeModel, MyFakeModelWithPassword from ..mutation import SerializerMutation @@ -213,6 +214,13 @@ def test_model_mutate_and_get_payload_error(): assert len(result.errors) > 0 +def test_mutation_error_camelcased(): + graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = True + result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{}) + assert result.errors[0].field == "coolName" + graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = False + + def test_invalid_serializer_operations(): with raises(Exception) as exc: diff --git a/graphene_django/settings.py b/graphene_django/settings.py index e5fad78..1b49dfb 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -35,6 +35,7 @@ DEFAULTS = { "RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False, # Max items returned in ConnectionFields / FilterConnectionFields "RELAY_CONNECTION_MAX_LIMIT": 100, + "DJANGO_GRAPHENE_CAMELCASE_ERRORS": False, } if settings.DEBUG: diff --git a/graphene_django/tests/test_utils.py b/graphene_django/tests/test_utils.py index becd031..55cfd4f 100644 --- a/graphene_django/tests/test_utils.py +++ b/graphene_django/tests/test_utils.py @@ -1,4 +1,6 @@ -from ..utils import get_model_fields +from django.utils.translation import gettext_lazy + +from ..utils import camelize, get_model_fields from .models import Film, Reporter @@ -10,3 +12,21 @@ def test_get_model_fields_no_duplication(): film_fields = get_model_fields(Film) film_name_set = set([field[0] for field in film_fields]) assert len(film_fields) == len(film_name_set) + + +def test_camelize(): + assert camelize({}) == {} + assert camelize("value_a") == "value_a" + assert camelize({"value_a": "value_b"}) == {"valueA": "value_b"} + assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]} + assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]} + assert camelize({"nested_field": {"value_a": ["error"], "value_b": ["error"]}}) == { + "nestedField": {"valueA": ["error"], "valueB": ["error"]} + } + assert camelize({"value_a": gettext_lazy("value_b")}) == {"valueA": "value_b"} + assert camelize({"value_a": [gettext_lazy("value_b")]}) == {"valueA": ["value_b"]} + assert camelize(gettext_lazy("value_a")) == "value_a" + assert camelize({gettext_lazy("value_a"): gettext_lazy("value_b")}) == { + "valueA": "value_b" + } + assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}} diff --git a/graphene_django/types.py b/graphene_django/types.py index 005300d..c296707 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -1,8 +1,9 @@ -import six from collections import OrderedDict +import six from django.db.models import Model from django.utils.functional import SimpleLazyObject + import graphene from graphene import Field from graphene.relay import Connection, Node @@ -11,8 +12,13 @@ from graphene.types.utils import yank_fields_from_attrs from .converter import convert_django_field_with_choices from .registry import Registry, get_global_registry -from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model - +from .settings import graphene_settings +from .utils import ( + DJANGO_FILTER_INSTALLED, + camelize, + get_model_fields, + is_valid_django_model, +) if six.PY3: from typing import Type @@ -182,3 +188,12 @@ class DjangoObjectType(ObjectType): class ErrorType(ObjectType): field = graphene.String(required=True) messages = graphene.List(graphene.NonNull(graphene.String), required=True) + + @classmethod + def from_errors(cls, errors): + data = ( + camelize(errors) + if graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS + else errors + ) + return [ErrorType(field=key, messages=value) for key, value in data.items()] diff --git a/graphene_django/utils/__init__.py b/graphene_django/utils/__init__.py index f9c388d..9d8658b 100644 --- a/graphene_django/utils/__init__.py +++ b/graphene_django/utils/__init__.py @@ -1,18 +1,20 @@ +from .testing import GraphQLTestCase from .utils import ( DJANGO_FILTER_INSTALLED, - get_reverse_fields, - maybe_queryset, + camelize, get_model_fields, - is_valid_django_model, + get_reverse_fields, import_single_dispatch, + is_valid_django_model, + maybe_queryset, ) -from .testing import GraphQLTestCase __all__ = [ "DJANGO_FILTER_INSTALLED", "get_reverse_fields", "maybe_queryset", "get_model_fields", + "camelize", "is_valid_django_model", "import_single_dispatch", "GraphQLTestCase", diff --git a/graphene_django/utils/utils.py b/graphene_django/utils/utils.py index b8aaba0..47c0c37 100644 --- a/graphene_django/utils/utils.py +++ b/graphene_django/utils/utils.py @@ -2,7 +2,11 @@ import inspect from django.db import models from django.db.models.manager import Manager +from django.utils import six +from django.utils.encoding import force_text +from django.utils.functional import Promise +from graphene.utils.str_converters import to_camel_case try: import django_filters # noqa @@ -12,6 +16,28 @@ except ImportError: DJANGO_FILTER_INSTALLED = False +def isiterable(value): + try: + iter(value) + except TypeError: + return False + return True + + +def _camelize_django_str(s): + if isinstance(s, Promise): + s = force_text(s) + return to_camel_case(s) if isinstance(s, six.string_types) else s + + +def camelize(data): + if isinstance(data, dict): + return {_camelize_django_str(k): camelize(v) for k, v in data.items()} + if isiterable(data) and not isinstance(data, (six.string_types, Promise)): + return [camelize(d) for d in data] + return data + + def get_reverse_fields(model, local_field_names): for name, attr in model.__dict__.items(): # Don't duplicate any local fields From 54cc6a4b13c18b8efebccaacd8ac8df93bf56949 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Tue, 25 Jun 2019 16:30:30 +0100 Subject: [PATCH 31/44] Enforce NonNull for returned related Sets and their content (#690) * Enforce NonNull for returned related Sets and their content. https://github.com/graphql-python/graphene-django/issues/448 * Run format. * Remove duplicate assertion --- graphene_django/converter.py | 6 +++++- graphene_django/fields.py | 3 ++- graphene_django/tests/test_converter.py | 16 ++++++++++++---- graphene_django/tests/test_types.py | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 4d0b45f..64bf341 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -198,7 +198,11 @@ def convert_field_to_list_or_connection(field, registry=None): return DjangoConnectionField(_type, description=description) - return DjangoListField(_type, description=description) + return DjangoListField( + _type, + required=True, # A Set is always returned, never None. + description=description, + ) return Dynamic(dynamic_type) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 791e785..8c8fa2b 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -15,7 +15,8 @@ from .utils import maybe_queryset class DjangoListField(Field): def __init__(self, _type, *args, **kwargs): - super(DjangoListField, self).__init__(List(_type), *args, **kwargs) + # Django would never return a Set of None vvvvvvv + super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs) @property def model(self): diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 5542c90..00467b4 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -1,6 +1,7 @@ import pytest from django.db import models from django.utils.translation import ugettext_lazy as _ +from graphene import NonNull from py.test import raises import graphene @@ -234,8 +235,12 @@ def test_should_manytomany_convert_connectionorlist_list(): assert isinstance(graphene_field, graphene.Dynamic) dynamic_field = graphene_field.get_type() assert isinstance(dynamic_field, graphene.Field) - assert isinstance(dynamic_field.type, graphene.List) - assert dynamic_field.type.of_type == A + # A NonNull List of NonNull A ([A!]!) + # https://github.com/graphql-python/graphene-django/issues/448 + assert isinstance(dynamic_field.type, NonNull) + assert isinstance(dynamic_field.type.of_type, graphene.List) + assert isinstance(dynamic_field.type.of_type.of_type, NonNull) + assert dynamic_field.type.of_type.of_type.of_type == A def test_should_manytomany_convert_connectionorlist_connection(): @@ -262,8 +267,11 @@ def test_should_manytoone_convert_connectionorlist(): assert isinstance(graphene_field, graphene.Dynamic) dynamic_field = graphene_field.get_type() assert isinstance(dynamic_field, graphene.Field) - assert isinstance(dynamic_field.type, graphene.List) - assert dynamic_field.type.of_type == A + # a NonNull List of NonNull A ([A!]!) + assert isinstance(dynamic_field.type, NonNull) + assert isinstance(dynamic_field.type.of_type, graphene.List) + assert isinstance(dynamic_field.type.of_type.of_type, NonNull) + assert dynamic_field.type.of_type.of_type.of_type == A def test_should_onetoone_reverse_convert_model(): diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index c1ac6c2..6f5ab7e 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -170,7 +170,7 @@ type Reporter { firstName: String! lastName: String! email: String! - pets: [Reporter] + pets: [Reporter!]! aChoice: ReporterAChoice! reporterType: ReporterReporterType articles(before: String, after: String, first: Int, last: Int): ArticleConnection From 40ae7e53ec4d8be5e540ab26e110506733ea2b9b Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Tue, 2 Jul 2019 19:37:50 +0100 Subject: [PATCH 32/44] Fix manager check in DjangoConnectionField (#693) * Fix default manager check * Add test --- graphene_django/fields.py | 2 +- graphene_django/tests/test_query.py | 51 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 8c8fa2b..eb1215e 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -101,7 +101,7 @@ class DjangoConnectionField(ConnectionField): iterable = default_manager iterable = maybe_queryset(iterable) if isinstance(iterable, QuerySet): - if iterable is not default_manager: + if iterable.model.objects is not default_manager: default_queryset = maybe_queryset(default_manager) iterable = cls.merge_querysets(default_queryset, iterable) _len = iterable.count() diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 484a225..f466122 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1065,3 +1065,54 @@ def test_should_resolve_get_queryset_connectionfields(): result = schema.execute(query) assert not result.errors assert result.data == expected + + +def test_should_preserve_prefetch_related(django_assert_num_queries): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (graphene.relay.Node,) + + class FilmType(DjangoObjectType): + reporters = DjangoConnectionField(ReporterType) + + class Meta: + model = Film + interfaces = (graphene.relay.Node,) + + class Query(graphene.ObjectType): + films = DjangoConnectionField(FilmType) + + def resolve_films(root, info): + qs = Film.objects.prefetch_related("reporters") + return qs + + r1 = Reporter.objects.create(first_name="Dave", last_name="Smith") + r2 = Reporter.objects.create(first_name="Jane", last_name="Doe") + + f1 = Film.objects.create() + f1.reporters.set([r1, r2]) + f2 = Film.objects.create() + f2.reporters.set([r2]) + + query = """ + query { + films { + edges { + node { + reporters { + edges { + node { + firstName + } + } + } + } + } + } + } + """ + schema = graphene.Schema(query=Query) + with django_assert_num_queries(3) as captured: + result = schema.execute(query) + assert not result.errors From 470fb60dc5341b26a6069c29c6c3c12b4146ccdb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2019 10:26:27 +0100 Subject: [PATCH 33/44] Bump django from 2.1.9 to 2.1.10 in /examples/cookbook-plain (#695) Bumps [django](https://github.com/django/django) from 2.1.9 to 2.1.10. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/2.1.9...2.1.10) Signed-off-by: dependabot[bot] --- examples/cookbook-plain/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt index ea1f4ba..1dc8fcd 100644 --- a/examples/cookbook-plain/requirements.txt +++ b/examples/cookbook-plain/requirements.txt @@ -1,4 +1,4 @@ graphene graphene-django graphql-core>=2.1rc1 -django==2.1.9 +django==2.1.10 From 3b541e3d05d0ca8f15a138d9daa4d347019c02b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2019 10:26:54 +0100 Subject: [PATCH 34/44] Bump django from 2.2.2 to 2.2.3 in /examples/cookbook (#694) Bumps [django](https://github.com/django/django) from 2.2.2 to 2.2.3. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/2.2.2...2.2.3) Signed-off-by: dependabot[bot] --- examples/cookbook/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt index ccece5c..49470ed 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -1,5 +1,5 @@ graphene graphene-django graphql-core>=2.1rc1 -django==2.2.2 +django==2.2.3 django-filter>=2 From 9aabe2cbe62f412ee70ad9b0b47a15d28021b80e Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 7 Jul 2019 20:06:01 +0100 Subject: [PATCH 35/44] Remove duplicate ErrorType (#701) --- graphene_django/forms/types.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/graphene_django/forms/types.py b/graphene_django/forms/types.py index 1fe33f3..5005040 100644 --- a/graphene_django/forms/types.py +++ b/graphene_django/forms/types.py @@ -1,6 +1,3 @@ import graphene - -class ErrorType(graphene.ObjectType): - field = graphene.String() - messages = graphene.List(graphene.String) +from ..types import ErrorType # noqa Import ErrorType for backwards compatability From aa30750d395dc1cc5f550d933506d978c20d285e Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 7 Jul 2019 20:11:27 +0100 Subject: [PATCH 36/44] Bugfix: Correct filter types for DjangoFilterConnectionFields (#682) * Get form field from Django model before defaulting to django-filter * Add test * Cleanup some flake8 warnings and pytest warnings * Run isort and add black compatible config --- graphene_django/filter/tests/test_fields.py | 73 +++++++++++++++++---- graphene_django/filter/utils.py | 19 +++++- setup.cfg | 5 ++ 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index b9bc599..d163ff3 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -1,18 +1,17 @@ from datetime import datetime +from textwrap import dedent import pytest +from django.db.models import TextField, Value +from django.db.models.functions import Concat -from graphene import Field, ObjectType, Schema, Argument, Float, Boolean, String +from graphene import Argument, Boolean, Field, Float, ObjectType, Schema, String from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField from graphene_django.tests.models import Article, Pet, Reporter from graphene_django.utils import DJANGO_FILTER_INSTALLED -# for annotation test -from django.db.models import TextField, Value -from django.db.models.functions import Concat - pytestmark = [] if DJANGO_FILTER_INSTALLED: @@ -183,7 +182,7 @@ def test_filter_shortcut_filterset_context(): } """ schema = Schema(query=Query) - result = schema.execute(query, context_value=context()) + result = schema.execute(query, context=context()) assert not result.errors assert len(result.data["contextArticles"]["edges"]) == 1 @@ -462,15 +461,15 @@ def test_filter_filterset_related_results_with_filter(): class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) - r1 = Reporter.objects.create( + Reporter.objects.create( first_name="A test user", last_name="Last Name", email="test1@test.com" ) - r2 = Reporter.objects.create( + Reporter.objects.create( first_name="Other test user", last_name="Other Last Name", email="test2@test.com", ) - r3 = Reporter.objects.create( + Reporter.objects.create( first_name="Random", last_name="RandomLast", email="random@test.com" ) @@ -638,7 +637,7 @@ def test_should_query_filter_node_double_limit_raises(): Reporter.objects.create( first_name="Bob", last_name="Doe", email="bobdoe@example.com", a_choice=2 ) - r = Reporter.objects.create( + Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) @@ -684,7 +683,7 @@ def test_order_by_is_perserved(): return reporters Reporter.objects.create(first_name="b") - r = Reporter.objects.create(first_name="a") + Reporter.objects.create(first_name="a") schema = Schema(query=Query) query = """ @@ -767,3 +766,55 @@ def test_annotation_is_perserved(): assert not result.errors assert result.data == expected + + +def test_integer_field_filter_type(): + class PetType(DjangoObjectType): + class Meta: + model = Pet + interfaces = (Node,) + filter_fields = {"age": ["exact"]} + only_fields = ["age"] + + class Query(ObjectType): + pets = DjangoFilterConnectionField(PetType) + + schema = Schema(query=Query) + + assert str(schema) == dedent( + """\ + schema { + query: Query + } + + interface Node { + id: ID! + } + + type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String + } + + type PetType implements Node { + age: Int! + id: ID! + } + + type PetTypeConnection { + pageInfo: PageInfo! + edges: [PetTypeEdge]! + } + + type PetTypeEdge { + node: PetType + cursor: String! + } + + type Query { + pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection + } + """ + ) diff --git a/graphene_django/filter/utils.py b/graphene_django/filter/utils.py index cfa5621..00030a0 100644 --- a/graphene_django/filter/utils.py +++ b/graphene_django/filter/utils.py @@ -11,8 +11,25 @@ def get_filtering_args_from_filterset(filterset_class, type): from ..forms.converter import convert_form_field args = {} + model = filterset_class._meta.model for name, filter_field in six.iteritems(filterset_class.base_filters): - field_type = convert_form_field(filter_field.field).Argument() + if name in filterset_class.declared_filters: + form_field = filter_field.field + else: + field_name = name.split("__", 1)[0] + model_field = model._meta.get_field(field_name) + + if hasattr(model_field, "formfield"): + form_field = model_field.formfield( + required=filter_field.extra.get("required", False) + ) + + # Fallback to field defined on filter if we can't get it from the + # model field + if not form_field: + form_field = filter_field.field + + field_type = convert_form_field(form_field).Argument() field_type.description = filter_field.label args[name] = field_type diff --git a/setup.cfg b/setup.cfg index 7d93d3e..def0b67 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,3 +38,8 @@ omit = */tests/* [isort] known_first_party=graphene,graphene_django +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=88 From 0988e0798ac72a8ebca1b9c133bb31648b3b582b Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Mon, 8 Jul 2019 22:22:08 +0100 Subject: [PATCH 37/44] Adds documentation to `CAMELCASE_ERRORS` setting (#689) * Rename setting and add documentation * Add examples * Use `cls` --- docs/settings.rst | 39 +++++++++++++++++++ graphene_django/forms/tests/test_mutation.py | 4 +- .../rest_framework/tests/test_mutation.py | 4 +- graphene_django/settings.py | 2 +- graphene_django/types.py | 8 +--- 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 4d37a99..4776ce0 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -101,3 +101,42 @@ Default: ``100`` GRAPHENE = { 'RELAY_CONNECTION_MAX_LIMIT': 100, } + + +``CAMELCASE_ERRORS`` +------------------------------------ + +When set to ``True`` field names in the ``errors`` object will be camel case. +By default they will be snake case. + +Default: ``False`` + +.. code:: python + + GRAPHENE = { + 'CAMELCASE_ERRORS': False, + } + + # result = schema.execute(...) + print(result.errors) + # [ + # { + # 'field': 'test_field', + # 'messages': ['This field is required.'], + # } + # ] + +.. code:: python + + GRAPHENE = { + 'CAMELCASE_ERRORS': True, + } + + # result = schema.execute(...) + print(result.errors) + # [ + # { + # 'field': 'testField', + # 'messages': ['This field is required.'], + # } + # ] diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 4c46702..2de5113 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -53,10 +53,10 @@ def test_mutation_error_camelcased(): result = PetMutation.mutate_and_get_payload(None, None) assert {f.field for f in result.errors} == {"name", "age", "test_field"} - graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = True + graphene_settings.CAMELCASE_ERRORS = True result = PetMutation.mutate_and_get_payload(None, None) assert {f.field for f in result.errors} == {"name", "age", "testField"} - graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = False + graphene_settings.CAMELCASE_ERRORS = False class ModelFormMutationTests(TestCase): diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index 0dd5ad3..9d8b950 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -215,10 +215,10 @@ def test_model_mutate_and_get_payload_error(): def test_mutation_error_camelcased(): - graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = True + graphene_settings.CAMELCASE_ERRORS = True result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{}) assert result.errors[0].field == "coolName" - graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = False + graphene_settings.CAMELCASE_ERRORS = False def test_invalid_serializer_operations(): diff --git a/graphene_django/settings.py b/graphene_django/settings.py index 1b49dfb..af63890 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -35,7 +35,7 @@ DEFAULTS = { "RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False, # Max items returned in ConnectionFields / FilterConnectionFields "RELAY_CONNECTION_MAX_LIMIT": 100, - "DJANGO_GRAPHENE_CAMELCASE_ERRORS": False, + "CAMELCASE_ERRORS": False, } if settings.DEBUG: diff --git a/graphene_django/types.py b/graphene_django/types.py index c296707..6c100ef 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -191,9 +191,5 @@ class ErrorType(ObjectType): @classmethod def from_errors(cls, errors): - data = ( - camelize(errors) - if graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS - else errors - ) - return [ErrorType(field=key, messages=value) for key, value in data.items()] + data = camelize(errors) if graphene_settings.CAMELCASE_ERRORS else errors + return [cls(field=key, messages=value) for key, value in data.items()] From a2103c19f427888d749be90e525aaec79527300e Mon Sep 17 00:00:00 2001 From: Pablo Burgos Date: Tue, 9 Jul 2019 10:14:04 +0200 Subject: [PATCH 38/44] Fix error of multiple inputs with the same type. When using same serializer. (#530) --- .../rest_framework/serializer_converter.py | 11 +++- .../tests/test_multiple_model_serializers.py | 63 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 graphene_django/rest_framework/tests/test_multiple_model_serializers.py diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 9f8e516..35c8dc8 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -57,18 +57,25 @@ def convert_serializer_field(field, is_input=True): def convert_serializer_to_input_type(serializer_class): + cached_type = convert_serializer_to_input_type.cache.get(serializer_class.__name__, None) + if cached_type: + return cached_type serializer = serializer_class() items = { name: convert_serializer_field(field) for name, field in serializer.fields.items() } - - return type( + ret_type = type( "{}Input".format(serializer.__class__.__name__), (graphene.InputObjectType,), items, ) + convert_serializer_to_input_type.cache[serializer_class.__name__] = ret_type + return ret_type + + +convert_serializer_to_input_type.cache = {} @get_graphene_type_from_serializer_field.register(serializers.Field) diff --git a/graphene_django/rest_framework/tests/test_multiple_model_serializers.py b/graphene_django/rest_framework/tests/test_multiple_model_serializers.py new file mode 100644 index 0000000..4504610 --- /dev/null +++ b/graphene_django/rest_framework/tests/test_multiple_model_serializers.py @@ -0,0 +1,63 @@ +import graphene +import pytest +from django.db import models +from graphene import Schema +from rest_framework import serializers + +from graphene_django import DjangoObjectType +from graphene_django.rest_framework.mutation import SerializerMutation + +pytestmark = pytest.mark.django_db + + +class MyFakeChildModel(models.Model): + name = models.CharField(max_length=50) + created = models.DateTimeField(auto_now_add=True) + + +class MyFakeParentModel(models.Model): + name = models.CharField(max_length=50) + created = models.DateTimeField(auto_now_add=True) + child1 = models.OneToOneField(MyFakeChildModel, related_name='parent1', on_delete=models.CASCADE) + child2 = models.OneToOneField(MyFakeChildModel, related_name='parent2', on_delete=models.CASCADE) + + +class ParentType(DjangoObjectType): + class Meta: + model = MyFakeParentModel + interfaces = (graphene.relay.Node,) + + +class ChildType(DjangoObjectType): + class Meta: + model = MyFakeChildModel + interfaces = (graphene.relay.Node,) + + +class MyModelChildSerializer(serializers.ModelSerializer): + class Meta: + model = MyFakeChildModel + fields = "__all__" + + +class MyModelParentSerializer(serializers.ModelSerializer): + child1 = MyModelChildSerializer() + child2 = MyModelChildSerializer() + + class Meta: + model = MyFakeParentModel + fields = "__all__" + + +class MyParentModelMutation(SerializerMutation): + class Meta: + serializer_class = MyModelParentSerializer + + +class Mutation(graphene.ObjectType): + createParentWithChild = MyParentModelMutation.Field() + + +def test_create_schema(): + schema = Schema(mutation=Mutation, types=[ParentType, ChildType]) + assert schema From b7e4937775a951c6d3990db58689bd9acee8a222 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Tue, 9 Jul 2019 14:03:11 +0100 Subject: [PATCH 39/44] Alias `only_fields` as `fields` and `exclude_fields` as `exclude` (#691) * Create new fields and exclude options that are aliased to exclude_fields and only_fields * Update docs * Add some checking around fields and exclude definitions * Add all fields option * Update docs to include `__all__` option * Actual order of fields is not stable * Update docs/queries.rst Co-Authored-By: Semyon Pupkov * Fix example code * Format code * Start raising PendingDeprecationWarnings for using only_fields and exclude_fields * Update tests --- docs/queries.rst | 45 ++++++--- graphene_django/filter/tests/test_fields.py | 2 +- .../rest_framework/serializer_converter.py | 4 +- .../tests/test_multiple_model_serializers.py | 8 +- graphene_django/tests/test_query.py | 8 +- graphene_django/tests/test_schema.py | 2 +- graphene_django/tests/test_types.py | 99 +++++++++++++++++-- graphene_django/types.py | 54 +++++++++- 8 files changed, 187 insertions(+), 35 deletions(-) diff --git a/docs/queries.rst b/docs/queries.rst index 7aff572..67ebb06 100644 --- a/docs/queries.rst +++ b/docs/queries.rst @@ -41,14 +41,18 @@ Full example return Question.objects.get(pk=question_id) -Fields ------- +Specifying which fields to include +---------------------------------- By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL. -If you don't want to do this you can change this by setting either ``only_fields`` and ``exclude_fields``. +If you only want a subset of fields to be present, you can do so using +``fields`` or ``exclude``. It is strongly recommended that you explicitly set +all fields that should be exposed using the fields attribute. +This will make it less likely to result in unintentionally exposing data when +your models change. -only_fields -~~~~~~~~~~~ +``fields`` +~~~~~~~~~~ Show **only** these fields on the model: @@ -57,24 +61,35 @@ Show **only** these fields on the model: class QuestionType(DjangoObjectType): class Meta: model = Question - only_fields = ('question_text') + fields = ('id', 'question_text') +You can also set the ``fields`` attribute to the special value ``'__all__'`` to indicate that all fields in the model should be used. -exclude_fields -~~~~~~~~~~~~~~ - -Show all fields **except** those in ``exclude_fields``: +For example: .. code:: python class QuestionType(DjangoObjectType): class Meta: model = Question - exclude_fields = ('question_text') + fields = '__all__' -Customised fields -~~~~~~~~~~~~~~~~~ +``exclude`` +~~~~~~~~~~~ + +Show all fields **except** those in ``exclude``: + +.. code:: python + + class QuestionType(DjangoObjectType): + class Meta: + model = Question + exclude = ('question_text',) + + +Customising fields +------------------ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver: @@ -84,7 +99,7 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType class Meta: model = Question - exclude_fields = ('question_text') + fields = ('id', 'question_text') extra_field = graphene.String() @@ -178,7 +193,7 @@ When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``C class QuestionType(DjangoObjectType): class Meta: model = Question - only_fields = ('category',) + fields = ('category',) Then all query-able related models must be defined as DjangoObjectType subclass, or they will fail to show if you are trying to query those relation fields. You only diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index d163ff3..99876b6 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -774,7 +774,7 @@ def test_integer_field_filter_type(): model = Pet interfaces = (Node,) filter_fields = {"age": ["exact"]} - only_fields = ["age"] + fields = ("age",) class Query(ObjectType): pets = DjangoFilterConnectionField(PetType) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 35c8dc8..c419419 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -57,7 +57,9 @@ def convert_serializer_field(field, is_input=True): def convert_serializer_to_input_type(serializer_class): - cached_type = convert_serializer_to_input_type.cache.get(serializer_class.__name__, None) + cached_type = convert_serializer_to_input_type.cache.get( + serializer_class.__name__, None + ) if cached_type: return cached_type serializer = serializer_class() diff --git a/graphene_django/rest_framework/tests/test_multiple_model_serializers.py b/graphene_django/rest_framework/tests/test_multiple_model_serializers.py index 4504610..c1f4626 100644 --- a/graphene_django/rest_framework/tests/test_multiple_model_serializers.py +++ b/graphene_django/rest_framework/tests/test_multiple_model_serializers.py @@ -18,8 +18,12 @@ class MyFakeChildModel(models.Model): class MyFakeParentModel(models.Model): name = models.CharField(max_length=50) created = models.DateTimeField(auto_now_add=True) - child1 = models.OneToOneField(MyFakeChildModel, related_name='parent1', on_delete=models.CASCADE) - child2 = models.OneToOneField(MyFakeChildModel, related_name='parent2', on_delete=models.CASCADE) + child1 = models.OneToOneField( + MyFakeChildModel, related_name="parent1", on_delete=models.CASCADE + ) + child2 = models.OneToOneField( + MyFakeChildModel, related_name="parent2", on_delete=models.CASCADE + ) class ParentType(DjangoObjectType): diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index f466122..f24f84b 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -28,7 +28,7 @@ def test_should_query_only_fields(): class ReporterType(DjangoObjectType): class Meta: model = Reporter - only_fields = ("articles",) + fields = ("articles",) schema = graphene.Schema(query=ReporterType) query = """ @@ -44,7 +44,7 @@ def test_should_query_simplelazy_objects(): class ReporterType(DjangoObjectType): class Meta: model = Reporter - only_fields = ("id",) + fields = ("id",) class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) @@ -289,7 +289,7 @@ def test_should_query_connectionfields(): class Meta: model = Reporter interfaces = (Node,) - only_fields = ("articles",) + fields = ("articles",) class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) @@ -329,7 +329,7 @@ def test_should_keep_annotations(): class Meta: model = Reporter interfaces = (Node,) - only_fields = ("articles",) + fields = ("articles",) class ArticleType(DjangoObjectType): class Meta: diff --git a/graphene_django/tests/test_schema.py b/graphene_django/tests/test_schema.py index 452449b..2c2f74b 100644 --- a/graphene_django/tests/test_schema.py +++ b/graphene_django/tests/test_schema.py @@ -48,6 +48,6 @@ def test_should_map_only_few_fields(): class Reporter2(DjangoObjectType): class Meta: model = Reporter - only_fields = ("id", "email") + fields = ("id", "email") assert list(Reporter2._meta.fields.keys()) == ["id", "email"] diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 6f5ab7e..6cbaae0 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -211,26 +211,113 @@ def with_local_registry(func): @with_local_registry def test_django_objecttype_only_fields(): - class Reporter(DjangoObjectType): - class Meta: - model = ReporterModel - only_fields = ("id", "email", "films") + with pytest.warns(PendingDeprecationWarning): + + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + only_fields = ("id", "email", "films") fields = list(Reporter._meta.fields.keys()) assert fields == ["id", "email", "films"] @with_local_registry -def test_django_objecttype_exclude_fields(): +def test_django_objecttype_fields(): class Reporter(DjangoObjectType): class Meta: model = ReporterModel - exclude_fields = "email" + fields = ("id", "email", "films") + + fields = list(Reporter._meta.fields.keys()) + assert fields == ["id", "email", "films"] + + +@with_local_registry +def test_django_objecttype_only_fields_and_fields(): + with pytest.raises(Exception): + + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + only_fields = ("id", "email", "films") + fields = ("id", "email", "films") + + +@with_local_registry +def test_django_objecttype_all_fields(): + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + fields = "__all__" + + fields = list(Reporter._meta.fields.keys()) + assert len(fields) == len(ReporterModel._meta.get_fields()) + + +@with_local_registry +def test_django_objecttype_exclude_fields(): + with pytest.warns(PendingDeprecationWarning): + + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + exclude_fields = ["email"] fields = list(Reporter._meta.fields.keys()) assert "email" not in fields +@with_local_registry +def test_django_objecttype_exclude(): + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + exclude = ["email"] + + fields = list(Reporter._meta.fields.keys()) + assert "email" not in fields + + +@with_local_registry +def test_django_objecttype_exclude_fields_and_exclude(): + with pytest.raises(Exception): + + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + exclude = ["email"] + exclude_fields = ["email"] + + +@with_local_registry +def test_django_objecttype_exclude_and_only(): + with pytest.raises(AssertionError): + + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + exclude = ["email"] + fields = ["id"] + + +@with_local_registry +def test_django_objecttype_fields_exclude_type_checking(): + with pytest.raises(TypeError): + + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + fields = "foo" + + with pytest.raises(TypeError): + + class Reporter2(DjangoObjectType): + class Meta: + model = ReporterModel + fields = "foo" + + class TestDjangoObjectType: @pytest.fixture def PetModel(self): diff --git a/graphene_django/types.py b/graphene_django/types.py index 6c100ef..ec426f1 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -1,3 +1,4 @@ +import warnings from collections import OrderedDict import six @@ -24,6 +25,9 @@ if six.PY3: from typing import Type +ALL_FIELDS = "__all__" + + def construct_fields( model, registry, only_fields, exclude_fields, convert_choices_to_enum ): @@ -74,8 +78,10 @@ class DjangoObjectType(ObjectType): model=None, registry=None, skip_registry=False, - only_fields=(), - exclude_fields=(), + only_fields=(), # deprecated in favour of `fields` + fields=(), + exclude_fields=(), # deprecated in favour of `exclude` + exclude=(), filter_fields=None, filterset_class=None, connection=None, @@ -109,10 +115,48 @@ class DjangoObjectType(ObjectType): ) ) + assert not (fields and exclude), ( + "Cannot set both 'fields' and 'exclude' options on " + "DjangoObjectType {class_name}.".format(class_name=cls.__name__) + ) + + # Alias only_fields -> fields + if only_fields and fields: + raise Exception("Can't set both only_fields and fields") + if only_fields: + warnings.warn( + "Defining `only_fields` is deprecated in favour of `fields`.", + PendingDeprecationWarning, + stacklevel=2, + ) + fields = only_fields + if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)): + raise TypeError( + 'The `fields` option must be a list or tuple or "__all__". ' + "Got %s." % type(fields).__name__ + ) + + if fields == ALL_FIELDS: + fields = None + + # Alias exclude_fields -> exclude + if exclude_fields and exclude: + raise Exception("Can't set both exclude_fields and exclude") + if exclude_fields: + warnings.warn( + "Defining `exclude_fields` is deprecated in favour of `exclude`.", + PendingDeprecationWarning, + stacklevel=2, + ) + exclude = exclude_fields + if exclude and not isinstance(exclude, (list, tuple)): + raise TypeError( + "The `exclude` option must be a list or tuple. Got %s." + % type(exclude).__name__ + ) + django_fields = yank_fields_from_attrs( - construct_fields( - model, registry, only_fields, exclude_fields, convert_choices_to_enum - ), + construct_fields(model, registry, fields, exclude, convert_choices_to_enum), _as=Field, ) From 224725039bb15373890d49329bb588104ab275cd Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Thu, 11 Jul 2019 22:32:07 +0300 Subject: [PATCH 40/44] =?UTF-8?q?Asserting=20status=20code=20before=20deco?= =?UTF-8?q?ding=20json=20in=20assertResponseNoEr=E2=80=A6=20(#708)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphene_django/utils/testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/utils/testing.py b/graphene_django/utils/testing.py index db3e9f4..0fdac7e 100644 --- a/graphene_django/utils/testing.py +++ b/graphene_django/utils/testing.py @@ -54,8 +54,8 @@ class GraphQLTestCase(TestCase): the call was fine. :resp HttpResponse: Response """ - content = json.loads(resp.content) self.assertEqual(resp.status_code, 200) + content = json.loads(resp.content) self.assertNotIn("errors", list(content.keys())) def assertResponseHasErrors(self, resp): From de98fb58121ec5c7126800ef59896d4e2fc23702 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 12 Jul 2019 17:38:26 +0100 Subject: [PATCH 41/44] v2.4.0 (#706) --- graphene_django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py index 51acfd2..e09f2a2 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,6 +1,6 @@ from .types import DjangoObjectType from .fields import DjangoConnectionField -__version__ = "2.3.0" +__version__ = "2.4.0" __all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"] From 51adb3632bab8ce0f200cde0686a158436f07ab3 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sat, 27 Jul 2019 16:14:34 +0200 Subject: [PATCH 42/44] Update readme with Django path (#720) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 159a592..33f71f3 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ GRAPHENE = { We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries. ```python -from django.conf.urls import url +from django.urls import path from graphene_django.views import GraphQLView urlpatterns = [ # ... - url(r'^graphql$', GraphQLView.as_view(graphiql=True)), + path('graphql', GraphQLView.as_view(graphiql=True)), ] ``` @@ -100,4 +100,4 @@ To learn more check out the following [examples](examples/): ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) \ No newline at end of file +See [CONTRIBUTING.md](CONTRIBUTING.md) From b1a9293016a5263efe9ed39b1f6db2dac0b9623a Mon Sep 17 00:00:00 2001 From: Jason Kraus Date: Thu, 1 Aug 2019 01:07:52 -0700 Subject: [PATCH 43/44] fix choices enum: if field can be blank then it isnt required (#714) --- graphene_django/converter.py | 3 ++- graphene_django/tests/models.py | 2 +- graphene_django/tests/test_types.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 64bf341..b1e27fc 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -73,7 +73,8 @@ def convert_django_field_with_choices( return named_choices_descriptions[self.name] enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType) - converted = enum(description=field.help_text, required=not field.null) + required = not (field.blank or field.null) + converted = enum(description=field.help_text, required=required) else: converted = convert_django_field(field, registry) if registry is not None: diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index b4eb3ce..14a8367 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -38,7 +38,7 @@ class Reporter(models.Model): last_name = models.CharField(max_length=30) email = models.EmailField() pets = models.ManyToManyField("self") - a_choice = models.CharField(max_length=30, choices=CHOICES) + a_choice = models.CharField(max_length=30, choices=CHOICES, blank=True) objects = models.Manager() doe_objects = DoeReporterManager() diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 6cbaae0..8b84fca 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -171,7 +171,7 @@ type Reporter { lastName: String! email: String! pets: [Reporter!]! - aChoice: ReporterAChoice! + aChoice: ReporterAChoice reporterType: ReporterReporterType articles(before: String, after: String, first: Int, last: Int): ArticleConnection } From 59f4f134b584d54e3accc5c8f1abeaca8b17a003 Mon Sep 17 00:00:00 2001 From: Alexandre Kirszenberg Date: Thu, 1 Aug 2019 18:31:18 +0200 Subject: [PATCH 44/44] Set converted Django connections to required (#610) --- graphene_django/converter.py | 6 ++++-- graphene_django/filter/fields.py | 2 +- graphene_django/tests/test_converter.py | 2 +- graphene_django/tests/test_types.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index b1e27fc..063d6be 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -195,9 +195,11 @@ def convert_field_to_list_or_connection(field, registry=None): if _type._meta.filter_fields or _type._meta.filterset_class: from .filter.fields import DjangoFilterConnectionField - return DjangoFilterConnectionField(_type, description=description) + return DjangoFilterConnectionField( + _type, required=True, description=description + ) - return DjangoConnectionField(_type, description=description) + return DjangoConnectionField(_type, required=True, description=description) return DjangoListField( _type, diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index 62f4b1a..338becb 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -111,7 +111,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): return partial( self.connection_resolver, parent_resolver, - self.type, + self.connection_type, self.get_manager(), self.max_limit, self.enforce_first_or_last, diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 00467b4..3790c4a 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -255,7 +255,7 @@ def test_should_manytomany_convert_connectionorlist_connection(): assert isinstance(graphene_field, graphene.Dynamic) dynamic_field = graphene_field.get_type() assert isinstance(dynamic_field, ConnectionField) - assert dynamic_field.type == A._meta.connection + assert dynamic_field.type.of_type == A._meta.connection def test_should_manytoone_convert_connectionorlist(): diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 8b84fca..5e9d1c2 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -173,7 +173,7 @@ type Reporter { pets: [Reporter!]! aChoice: ReporterAChoice reporterType: ReporterReporterType - articles(before: String, after: String, first: Int, last: Int): ArticleConnection + articles(before: String, after: String, first: Int, last: Int): ArticleConnection! } enum ReporterAChoice {