From e7f7d8da07ba1020f9916153f17e97b0ec037712 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Fri, 11 Jun 2021 15:41:02 -0500 Subject: [PATCH 01/38] Add missing auto fields (#1212) * add missing auto fields * add missing auto fields * skip small auto field sometimes * make small auto optional * make small auto optional --- graphene_django/converter.py | 8 ++++++++ graphene_django/tests/test_converter.py | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index da96161..c243e82 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -166,11 +166,19 @@ def convert_field_to_string(field, registry=None): ) +@convert_django_field.register(models.BigAutoField) @convert_django_field.register(models.AutoField) def convert_field_to_id(field, registry=None): return ID(description=get_django_field_description(field), required=not field.null) +if hasattr(models, "SmallAutoField"): + + @convert_django_field.register(models.SmallAutoField) + def convert_field_small_to_id(field, registry=None): + return convert_field_to_id(field, registry) + + @convert_django_field.register(models.UUIDField) def convert_field_to_uuid(field, registry=None): return UUID( diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index fe84e73..afd744f 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -111,6 +111,15 @@ def test_should_auto_convert_id(): assert_conversion(models.AutoField, graphene.ID, primary_key=True) +def test_should_big_auto_convert_id(): + assert_conversion(models.BigAutoField, graphene.ID, primary_key=True) + + +def test_should_small_auto_convert_id(): + if hasattr(models, "SmallAutoField"): + assert_conversion(models.SmallAutoField, graphene.ID, primary_key=True) + + def test_should_uuid_convert_id(): assert_conversion(models.UUIDField, graphene.UUID) From 1e4b03b9756baebbfcf7e9e3a25932a4e613570b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:49:16 +0300 Subject: [PATCH 02/38] Bump django from 3.1.8 to 3.1.14 in /examples/cookbook-plain (#1282) Bumps [django](https://github.com/django/django) from 3.1.8 to 3.1.14. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.1.8...3.1.14) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- 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 9fc1a3a..85a8963 100644 --- a/examples/cookbook-plain/requirements.txt +++ b/examples/cookbook-plain/requirements.txt @@ -1,4 +1,4 @@ graphene>=2.1,<3 graphene-django>=2.1,<3 graphql-core>=2.1,<3 -django==3.1.8 +django==3.1.14 From ef9d67302ef85f1a5d3047a44cf65a2173c3ca2e Mon Sep 17 00:00:00 2001 From: Chouaib Lammas <54365321+chlammas@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:51:10 +0000 Subject: [PATCH 03/38] Fix ingredient model (#1258) Add the required positional argument: 'on_delete' --- docs/tutorial-relay.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index acc4b0d..3de9022 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -70,7 +70,7 @@ Let's get started with these models: class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField() - category = models.ForeignKey(Category, related_name='ingredients') + category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE) def __str__(self): return self.name From 32667b5407a81ec58a0c22936f9ef234be47e20c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:58:03 +0300 Subject: [PATCH 04/38] Bump django from 3.1.8 to 3.1.14 in /examples/cookbook (#1283) Bumps [django](https://github.com/django/django) from 3.1.8 to 3.1.14. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.1.8...3.1.14) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- 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 f6d03ff..a5b0b96 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -1,5 +1,5 @@ graphene>=2.1,<3 graphene-django>=2.1,<3 graphql-core>=2.1,<3 -django==3.1.8 +django==3.1.14 django-filter>=2 From 5d5d7f18154adc0d89e2c6baebd47c4e5d7d9fbf Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Fri, 7 Jan 2022 14:26:07 -0600 Subject: [PATCH 05/38] Django v4, python 3.10 support for graphene-django v3 (#1281) Co-authored-by: Yair Silbermintz --- .github/workflows/tests.yml | 8 +++++++- README.md | 2 +- docs/authorization.rst | 2 +- graphene_django/debug/exception/formating.py | 2 +- .../management/commands/graphql_schema.py | 2 +- graphene_django/tests/urls.py | 6 +++--- graphene_django/tests/urls_inherited.py | 4 ++-- graphene_django/tests/urls_pretty.py | 4 ++-- graphene_django/utils/testing.py | 2 +- tox.ini | 13 ++++++++----- 10 files changed, 27 insertions(+), 18 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2dbf822..c63742a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,13 @@ jobs: matrix: django: ["2.2", "3.0", "3.1", "3.2"] python-version: ["3.6", "3.7", "3.8", "3.9"] - + include: + - django: "3.2" + python-version: "3.10" + - django: "4.0" + python-version: "3.10" + - django: "main" + python-version: "3.10" steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/README.md b/README.md index 5045e78..6f06ccc 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ from graphene_django.views import GraphQLView urlpatterns = [ # ... - path('graphql', GraphQLView.as_view(graphiql=True)), + path('graphql/', GraphQLView.as_view(graphiql=True)), ] ``` diff --git a/docs/authorization.rst b/docs/authorization.rst index 39305f6..bc88cda 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -198,7 +198,7 @@ For Django 2.2 and above: urlpatterns = [ # some other urls - path('graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)), + path('graphql/', PrivateGraphQLView.as_view(graphiql=True, schema=schema)), ] .. _LoginRequiredMixin: https://docs.djangoproject.com/en/dev/topics/auth/default/#the-loginrequired-mixin diff --git a/graphene_django/debug/exception/formating.py b/graphene_django/debug/exception/formating.py index ed7ebab..0d477b3 100644 --- a/graphene_django/debug/exception/formating.py +++ b/graphene_django/debug/exception/formating.py @@ -11,7 +11,7 @@ def wrap_exception(exception): exc_type=force_str(type(exception)), stack="".join( traceback.format_exception( - etype=type(exception), value=exception, tb=exception.__traceback__ + exception, value=exception, tb=exception.__traceback__ ) ), ) diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index 565f5d8..4165430 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -48,7 +48,7 @@ class CommandArguments(BaseCommand): class Command(CommandArguments): help = "Dump Graphene schema as a JSON or GraphQL file" can_import_settings = True - requires_system_checks = False + requires_system_checks = [] def save_json_file(self, out, schema_dict, indent): with open(out, "w") as outfile: diff --git a/graphene_django/tests/urls.py b/graphene_django/tests/urls.py index 66b3fc4..3702ce5 100644 --- a/graphene_django/tests/urls.py +++ b/graphene_django/tests/urls.py @@ -1,8 +1,8 @@ -from django.conf.urls import url +from django.urls import path from ..views import GraphQLView urlpatterns = [ - url(r"^graphql/batch", GraphQLView.as_view(batch=True)), - url(r"^graphql", GraphQLView.as_view(graphiql=True)), + path("graphql/batch", GraphQLView.as_view(batch=True)), + path("graphql", GraphQLView.as_view(graphiql=True)), ] diff --git a/graphene_django/tests/urls_inherited.py b/graphene_django/tests/urls_inherited.py index 6fa8019..1e65da0 100644 --- a/graphene_django/tests/urls_inherited.py +++ b/graphene_django/tests/urls_inherited.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from ..views import GraphQLView from .schema_view import schema @@ -10,4 +10,4 @@ class CustomGraphQLView(GraphQLView): pretty = True -urlpatterns = [url(r"^graphql/inherited/$", CustomGraphQLView.as_view())] +urlpatterns = [path("graphql/inherited/", CustomGraphQLView.as_view())] diff --git a/graphene_django/tests/urls_pretty.py b/graphene_django/tests/urls_pretty.py index 1133c87..6275934 100644 --- a/graphene_django/tests/urls_pretty.py +++ b/graphene_django/tests/urls_pretty.py @@ -1,6 +1,6 @@ -from django.conf.urls import url +from django.urls import path from ..views import GraphQLView from .schema_view import schema -urlpatterns = [url(r"^graphql", GraphQLView.as_view(schema=schema, pretty=True))] +urlpatterns = [path("graphql", GraphQLView.as_view(schema=schema, pretty=True))] diff --git a/graphene_django/utils/testing.py b/graphene_django/utils/testing.py index 763196d..b91a02f 100644 --- a/graphene_django/utils/testing.py +++ b/graphene_django/utils/testing.py @@ -3,7 +3,7 @@ import warnings from django.test import Client, TestCase, TransactionTestCase -DEFAULT_GRAPHQL_URL = "/graphql/" +DEFAULT_GRAPHQL_URL = "/graphql" def graphql_query( diff --git a/tox.ini b/tox.ini index 7128afe..d65839a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,8 @@ [tox] envlist = - py{36,37,38,39}-django{22,30,31,32,main}, + py{36,37,38,39}-django{22,30,31}, + py{36,37,38,39,310}-django32, + py{38,39,310}-django{40,main}, black,flake8 [gh-actions] @@ -9,6 +11,7 @@ python = 3.7: py37 3.8: py38 3.9: py39 + 3.10: py310 [gh-actions:env] DJANGO = @@ -16,6 +19,7 @@ DJANGO = 3.0: django30 3.1: django31 3.2: django32 + 4.0: django40 main: djangomain [testenv] @@ -26,12 +30,11 @@ setenv = deps = -e.[test] psycopg2-binary - django20: Django>=2.0,<2.1 - django21: Django>=2.1,<2.2 django22: Django>=2.2,<3.0 - django30: Django>=3.0a1,<3.1 + django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 - django32: Django>=3.2a1,<3.3 + django32: Django>=3.2,<4.0 + django40: Django>=4.0,<4.1 djangomain: https://github.com/django/django/archive/main.zip commands = {posargs:py.test --cov=graphene_django graphene_django examples} From e1a7d1983314174c91ede1ebbfe35a9009cf6268 Mon Sep 17 00:00:00 2001 From: Jarkko Piiroinen <183207+jmp@users.noreply.github.com> Date: Tue, 18 Jan 2022 16:03:08 +0200 Subject: [PATCH 06/38] Convert DecimalField to Decimal instead of Float in DRF and form converters (#1277) * Convert serializer DecimalField to Decimal * Convert form DecimalField to Decimal --- graphene_django/filter/tests/test_fields.py | 4 ++-- graphene_django/forms/converter.py | 22 +++++++++++++++++-- graphene_django/forms/tests/test_converter.py | 5 +++-- .../rest_framework/serializer_converter.py | 6 ++++- .../tests/test_field_converter.py | 4 ++-- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 17b4630..7d440f4 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -5,7 +5,7 @@ import pytest from django.db.models import TextField, Value from django.db.models.functions import Concat -from graphene import Argument, Boolean, Field, Float, ObjectType, Schema, String +from graphene import Argument, Boolean, Decimal, Field, ObjectType, Schema, String from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField @@ -401,7 +401,7 @@ def test_filterset_descriptions(): field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter) max_time = field.args["max_time"] assert isinstance(max_time, Argument) - assert max_time.type == Float + assert max_time.type == Decimal assert max_time.description == "The maximum time" diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index b64e478..47eb51d 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -3,7 +3,19 @@ from functools import singledispatch from django import forms from django.core.exceptions import ImproperlyConfigured -from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time +from graphene import ( + ID, + Boolean, + Decimal, + Float, + Int, + List, + String, + UUID, + Date, + DateTime, + Time, +) from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField @@ -57,12 +69,18 @@ def convert_form_field_to_nullboolean(field): return Boolean(description=get_form_field_description(field)) -@convert_form_field.register(forms.DecimalField) @convert_form_field.register(forms.FloatField) def convert_form_field_to_float(field): return Float(description=get_form_field_description(field), required=field.required) +@convert_form_field.register(forms.DecimalField) +def convert_form_field_to_decimal(field): + return Decimal( + description=get_form_field_description(field), required=field.required + ) + + @convert_form_field.register(forms.MultipleChoiceField) def convert_form_field_to_string_list(field): return List( diff --git a/graphene_django/forms/tests/test_converter.py b/graphene_django/forms/tests/test_converter.py index ccf630f..05584a5 100644 --- a/graphene_django/forms/tests/test_converter.py +++ b/graphene_django/forms/tests/test_converter.py @@ -6,6 +6,7 @@ from graphene import ( String, Int, Boolean, + Decimal, Float, ID, UUID, @@ -97,8 +98,8 @@ def test_should_float_convert_float(): assert_conversion(forms.FloatField, Float) -def test_should_decimal_convert_float(): - assert_conversion(forms.DecimalField, Float) +def test_should_decimal_convert_decimal(): + assert_conversion(forms.DecimalField, Decimal) def test_should_multiple_choice_convert_list(): diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index b26e5e6..9835475 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -110,11 +110,15 @@ def convert_serializer_field_to_bool(field): @get_graphene_type_from_serializer_field.register(serializers.FloatField) -@get_graphene_type_from_serializer_field.register(serializers.DecimalField) def convert_serializer_field_to_float(field): return graphene.Float +@get_graphene_type_from_serializer_field.register(serializers.DecimalField) +def convert_serializer_field_to_decimal(field): + return graphene.Decimal + + @get_graphene_type_from_serializer_field.register(serializers.DateTimeField) def convert_serializer_field_to_datetime_time(field): return graphene.types.datetime.DateTime diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index daa8349..4858365 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -133,9 +133,9 @@ def test_should_float_convert_float(): assert_conversion(serializers.FloatField, graphene.Float) -def test_should_decimal_convert_float(): +def test_should_decimal_convert_decimal(): assert_conversion( - serializers.DecimalField, graphene.Float, max_digits=4, decimal_places=2 + serializers.DecimalField, graphene.Decimal, max_digits=4, decimal_places=2 ) From 775644b5369bdc5fbb45d3535ae391a069ebf9d4 Mon Sep 17 00:00:00 2001 From: Keith Date: Sat, 22 Jan 2022 12:04:30 -0800 Subject: [PATCH 07/38] Update requirements to the official graphene 3.0 release (#1290) --- graphene_django/fields.py | 4 +++- setup.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index e1972c7..eb932c1 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,12 +1,14 @@ from functools import partial from django.db.models.query import QuerySet -from graphql_relay.connection.arrayconnection import ( + +from graphql_relay.connection.array_connection import ( connection_from_array_slice, cursor_to_offset, get_offset_with_default, offset_to_cursor, ) + from promise import Promise from graphene import Int, NonNull diff --git a/setup.py b/setup.py index fd403c0..1762760 100644 --- a/setup.py +++ b/setup.py @@ -60,8 +60,9 @@ setup( keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests", "examples", "examples.*"]), install_requires=[ - "graphene>=3.0.0b5,<4", + "graphene>=3.0,<4", "graphql-core>=3.1.0,<4", + "graphql-relay>=3.1.1,<4", "Django>=2.2", "promise>=2.1", "text-unidecode", From bf8fd7696b2aa35489b97956915f596527441f55 Mon Sep 17 00:00:00 2001 From: Peter Paul Kiefer Date: Sat, 12 Feb 2022 15:31:45 +0100 Subject: [PATCH 08/38] fixed broken links to graphene filter documentation (master->main) (#1309) Co-authored-by: Peter Paul Kiefer --- docs/filtering.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/filtering.rst b/docs/filtering.rst index 934bad6..16002c2 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -2,8 +2,8 @@ Filtering ========= Graphene integrates with -`django-filter `__ to provide filtering of results. -See the `usage documentation `__ +`django-filter `__ to provide filtering of results. +See the `usage documentation `__ for details on the format for ``filter_fields``. This filtering is automatically available when implementing a ``relay.Node``. @@ -34,7 +34,7 @@ Filterable fields The ``filter_fields`` parameter is used to specify the fields which can be filtered upon. The value specified here is passed directly to ``django-filter``, so see the `filtering -documentation `__ +documentation `__ for full details on the range of options available. For example: @@ -192,7 +192,7 @@ in unison with the ``filter_fields`` parameter: all_animals = DjangoFilterConnectionField(AnimalNode) -The context argument is passed on as the `request argument `__ +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 pre-filter animals owned by the authenticated user (set in ``context.user``). From 0bb9f1ca60458050911eb17ab2f0e1203561c91f Mon Sep 17 00:00:00 2001 From: Peter Paul Kiefer Date: Sun, 13 Feb 2022 06:50:53 +0100 Subject: [PATCH 09/38] I found another wrong link in the filter dokumentation see #1309 (#1311) * fixed broken links to graphene filter documentation (master->main) * #1295 There is still a wrong link to github The referenced example is in main branch but the link goes to the master branch which still exists. Co-authored-by: Peter Paul Kiefer --- docs/filtering.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/filtering.rst b/docs/filtering.rst index 16002c2..fb686a1 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -26,7 +26,7 @@ After installing ``django-filter`` you'll need to add the application in the ``s ] Note: The techniques below are demoed in the `cookbook example -app `__. +app `__. Filterable fields ----------------- From f6ec0689c18929344c79ae363d2e3d5628fa4a2d Mon Sep 17 00:00:00 2001 From: Aaron Forsander Date: Thu, 3 Mar 2022 08:58:48 -0500 Subject: [PATCH 10/38] Fix documentation references: op_name -> operation_name (#1312) --- docs/testing.rst | 10 +++++----- graphene_django/utils/testing.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 65b6f64..fb0a85d 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -27,7 +27,7 @@ Usage: } } ''', - op_name='myModel' + operation_name='myModel' ) content = json.loads(response.content) @@ -48,7 +48,7 @@ Usage: } } ''', - op_name='myModel', + operation_name='myModel', variables={'id': 1} ) @@ -72,7 +72,7 @@ Usage: } } ''', - op_name='myMutation', + operation_name='myMutation', input_data={'my_field': 'foo', 'other_field': 'bar'} ) @@ -107,7 +107,7 @@ Usage: } } ''', - op_name='myMutation', + operation_name='myMutation', input_data={'my_field': 'foo', 'other_field': 'bar'} ) @@ -147,7 +147,7 @@ To use pytest define a simple fixture using the query helper below } } ''', - op_name='myModel' + operation_name='myModel' ) content = json.loads(response.content) diff --git a/graphene_django/utils/testing.py b/graphene_django/utils/testing.py index b91a02f..f94c385 100644 --- a/graphene_django/utils/testing.py +++ b/graphene_django/utils/testing.py @@ -19,7 +19,7 @@ def graphql_query( Args: query (string) - GraphQL query to run operation_name (string) - If the query is a mutation or named query, you must - supply the op_name. For annon queries ("{ ... }"), + supply the operation_name. For annon queries ("{ ... }"), should be None (default). input_data (dict) - If provided, the $input variable in GraphQL will be set to this value. If both ``input_data`` and ``variables``, @@ -78,7 +78,7 @@ class GraphQLTestMixin(object): Args: query (string) - GraphQL query to run operation_name (string) - If the query is a mutation or named query, you must - supply the op_name. For annon queries ("{ ... }"), + supply the operation_name. For annon queries ("{ ... }"), should be None (default). input_data (dict) - If provided, the $input variable in GraphQL will be set to this value. If both ``input_data`` and ``variables``, @@ -89,7 +89,7 @@ class GraphQLTestMixin(object): headers (dict) - If provided, the headers in POST request to GRAPHQL_URL will be set to this value. Keys should be prepended with "HTTP_" (e.g. to specify the "Authorization" HTTP header, - use "HTTP_AUTHORIZATION" as the key). + use "HTTP_AUTHORIZATION" as the key). Returns: Response object from client From 5f1731dca31bc1b8df766a73011be02744f59c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolai=20R=C3=B8ed=20Kristiansen?= Date: Mon, 15 Aug 2022 11:41:39 +0200 Subject: [PATCH 11/38] Fix: Use .formatted instead of format_error (#1327) & Fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 👽 Use .formatted instead of format_error * ✅ Fix test with newer graphene null default values (graphql-python/graphene@03277a5) no more trailing newlines --- .../tests/test_array_field_exact_filter.py | 5 +---- .../filter/tests/test_enum_filtering.py | 5 +---- graphene_django/filter/tests/test_fields.py | 10 ++++------ .../filter/tests/test_typed_filter.py | 2 +- graphene_django/tests/test_command.py | 3 +-- graphene_django/tests/test_types.py | 20 +++++++------------ graphene_django/tests/test_views.py | 5 ----- graphene_django/views.py | 3 +-- 8 files changed, 16 insertions(+), 37 deletions(-) diff --git a/graphene_django/filter/tests/test_array_field_exact_filter.py b/graphene_django/filter/tests/test_array_field_exact_filter.py index cd72868..10e32ef 100644 --- a/graphene_django/filter/tests/test_array_field_exact_filter.py +++ b/graphene_django/filter/tests/test_array_field_exact_filter.py @@ -120,10 +120,7 @@ def test_array_field_filter_schema_type(Query): "randomField": "[Boolean!]", } filters_str = ", ".join( - [ - f"{filter_field}: {gql_type} = null" - for filter_field, gql_type in filters.items() - ] + [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()] ) assert ( f"type Query {{\n events({filters_str}): EventTypeConnection\n}}" in schema_str diff --git a/graphene_django/filter/tests/test_enum_filtering.py b/graphene_django/filter/tests/test_enum_filtering.py index 09c69b3..4fe7ddd 100644 --- a/graphene_django/filter/tests/test_enum_filtering.py +++ b/graphene_django/filter/tests/test_enum_filtering.py @@ -152,9 +152,6 @@ def test_filter_enum_field_schema_type(schema): "reporter_AChoice_In": "[TestsReporterAChoiceChoices]", } filters_str = ", ".join( - [ - f"{filter_field}: {gql_type} = null" - for filter_field, gql_type in filters.items() - ] + [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()] ) assert f" allArticles({filters_str}): ArticleTypeConnection\n" in schema_str diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 7d440f4..370c894 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -1008,7 +1008,7 @@ def test_integer_field_filter_type(): assert str(schema) == dedent( """\ type Query { - pets(offset: Int = null, before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection + pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection } type PetTypeConnection { @@ -1056,8 +1056,7 @@ def test_integer_field_filter_type(): interface Node { \"""The ID of the object\""" id: ID! - } - """ + }""" ) @@ -1077,7 +1076,7 @@ def test_other_filter_types(): assert str(schema) == dedent( """\ type Query { - pets(offset: Int = null, before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null, age_Isnull: Boolean = null, age_Lt: Int = null): PetTypeConnection + pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection } type PetTypeConnection { @@ -1125,8 +1124,7 @@ def test_other_filter_types(): interface Node { \"""The ID of the object\""" id: ID! - } - """ + }""" ) diff --git a/graphene_django/filter/tests/test_typed_filter.py b/graphene_django/filter/tests/test_typed_filter.py index b903b59..cc0bafe 100644 --- a/graphene_django/filter/tests/test_typed_filter.py +++ b/graphene_django/filter/tests/test_typed_filter.py @@ -98,7 +98,7 @@ def test_typed_filter_schema(schema): ) for filter_field, gql_type in filters.items(): - assert "{}: {} = null".format(filter_field, gql_type) in all_articles_filters + assert "{}: {}".format(filter_field, gql_type) in all_articles_filters def test_typed_filters_work(schema): diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py index 70116b8..11a15bc 100644 --- a/graphene_django/tests/test_command.py +++ b/graphene_django/tests/test_command.py @@ -53,6 +53,5 @@ def test_generate_graphql_file_on_call_graphql_schema(): """\ type Query { hi: String - } - """ + }""" ) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index bde72c7..4885917 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -183,7 +183,7 @@ def test_schema_representation(): pets: [Reporter!]! aChoice: TestsReporterAChoiceChoices reporterType: TestsReporterReporterTypeChoices - articles(offset: Int = null, before: String = null, after: String = null, first: Int = null, last: Int = null): ArticleConnection! + articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection! } \"""An enumeration.\""" @@ -244,8 +244,7 @@ def test_schema_representation(): \"""The ID of the object\""" id: ID! ): Node - } - """ + }""" ) assert str(schema) == expected @@ -525,8 +524,7 @@ class TestDjangoObjectType: id: ID! kind: String! cuteness: Int! - } - """ + }""" ) def test_django_objecttype_convert_choices_enum_list(self, PetModel): @@ -560,8 +558,7 @@ class TestDjangoObjectType: \"""Dog\""" DOG - } - """ + }""" ) def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel): @@ -586,8 +583,7 @@ class TestDjangoObjectType: id: ID! kind: String! cuteness: Int! - } - """ + }""" ) def test_django_objecttype_convert_choices_enum_naming_collisions( @@ -621,8 +617,7 @@ class TestDjangoObjectType: \"""Dog\""" DOG - } - """ + }""" ) def test_django_objecttype_choices_custom_enum_name( @@ -660,8 +655,7 @@ class TestDjangoObjectType: \"""Dog\""" DOG - } - """ + }""" ) diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py index 945fa87..c2f18c3 100644 --- a/graphene_django/tests/test_views.py +++ b/graphene_django/tests/test_views.py @@ -109,12 +109,10 @@ def test_reports_validation_errors(client): { "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], - "path": None, }, { "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], - "path": None, }, ] } @@ -135,8 +133,6 @@ def test_errors_when_missing_operation_name(client): "errors": [ { "message": "Must provide operation name if query contains multiple operations.", - "locations": None, - "path": None, } ] } @@ -477,7 +473,6 @@ def test_handles_syntax_errors_caught_by_graphql(client): { "locations": [{"column": 1, "line": 1}], "message": "Syntax Error: Unexpected Name 'syntaxerror'.", - "path": None, } ] } diff --git a/graphene_django/views.py b/graphene_django/views.py index c23b020..f533f70 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -11,7 +11,6 @@ from django.views.decorators.csrf import ensure_csrf_cookie from django.views.generic import View from graphql import OperationType, get_operation_ast, parse, validate from graphql.error import GraphQLError -from graphql.error import format_error as format_graphql_error from graphql.execution import ExecutionResult from graphene import Schema @@ -387,7 +386,7 @@ class GraphQLView(View): @staticmethod def format_error(error): if isinstance(error, GraphQLError): - return format_graphql_error(error) + return error.formatted return {"message": str(error)} From 2aeb86ba3bed7dc821cc8508e1dca8297aa7dea4 Mon Sep 17 00:00:00 2001 From: Thomas Leonard <64223923+tcleonard@users.noreply.github.com> Date: Tue, 6 Sep 2022 14:00:13 +0200 Subject: [PATCH 12/38] fix: backward pagination indexing error when using bigger last argument than total number of elements (#1344) Co-authored-by: Thomas Leonard --- graphene_django/fields.py | 24 ++++----- graphene_django/tests/test_query.py | 75 +++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index eb932c1..c881456 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -146,36 +146,38 @@ class DjangoConnectionField(ConnectionField): iterable = maybe_queryset(iterable) if isinstance(iterable, QuerySet): - list_length = iterable.count() + array_length = iterable.count() else: - list_length = len(iterable) - list_slice_length = ( - min(max_limit, list_length) if max_limit is not None else list_length + array_length = len(iterable) + array_slice_length = ( + min(max_limit, array_length) if max_limit is not None else array_length ) # If after is higher than list_length, connection_from_list_slice # would try to do a negative slicing which makes django throw an # AssertionError - after = min(get_offset_with_default(args.get("after"), -1) + 1, list_length) + slice_start = min( + get_offset_with_default(args.get("after"), -1) + 1, array_length + ) if max_limit is not None and args.get("first", None) is None: if args.get("last", None) is not None: - after = list_length - args["last"] + slice_start = max(array_length - args["last"], 0) else: args["first"] = max_limit connection = connection_from_array_slice( - iterable[after:], + iterable[slice_start:], args, - slice_start=after, - array_length=list_length, - array_slice_length=list_slice_length, + slice_start=slice_start, + array_length=array_length, + array_slice_length=array_slice_length, connection_type=partial(connection_adapter, connection), edge_type=connection.Edge, page_info_type=page_info_adapter, ) connection.iterable = iterable - connection.length = list_length + connection.length = array_length return connection @classmethod diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index aabe19c..5cbf90e 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1593,3 +1593,78 @@ def test_connection_should_allow_offset_filtering_with_after(): "allReporters": {"edges": [{"node": {"firstName": "Jane", "lastName": "Roe"}},]} } assert result.data == expected + + +def test_connection_should_succeed_if_last_higher_than_number_of_objects(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query ReporterPromiseConnectionQuery ($last: Int) { + allReporters(last: $last) { + edges { + node { + firstName + lastName + } + } + } + } + """ + + result = schema.execute(query, variable_values=dict(last=2)) + assert not result.errors + expected = {"allReporters": {"edges": []}} + assert result.data == expected + + Reporter.objects.create(first_name="John", last_name="Doe") + Reporter.objects.create(first_name="Some", last_name="Guy") + Reporter.objects.create(first_name="Jane", last_name="Roe") + Reporter.objects.create(first_name="Some", last_name="Lady") + + result = schema.execute(query, variable_values=dict(last=2)) + assert not result.errors + expected = { + "allReporters": { + "edges": [ + {"node": {"firstName": "Jane", "lastName": "Roe"}}, + {"node": {"firstName": "Some", "lastName": "Lady"}}, + ] + } + } + assert result.data == expected + + result = schema.execute(query, variable_values=dict(last=4)) + assert not result.errors + expected = { + "allReporters": { + "edges": [ + {"node": {"firstName": "John", "lastName": "Doe"}}, + {"node": {"firstName": "Some", "lastName": "Guy"}}, + {"node": {"firstName": "Jane", "lastName": "Roe"}}, + {"node": {"firstName": "Some", "lastName": "Lady"}}, + ] + } + } + assert result.data == expected + + result = schema.execute(query, variable_values=dict(last=20)) + assert not result.errors + expected = { + "allReporters": { + "edges": [ + {"node": {"firstName": "John", "lastName": "Doe"}}, + {"node": {"firstName": "Some", "lastName": "Guy"}}, + {"node": {"firstName": "Jane", "lastName": "Roe"}}, + {"node": {"firstName": "Some", "lastName": "Lady"}}, + ] + } + } + assert result.data == expected From 8ae576394ec9922cc1ab962697c2239b6a9a3325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolai=20R=C3=B8ed=20Kristiansen?= Date: Mon, 19 Sep 2022 14:31:04 +0200 Subject: [PATCH 13/38] =?UTF-8?q?=F0=9F=92=A5=20Stop=20supporting=20EOL=20?= =?UTF-8?q?djangos=20and=20pythons=20(#1337)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 💥 Stop supporting EOL djangos and pythons * 👷 Run only supported version in test workflow --- .github/workflows/tests.yml | 10 +++------- setup.py | 9 ++++----- tox.ini | 14 ++++---------- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c63742a..c2cdc99 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,15 +8,11 @@ jobs: strategy: max-parallel: 4 matrix: - django: ["2.2", "3.0", "3.1", "3.2"] - python-version: ["3.6", "3.7", "3.8", "3.9"] + django: ["3.2", "4.0", "4.1"] + python-version: ["3.8", "3.9", "3.10"] include: - django: "3.2" - python-version: "3.10" - - django: "4.0" - python-version: "3.10" - - django: "main" - python-version: "3.10" + python-version: "3.7" steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/setup.py b/setup.py index 1762760..306ec33 100644 --- a/setup.py +++ b/setup.py @@ -46,16 +46,15 @@ setup( "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: Django", - "Framework :: Django :: 2.2", - "Framework :: Django :: 3.0", - "Framework :: Django :: 3.1", "Framework :: Django :: 3.2", + "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", ], keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests", "examples", "examples.*"]), @@ -63,7 +62,7 @@ setup( "graphene>=3.0,<4", "graphql-core>=3.1.0,<4", "graphql-relay>=3.1.1,<4", - "Django>=2.2", + "Django>=3.2", "promise>=2.1", "text-unidecode", ], diff --git a/tox.ini b/tox.ini index d65839a..11b4893 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,11 @@ [tox] envlist = - py{36,37,38,39}-django{22,30,31}, - py{36,37,38,39,310}-django32, - py{38,39,310}-django{40,main}, + py{37,38,39,310}-django32, + py{38,39,310}-django{40,41,main}, black,flake8 [gh-actions] python = - 3.6: py36 3.7: py37 3.8: py38 3.9: py39 @@ -15,11 +13,9 @@ python = [gh-actions:env] DJANGO = - 2.2: django22 - 3.0: django30 - 3.1: django31 3.2: django32 4.0: django40 + 4.1: django41 main: djangomain [testenv] @@ -30,11 +26,9 @@ setenv = deps = -e.[test] psycopg2-binary - django22: Django>=2.2,<3.0 - django30: Django>=3.0,<3.1 - django31: Django>=3.1,<3.2 django32: Django>=3.2,<4.0 django40: Django>=4.0,<4.1 + django41: Django>=4.1,<4.2 djangomain: https://github.com/django/django/archive/main.zip commands = {posargs:py.test --cov=graphene_django graphene_django examples} From 42a40b4df0f9f90e918c2b11985079f52f8de73b Mon Sep 17 00:00:00 2001 From: Thomas Leonard <64223923+tcleonard@users.noreply.github.com> Date: Thu, 22 Sep 2022 10:26:21 +0100 Subject: [PATCH 14/38] chore: update dev dependencies (#1345) Co-authored-by: Thomas Leonard --- Makefile | 17 ++++++++--------- graphene_django/fields.py | 5 ++++- .../filter/filters/global_id_filter.py | 2 +- graphene_django/filter/filterset.py | 10 ++++------ graphene_django/filter/tests/conftest.py | 8 ++++---- .../filter/tests/test_enum_filtering.py | 14 ++++++++++---- graphene_django/filter/tests/test_fields.py | 16 +++++++++++++--- .../filter/tests/test_typed_filter.py | 12 +++--------- graphene_django/filter/utils.py | 4 +++- graphene_django/tests/test_query.py | 16 +++++++++++++--- graphene_django/types.py | 2 +- .../utils/tests/test_str_converters.py | 2 +- setup.py | 14 +++++++------- 13 files changed, 72 insertions(+), 50 deletions(-) diff --git a/Makefile b/Makefile index b850ae8..d8ceaef 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,21 @@ +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}' + .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 +.PHONY: tests ## Run unit tests tests: py.test graphene_django --cov=graphene_django -vv -.PHONY: test -test: tests # Alias test -> tests - -.PHONY: format +.PHONY: format ## Format code format: black --exclude "/migrations/" graphene_django examples setup.py -.PHONY: lint +.PHONY: lint ## Lint code lint: flake8 graphene_django examples diff --git a/graphene_django/fields.py b/graphene_django/fields.py index c881456..f26f851 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -69,7 +69,10 @@ class DjangoListField(Field): _type = _type.of_type django_object_type = _type.of_type.of_type return partial( - self.list_resolver, django_object_type, resolver, self.get_manager(), + self.list_resolver, + django_object_type, + resolver, + self.get_manager(), ) diff --git a/graphene_django/filter/filters/global_id_filter.py b/graphene_django/filter/filters/global_id_filter.py index a612a8a..da16585 100644 --- a/graphene_django/filter/filters/global_id_filter.py +++ b/graphene_django/filter/filters/global_id_filter.py @@ -13,7 +13,7 @@ class GlobalIDFilter(Filter): field_class = GlobalIDFormField def filter(self, qs, value): - """ Convert the filter value to a primary key before filtering """ + """Convert the filter value to a primary key before filtering""" _id = None if value is not None: _, _id = from_global_id(value) diff --git a/graphene_django/filter/filterset.py b/graphene_django/filter/filterset.py index b3333bf..57c35af 100644 --- a/graphene_django/filter/filterset.py +++ b/graphene_django/filter/filterset.py @@ -18,8 +18,8 @@ GRAPHENE_FILTER_SET_OVERRIDES = { class GrapheneFilterSetMixin(BaseFilterSet): - """ A django_filters.filterset.BaseFilterSet with default filter overrides - to handle global IDs """ + """A django_filters.filterset.BaseFilterSet with default filter overrides + to handle global IDs""" FILTER_DEFAULTS = dict( itertools.chain( @@ -29,8 +29,7 @@ class GrapheneFilterSetMixin(BaseFilterSet): def setup_filterset(filterset_class): - """ Wrap a provided filterset in Graphene-specific functionality - """ + """Wrap a provided filterset in Graphene-specific functionality""" return type( "Graphene{}".format(filterset_class.__name__), (filterset_class, GrapheneFilterSetMixin), @@ -39,8 +38,7 @@ def setup_filterset(filterset_class): def custom_filterset_factory(model, filterset_base_class=FilterSet, **meta): - """ Create a filterset for the given model using the provided meta data - """ + """Create a filterset for the given model using the provided meta data""" meta.update({"model": model}) meta_class = type(str("Meta"), (object,), meta) filterset = type( diff --git a/graphene_django/filter/tests/conftest.py b/graphene_django/filter/tests/conftest.py index 57924af..e2bba68 100644 --- a/graphene_django/filter/tests/conftest.py +++ b/graphene_django/filter/tests/conftest.py @@ -89,10 +89,10 @@ def Query(EventType): def resolve_events(self, info, **kwargs): events = [ - Event(name="Live Show", tags=["concert", "music", "rock"],), - Event(name="Musical", tags=["movie", "music"],), - Event(name="Ballet", tags=["concert", "dance"],), - Event(name="Speech", tags=[],), + Event(name="Live Show", tags=["concert", "music", "rock"]), + Event(name="Musical", tags=["movie", "music"]), + Event(name="Ballet", tags=["concert", "dance"]), + Event(name="Speech", tags=[]), ] STORE["events"] = events diff --git a/graphene_django/filter/tests/test_enum_filtering.py b/graphene_django/filter/tests/test_enum_filtering.py index 4fe7ddd..a284d08 100644 --- a/graphene_django/filter/tests/test_enum_filtering.py +++ b/graphene_django/filter/tests/test_enum_filtering.py @@ -54,13 +54,13 @@ def reporter_article_data(): first_name="Jane", last_name="Doe", email="janedoe@example.com", a_choice=2 ) Article.objects.create( - headline="Article Node 1", reporter=john, editor=john, lang="es", + headline="Article Node 1", reporter=john, editor=john, lang="es" ) Article.objects.create( - headline="Article Node 2", reporter=john, editor=john, lang="en", + headline="Article Node 2", reporter=john, editor=john, lang="en" ) Article.objects.create( - headline="Article Node 3", reporter=jane, editor=jane, lang="en", + headline="Article Node 3", reporter=jane, editor=jane, lang="en" ) @@ -80,7 +80,13 @@ def test_filter_enum_on_connection(schema, reporter_article_data): } """ - expected = {"allArticles": {"edges": [{"node": {"headline": "Article Node 1"}},]}} + expected = { + "allArticles": { + "edges": [ + {"node": {"headline": "Article Node 1"}}, + ] + } + } result = schema.execute(query) assert not result.errors diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 370c894..fe4ae87 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -1224,7 +1224,7 @@ def test_filter_filterset_based_on_mixin(): } } - result = schema.execute(query, variable_values={"email": reporter_1.email},) + result = schema.execute(query, variable_values={"email": reporter_1.email}) assert not result.errors assert result.data == expected @@ -1265,13 +1265,23 @@ def test_filter_string_contains(): result = schema.execute(query, variables={"filter": "Ja"}) assert not result.errors assert result.data == { - "people": {"edges": [{"node": {"name": "Jack"}}, {"node": {"name": "Jane"}},]} + "people": { + "edges": [ + {"node": {"name": "Jack"}}, + {"node": {"name": "Jane"}}, + ] + } } result = schema.execute(query, variables={"filter": "o"}) assert not result.errors assert result.data == { - "people": {"edges": [{"node": {"name": "Joe"}}, {"node": {"name": "Bob"}},]} + "people": { + "edges": [ + {"node": {"name": "Joe"}}, + {"node": {"name": "Bob"}}, + ] + } } diff --git a/graphene_django/filter/tests/test_typed_filter.py b/graphene_django/filter/tests/test_typed_filter.py index cc0bafe..a7edc56 100644 --- a/graphene_django/filter/tests/test_typed_filter.py +++ b/graphene_django/filter/tests/test_typed_filter.py @@ -103,15 +103,9 @@ def test_typed_filter_schema(schema): def test_typed_filters_work(schema): reporter = Reporter.objects.create(first_name="John", last_name="Doe", email="") - Article.objects.create( - headline="A", reporter=reporter, editor=reporter, lang="es", - ) - Article.objects.create( - headline="B", reporter=reporter, editor=reporter, lang="es", - ) - Article.objects.create( - headline="C", reporter=reporter, editor=reporter, lang="en", - ) + Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es") + Article.objects.create(headline="B", reporter=reporter, editor=reporter, lang="es") + Article.objects.create(headline="C", reporter=reporter, editor=reporter, lang="en") query = "query { articles (lang_In: [ES]) { edges { node { headline } } } }" diff --git a/graphene_django/filter/utils.py b/graphene_django/filter/utils.py index cd05a87..ebd2a00 100644 --- a/graphene_django/filter/utils.py +++ b/graphene_django/filter/utils.py @@ -94,7 +94,9 @@ def get_filtering_args_from_filterset(filterset_class, type): field_type = graphene.List(field_type) args[name] = graphene.Argument( - field_type, description=filter_field.label, required=required, + field_type, + description=filter_field.label, + required=required, ) return args diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 5cbf90e..f1815d7 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1480,7 +1480,11 @@ def test_connection_should_enable_offset_filtering(): result = schema.execute(query) assert not result.errors expected = { - "allReporters": {"edges": [{"node": {"firstName": "Some", "lastName": "Guy"}},]} + "allReporters": { + "edges": [ + {"node": {"firstName": "Some", "lastName": "Guy"}}, + ] + } } assert result.data == expected @@ -1521,7 +1525,9 @@ def test_connection_should_enable_offset_filtering_higher_than_max_limit( assert not result.errors expected = { "allReporters": { - "edges": [{"node": {"firstName": "Some", "lastName": "Lady"}},] + "edges": [ + {"node": {"firstName": "Some", "lastName": "Lady"}}, + ] } } assert result.data == expected @@ -1590,7 +1596,11 @@ def test_connection_should_allow_offset_filtering_with_after(): result = schema.execute(query, variable_values=dict(after=after)) assert not result.errors expected = { - "allReporters": {"edges": [{"node": {"firstName": "Jane", "lastName": "Roe"}},]} + "allReporters": { + "edges": [ + {"node": {"firstName": "Jane", "lastName": "Roe"}}, + ] + } } assert result.data == expected diff --git a/graphene_django/types.py b/graphene_django/types.py index d272412..c256f1d 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -216,7 +216,7 @@ class DjangoObjectType(ObjectType): "Creating a DjangoObjectType without either the `fields` " "or the `exclude` option is deprecated. Add an explicit `fields " "= '__all__'` option on DjangoObjectType {class_name} to use all " - "fields".format(class_name=cls.__name__,), + "fields".format(class_name=cls.__name__), DeprecationWarning, stacklevel=2, ) diff --git a/graphene_django/utils/tests/test_str_converters.py b/graphene_django/utils/tests/test_str_converters.py index 6460c4e..d3d33c2 100644 --- a/graphene_django/utils/tests/test_str_converters.py +++ b/graphene_django/utils/tests/test_str_converters.py @@ -6,4 +6,4 @@ def test_to_const(): def test_to_const_unicode(): - assert to_const(u"Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF" + assert to_const("Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF" diff --git a/setup.py b/setup.py index 306ec33..d9aefef 100644 --- a/setup.py +++ b/setup.py @@ -14,22 +14,22 @@ rest_framework_require = ["djangorestframework>=3.6.3"] tests_require = [ - "pytest>=3.6.3", + "pytest>=7.1.3", "pytest-cov", "pytest-random-order", "coveralls", "mock", "pytz", - "django-filter>=2", - "pytest-django>=3.3.2", + "django-filter>=22.1", + "pytest-django>=4.5.2", ] + rest_framework_require dev_requires = [ - "black==19.10b0", - "flake8==3.7.9", - "flake8-black==0.1.1", - "flake8-bugbear==20.1.4", + "black==22.8.0", + "flake8==5.0.4", + "flake8-black==0.3.3", + "flake8-bugbear==22.9.11", ] + tests_require setup( From 3473fe025e1efb5de97040087dbe6edf050ef373 Mon Sep 17 00:00:00 2001 From: Thomas Leonard <64223923+tcleonard@users.noreply.github.com> Date: Thu, 22 Sep 2022 16:01:28 +0100 Subject: [PATCH 15/38] fix: backward pagination (#1346) Co-authored-by: Thomas Leonard Co-authored-by: Laurent --- graphene_django/fields.py | 22 ++++++------ graphene_django/tests/test_query.py | 53 ++++++++++++++++++++--------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index f26f851..3c48595 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -152,22 +152,24 @@ class DjangoConnectionField(ConnectionField): array_length = iterable.count() else: array_length = len(iterable) - array_slice_length = ( - min(max_limit, array_length) if max_limit is not None else array_length - ) - # If after is higher than list_length, connection_from_list_slice + # If after is higher than array_length, connection_from_array_slice # would try to do a negative slicing which makes django throw an # AssertionError slice_start = min( - get_offset_with_default(args.get("after"), -1) + 1, array_length + get_offset_with_default(args.get("after"), -1) + 1, + array_length, ) + array_slice_length = array_length - slice_start - if max_limit is not None and args.get("first", None) is None: - if args.get("last", None) is not None: - slice_start = max(array_length - args["last"], 0) - else: - args["first"] = max_limit + # Impose the maximum limit via the `first` field if neither first or last are already provided + # (note that if any of them is provided they must be under max_limit otherwise an error is raised). + if ( + max_limit is not None + and args.get("first", None) is None + and args.get("last", None) is None + ): + args["first"] = max_limit connection = connection_from_array_slice( iterable[slice_start:], diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index f1815d7..207c211 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1243,6 +1243,7 @@ def test_should_have_next_page(graphene_settings): } +@pytest.mark.parametrize("max_limit", [100, 4]) class TestBackwardPagination: def setup_schema(self, graphene_settings, max_limit): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit @@ -1261,8 +1262,8 @@ class TestBackwardPagination: schema = graphene.Schema(query=Query) return schema - def do_queries(self, schema): - # Simply last 3 + def test_query_last(self, graphene_settings, max_limit): + schema = self.setup_schema(graphene_settings, max_limit=max_limit) query_last = """ query { allReporters(last: 3) { @@ -1282,7 +1283,8 @@ class TestBackwardPagination: e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 3", "First 4", "First 5"] - # Use a combination of first and last + def test_query_first_and_last(self, graphene_settings, max_limit): + schema = self.setup_schema(graphene_settings, max_limit=max_limit) query_first_and_last = """ query { allReporters(first: 4, last: 3) { @@ -1302,7 +1304,8 @@ class TestBackwardPagination: e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 1", "First 2", "First 3"] - # Use a combination of first and last and after + def test_query_first_last_and_after(self, graphene_settings, max_limit): + schema = self.setup_schema(graphene_settings, max_limit=max_limit) query_first_last_and_after = """ query queryAfter($after: String) { allReporters(first: 4, last: 3, after: $after) { @@ -1317,7 +1320,8 @@ class TestBackwardPagination: after = base64.b64encode(b"arrayconnection:0").decode() result = schema.execute( - query_first_last_and_after, variable_values=dict(after=after) + query_first_last_and_after, + variable_values=dict(after=after), ) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 3 @@ -1325,20 +1329,35 @@ class TestBackwardPagination: e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 2", "First 3", "First 4"] - def test_should_query(self, graphene_settings): + def test_query_last_and_before(self, graphene_settings, max_limit): + schema = self.setup_schema(graphene_settings, max_limit=max_limit) + query_first_last_and_after = """ + query queryAfter($before: String) { + allReporters(last: 1, before: $before) { + edges { + node { + firstName + } + } + } + } """ - Backward pagination should work as expected - """ - schema = self.setup_schema(graphene_settings, max_limit=100) - self.do_queries(schema) - def test_should_query_with_low_max_limit(self, graphene_settings): - """ - When doing backward pagination (using last) in combination with a max limit higher than the number of objects - we should really retrieve the last ones. - """ - schema = self.setup_schema(graphene_settings, max_limit=4) - self.do_queries(schema) + result = schema.execute( + query_first_last_and_after, + ) + assert not result.errors + assert len(result.data["allReporters"]["edges"]) == 1 + assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 5" + + before = base64.b64encode(b"arrayconnection:5").decode() + result = schema.execute( + query_first_last_and_after, + variable_values=dict(before=before), + ) + assert not result.errors + assert len(result.data["allReporters"]["edges"]) == 1 + assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 4" def test_should_preserve_prefetch_related(django_assert_num_queries): From 37848fa2dfab6feaa2d91d8e41adf7a619b4eb67 Mon Sep 17 00:00:00 2001 From: Thomas Leonard <64223923+tcleonard@users.noreply.github.com> Date: Thu, 22 Sep 2022 19:09:11 +0100 Subject: [PATCH 16/38] fix: convert Django BigIntegerField to BigInt GraphQL type (#1318) Co-authored-by: Thomas Leonard --- graphene_django/converter.py | 7 ++++++- graphene_django/tests/test_converter.py | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index c243e82..90b1466 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -24,6 +24,7 @@ from graphene import ( Decimal, ) from graphene.types.json import JSONString +from graphene.types.scalars import BigInt from graphene.utils.str_converters import to_camel_case from graphql import GraphQLError, assert_valid_name from graphql.pyutils import register_description @@ -186,10 +187,14 @@ def convert_field_to_uuid(field, registry=None): ) +@convert_django_field.register(models.BigIntegerField) +def convert_big_int_field(field, registry=None): + return BigInt(description=field.help_text, required=not field.null) + + @convert_django_field.register(models.PositiveIntegerField) @convert_django_field.register(models.PositiveSmallIntegerField) @convert_django_field.register(models.SmallIntegerField) -@convert_django_field.register(models.BigIntegerField) @convert_django_field.register(models.IntegerField) def convert_field_to_int(field, registry=None): return Int(description=get_django_field_description(field), required=not field.null) diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index afd744f..9158b12 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -10,6 +10,7 @@ from graphene import NonNull from graphene.relay import ConnectionField, Node from graphene.types.datetime import Date, DateTime, Time from graphene.types.json import JSONString +from graphene.types.scalars import BigInt from ..compat import ( ArrayField, @@ -140,8 +141,8 @@ def test_should_small_integer_convert_int(): assert_conversion(models.SmallIntegerField, graphene.Int) -def test_should_big_integer_convert_int(): - assert_conversion(models.BigIntegerField, graphene.Int) +def test_should_big_integer_convert_big_int(): + assert_conversion(models.BigIntegerField, BigInt) def test_should_integer_convert_int(): From a53ded611bbe1519742a5254e4cbfa12af0cc2d9 Mon Sep 17 00:00:00 2001 From: Thomas Leonard <64223923+tcleonard@users.noreply.github.com> Date: Thu, 22 Sep 2022 19:09:29 +0100 Subject: [PATCH 17/38] feat: update name of DjangoFilterConnectionField type input to be consistent with graphene (Issue #1316) (#1317) Co-authored-by: Thomas Leonard --- graphene_django/filter/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index c6dd50e..eeb197e 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -30,7 +30,7 @@ def convert_enum(data): class DjangoFilterConnectionField(DjangoConnectionField): def __init__( self, - type, + type_, fields=None, order_by=None, extra_filter_meta=None, @@ -44,7 +44,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): self._filtering_args = None self._extra_filter_meta = extra_filter_meta self._base_args = None - super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs) + super(DjangoFilterConnectionField, self).__init__(type_, *args, **kwargs) @property def args(self): From 4f315c365d36c54b0803a9a029d534ae3bf03fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yi=C4=9Fit=20Y=2E=20Er?= Date: Thu, 22 Sep 2022 21:10:52 +0300 Subject: [PATCH 18/38] minor fix on schema.py part (#1306) The documentation already suggests importing ObjectType from graphene, graphene.ObjectType is not necessary while defining the Query class. --- docs/tutorial-relay.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index 3de9022..a27e255 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -151,7 +151,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following: interfaces = (relay.Node, ) - class Query(graphene.ObjectType): + class Query(ObjectType): category = relay.Node.Field(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode) From 56892d7f4b2c4d237f726092a5ec5987631569e6 Mon Sep 17 00:00:00 2001 From: andrei-datcu Date: Thu, 22 Sep 2022 12:13:30 -0600 Subject: [PATCH 19/38] Cast translated description for DecimalField (#1255) * Cast translated description for DecimalField https://github.com/graphql-python/graphene-django/pull/976 casts all the description fields to strings to prevent schema printing from failing whenever the description is a lazy translated string. The `DecimalField` however got in after the v3 merge and it currently misses the cast. * Fix row size --- graphene_django/converter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 90b1466..2c4fa19 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -210,7 +210,9 @@ def convert_field_to_boolean(field, registry=None): @convert_django_field.register(models.DecimalField) def convert_field_to_decimal(field, registry=None): - return Decimal(description=field.help_text, required=not field.null) + return Decimal( + description=get_django_field_description(field), required=not field.null + ) @convert_django_field.register(models.FloatField) From b2f83eb277ddaca49ad3320038e6baf381890a3e Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Fri, 23 Sep 2022 11:38:11 +0300 Subject: [PATCH 20/38] Bump version to 3.0.0b8 (#1348) --- 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 999f3de..93a697a 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,7 +1,7 @@ from .fields import DjangoConnectionField, DjangoListField from .types import DjangoObjectType -__version__ = "3.0.0b7" +__version__ = "3.0.0b8" __all__ = [ "__version__", From 5d81ba04f9cc4643f537ba56aac224f5862f0d38 Mon Sep 17 00:00:00 2001 From: Thomas Leonard <64223923+tcleonard@users.noreply.github.com> Date: Fri, 23 Sep 2022 09:45:02 +0100 Subject: [PATCH 21/38] fix: unit test for graphene pr#1412 (#1315) * Issue #1111: foreign key should also call get_queryset method * fix: test for graphene PR https://github.com/graphql-python/graphene/pull/1412 Co-authored-by: Thomas Leonard --- graphene_django/converter.py | 19 +- graphene_django/tests/models.py | 3 + graphene_django/tests/test_get_queryset.py | 355 +++++++++++++++++++++ graphene_django/tests/test_query.py | 70 +++- setup.py | 3 +- 5 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 graphene_django/tests/test_get_queryset.py diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 2c4fa19..338ab6d 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -308,7 +308,24 @@ def convert_field_to_djangomodel(field, registry=None): if not _type: return - return Field( + class CustomField(Field): + def wrap_resolve(self, parent_resolver): + """ + Implements a custom resolver which go through the `get_node` method to insure that + it goes through the `get_queryset` method of the DjangoObjectType. + """ + resolver = super().wrap_resolve(parent_resolver) + + def custom_resolver(root, info, **args): + fk_obj = resolver(root, info, **args) + if fk_obj is None: + return None + else: + return _type.get_node(info, fk_obj.pk) + + return custom_resolver + + return CustomField( _type, description=get_django_field_description(field), required=not field.null, diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index 7b76cd3..c26a6d8 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -13,6 +13,9 @@ class Person(models.Model): class Pet(models.Model): name = models.CharField(max_length=30) age = models.PositiveIntegerField() + owner = models.ForeignKey( + "Person", on_delete=models.CASCADE, null=True, blank=True, related_name="pets" + ) class FilmDetails(models.Model): diff --git a/graphene_django/tests/test_get_queryset.py b/graphene_django/tests/test_get_queryset.py new file mode 100644 index 0000000..b2647c3 --- /dev/null +++ b/graphene_django/tests/test_get_queryset.py @@ -0,0 +1,355 @@ +import pytest + +import graphene +from graphene.relay import Node + +from graphql_relay import to_global_id + +from ..fields import DjangoConnectionField +from ..types import DjangoObjectType + +from .models import Article, Reporter + + +class TestShouldCallGetQuerySetOnForeignKey: + """ + Check that the get_queryset method is called in both forward and reversed direction + of a foreignkey on types. + (see issue #1111) + """ + + @pytest.fixture(autouse=True) + def setup_schema(self): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + + @classmethod + def get_queryset(cls, queryset, info): + if info.context and info.context.get("admin"): + return queryset + raise Exception("Not authorized to access reporters.") + + class ArticleType(DjangoObjectType): + class Meta: + model = Article + + @classmethod + def get_queryset(cls, queryset, info): + return queryset.exclude(headline__startswith="Draft") + + class Query(graphene.ObjectType): + reporter = graphene.Field(ReporterType, id=graphene.ID(required=True)) + article = graphene.Field(ArticleType, id=graphene.ID(required=True)) + + def resolve_reporter(self, info, id): + return ( + ReporterType.get_queryset(Reporter.objects, info) + .filter(id=id) + .last() + ) + + def resolve_article(self, info, id): + return ( + ArticleType.get_queryset(Article.objects, info).filter(id=id).last() + ) + + self.schema = graphene.Schema(query=Query) + + self.reporter = Reporter.objects.create(first_name="Jane", last_name="Doe") + + self.articles = [ + Article.objects.create( + headline="A fantastic article", + reporter=self.reporter, + editor=self.reporter, + ), + Article.objects.create( + headline="Draft: My next best seller", + reporter=self.reporter, + editor=self.reporter, + ), + ] + + def test_get_queryset_called_on_field(self): + # If a user tries to access an article it is fine as long as it's not a draft one + query = """ + query getArticle($id: ID!) { + article(id: $id) { + headline + } + } + """ + # Non-draft + result = self.schema.execute(query, variables={"id": self.articles[0].id}) + assert not result.errors + assert result.data["article"] == { + "headline": "A fantastic article", + } + # Draft + result = self.schema.execute(query, variables={"id": self.articles[1].id}) + assert not result.errors + assert result.data["article"] is None + + # If a non admin user tries to access a reporter they should get our authorization error + query = """ + query getReporter($id: ID!) { + reporter(id: $id) { + firstName + } + } + """ + + result = self.schema.execute(query, variables={"id": self.reporter.id}) + assert len(result.errors) == 1 + assert result.errors[0].message == "Not authorized to access reporters." + + # An admin user should be able to get reporters + query = """ + query getReporter($id: ID!) { + reporter(id: $id) { + firstName + } + } + """ + + result = self.schema.execute( + query, variables={"id": self.reporter.id}, context_value={"admin": True}, + ) + assert not result.errors + assert result.data == {"reporter": {"firstName": "Jane"}} + + def test_get_queryset_called_on_foreignkey(self): + # If a user tries to access a reporter through an article they should get our authorization error + query = """ + query getArticle($id: ID!) { + article(id: $id) { + headline + reporter { + firstName + } + } + } + """ + + result = self.schema.execute(query, variables={"id": self.articles[0].id}) + assert len(result.errors) == 1 + assert result.errors[0].message == "Not authorized to access reporters." + + # An admin user should be able to get reporters through an article + query = """ + query getArticle($id: ID!) { + article(id: $id) { + headline + reporter { + firstName + } + } + } + """ + + result = self.schema.execute( + query, variables={"id": self.articles[0].id}, context_value={"admin": True}, + ) + assert not result.errors + assert result.data["article"] == { + "headline": "A fantastic article", + "reporter": {"firstName": "Jane"}, + } + + # An admin user should not be able to access draft article through a reporter + query = """ + query getReporter($id: ID!) { + reporter(id: $id) { + firstName + articles { + headline + } + } + } + """ + + result = self.schema.execute( + query, variables={"id": self.reporter.id}, context_value={"admin": True}, + ) + assert not result.errors + assert result.data["reporter"] == { + "firstName": "Jane", + "articles": [{"headline": "A fantastic article"}], + } + + +class TestShouldCallGetQuerySetOnForeignKeyNode: + """ + Check that the get_queryset method is called in both forward and reversed direction + of a foreignkey on types using a node interface. + (see issue #1111) + """ + + @pytest.fixture(autouse=True) + def setup_schema(self): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + + @classmethod + def get_queryset(cls, queryset, info): + if info.context and info.context.get("admin"): + return queryset + raise Exception("Not authorized to access reporters.") + + class ArticleType(DjangoObjectType): + class Meta: + model = Article + interfaces = (Node,) + + @classmethod + def get_queryset(cls, queryset, info): + return queryset.exclude(headline__startswith="Draft") + + class Query(graphene.ObjectType): + reporter = Node.Field(ReporterType) + article = Node.Field(ArticleType) + + self.schema = graphene.Schema(query=Query) + + self.reporter = Reporter.objects.create(first_name="Jane", last_name="Doe") + + self.articles = [ + Article.objects.create( + headline="A fantastic article", + reporter=self.reporter, + editor=self.reporter, + ), + Article.objects.create( + headline="Draft: My next best seller", + reporter=self.reporter, + editor=self.reporter, + ), + ] + + def test_get_queryset_called_on_node(self): + # If a user tries to access an article it is fine as long as it's not a draft one + query = """ + query getArticle($id: ID!) { + article(id: $id) { + headline + } + } + """ + # Non-draft + result = self.schema.execute( + query, variables={"id": to_global_id("ArticleType", self.articles[0].id)} + ) + assert not result.errors + assert result.data["article"] == { + "headline": "A fantastic article", + } + # Draft + result = self.schema.execute( + query, variables={"id": to_global_id("ArticleType", self.articles[1].id)} + ) + assert not result.errors + assert result.data["article"] is None + + # If a non admin user tries to access a reporter they should get our authorization error + query = """ + query getReporter($id: ID!) { + reporter(id: $id) { + firstName + } + } + """ + + result = self.schema.execute( + query, variables={"id": to_global_id("ReporterType", self.reporter.id)} + ) + assert len(result.errors) == 1 + assert result.errors[0].message == "Not authorized to access reporters." + + # An admin user should be able to get reporters + query = """ + query getReporter($id: ID!) { + reporter(id: $id) { + firstName + } + } + """ + + result = self.schema.execute( + query, + variables={"id": to_global_id("ReporterType", self.reporter.id)}, + context_value={"admin": True}, + ) + assert not result.errors + assert result.data == {"reporter": {"firstName": "Jane"}} + + def test_get_queryset_called_on_foreignkey(self): + # If a user tries to access a reporter through an article they should get our authorization error + query = """ + query getArticle($id: ID!) { + article(id: $id) { + headline + reporter { + firstName + } + } + } + """ + + result = self.schema.execute( + query, variables={"id": to_global_id("ArticleType", self.articles[0].id)} + ) + assert len(result.errors) == 1 + assert result.errors[0].message == "Not authorized to access reporters." + + # An admin user should be able to get reporters through an article + query = """ + query getArticle($id: ID!) { + article(id: $id) { + headline + reporter { + firstName + } + } + } + """ + + result = self.schema.execute( + query, + variables={"id": to_global_id("ArticleType", self.articles[0].id)}, + context_value={"admin": True}, + ) + assert not result.errors + assert result.data["article"] == { + "headline": "A fantastic article", + "reporter": {"firstName": "Jane"}, + } + + # An admin user should not be able to access draft article through a reporter + query = """ + query getReporter($id: ID!) { + reporter(id: $id) { + firstName + articles { + edges { + node { + headline + } + } + } + } + } + """ + + result = self.schema.execute( + query, + variables={"id": to_global_id("ReporterType", self.reporter.id)}, + context_value={"admin": True}, + ) + assert not result.errors + assert result.data["reporter"] == { + "firstName": "Jane", + "articles": {"edges": [{"node": {"headline": "A fantastic article"}}]}, + } diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 207c211..e6ae64f 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -15,7 +15,7 @@ from ..compat import IntegerRangeField, MissingType from ..fields import DjangoConnectionField from ..types import DjangoObjectType from ..utils import DJANGO_FILTER_INSTALLED -from .models import Article, CNNReporter, Film, FilmDetails, Reporter +from .models import Article, CNNReporter, Film, FilmDetails, Person, Pet, Reporter def test_should_query_only_fields(): @@ -251,8 +251,8 @@ def test_should_node(): def test_should_query_onetoone_fields(): - film = Film(id=1) - film_details = FilmDetails(id=1, film=film) + film = Film.objects.create(id=1) + film_details = FilmDetails.objects.create(id=1, film=film) class FilmNode(DjangoObjectType): class Meta: @@ -1697,3 +1697,67 @@ def test_connection_should_succeed_if_last_higher_than_number_of_objects(): } } assert result.data == expected + + +def test_should_query_nullable_foreign_key(): + class PetType(DjangoObjectType): + class Meta: + model = Pet + + class PersonType(DjangoObjectType): + class Meta: + model = Person + + class Query(graphene.ObjectType): + pet = graphene.Field(PetType, name=graphene.String(required=True)) + person = graphene.Field(PersonType, name=graphene.String(required=True)) + + def resolve_pet(self, info, name): + return Pet.objects.filter(name=name).first() + + def resolve_person(self, info, name): + return Person.objects.filter(name=name).first() + + schema = graphene.Schema(query=Query) + + person = Person.objects.create(name="Jane") + pets = [ + Pet.objects.create(name="Stray dog", age=1), + Pet.objects.create(name="Jane's dog", owner=person, age=1), + ] + + query_pet = """ + query getPet($name: String!) { + pet(name: $name) { + owner { + name + } + } + } + """ + result = schema.execute(query_pet, variables={"name": "Stray dog"}) + assert not result.errors + assert result.data["pet"] == { + "owner": None, + } + + result = schema.execute(query_pet, variables={"name": "Jane's dog"}) + assert not result.errors + assert result.data["pet"] == { + "owner": {"name": "Jane"}, + } + + query_owner = """ + query getOwner($name: String!) { + person(name: $name) { + pets { + name + } + } + } + """ + result = schema.execute(query_owner, variables={"name": "Jane"}) + assert not result.errors + assert result.data["person"] == { + "pets": [{"name": "Jane's dog"}], + } diff --git a/setup.py b/setup.py index d9aefef..3a46d24 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,8 @@ setup( keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests", "examples", "examples.*"]), install_requires=[ - "graphene>=3.0,<4", + # "graphene>=3.0,<4", + "graphene @ git+https://github.com/loft-orbital/graphene.git@loft-v3-1.0#egg=graphene", "graphql-core>=3.1.0,<4", "graphql-relay>=3.1.1,<4", "Django>=3.2", From 0f40da7b317e474bbb0d777e4e96599f3b989903 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Fri, 23 Sep 2022 13:47:10 +0500 Subject: [PATCH 22/38] Make errors in form mutation non nullable (#1286) --- graphene_django/forms/mutation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 5a3d8e7..13e9863 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -117,7 +117,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): class Meta: abstract = True - errors = graphene.List(ErrorType) + errors = graphene.List(graphene.NonNull(ErrorType), required=True) @classmethod def __init_subclass_with_meta__( From 541caa117eedf74923c2ed396afeee5fd568850e Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Sat, 24 Sep 2022 15:50:40 +0300 Subject: [PATCH 23/38] Fixes related to pr#1412 (#1352) * fix: setup.py graphene dependency * fix: graphene_django/tests/test_get_queryset.py format Co-authored-by: Firas Kafri --- graphene_django/tests/test_get_queryset.py | 12 +++++++++--- setup.py | 3 +-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/graphene_django/tests/test_get_queryset.py b/graphene_django/tests/test_get_queryset.py index b2647c3..91bdc70 100644 --- a/graphene_django/tests/test_get_queryset.py +++ b/graphene_django/tests/test_get_queryset.py @@ -114,7 +114,9 @@ class TestShouldCallGetQuerySetOnForeignKey: """ result = self.schema.execute( - query, variables={"id": self.reporter.id}, context_value={"admin": True}, + query, + variables={"id": self.reporter.id}, + context_value={"admin": True}, ) assert not result.errors assert result.data == {"reporter": {"firstName": "Jane"}} @@ -149,7 +151,9 @@ class TestShouldCallGetQuerySetOnForeignKey: """ result = self.schema.execute( - query, variables={"id": self.articles[0].id}, context_value={"admin": True}, + query, + variables={"id": self.articles[0].id}, + context_value={"admin": True}, ) assert not result.errors assert result.data["article"] == { @@ -170,7 +174,9 @@ class TestShouldCallGetQuerySetOnForeignKey: """ result = self.schema.execute( - query, variables={"id": self.reporter.id}, context_value={"admin": True}, + query, + variables={"id": self.reporter.id}, + context_value={"admin": True}, ) assert not result.errors assert result.data["reporter"] == { diff --git a/setup.py b/setup.py index 3a46d24..d9aefef 100644 --- a/setup.py +++ b/setup.py @@ -59,8 +59,7 @@ setup( keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests", "examples", "examples.*"]), install_requires=[ - # "graphene>=3.0,<4", - "graphene @ git+https://github.com/loft-orbital/graphene.git@loft-v3-1.0#egg=graphene", + "graphene>=3.0,<4", "graphql-core>=3.1.0,<4", "graphql-relay>=3.1.1,<4", "Django>=3.2", From 05d3df92e7be6e1c547f2cb1b3b55ff13339f713 Mon Sep 17 00:00:00 2001 From: Craig <41215134+c-py@users.noreply.github.com> Date: Sat, 24 Sep 2022 22:59:53 +1000 Subject: [PATCH 24/38] Delay assignment of csrftoken (#1289) --- .../static/graphene_django/graphiql.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js index ac010e8..f457f65 100644 --- a/graphene_django/static/graphene_django/graphiql.js +++ b/graphene_django/static/graphene_django/graphiql.js @@ -10,14 +10,6 @@ history, location, ) { - // Parse the cookie value for a CSRF token - var csrftoken; - var cookies = ("; " + document.cookie).split("; csrftoken="); - if (cookies.length == 2) { - csrftoken = cookies.pop().split(";").shift(); - } else { - csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value; - } // Collect the URL parameters var parameters = {}; @@ -68,9 +60,19 @@ var headers = opts.headers || {}; headers['Accept'] = headers['Accept'] || 'application/json'; headers['Content-Type'] = headers['Content-Type'] || 'application/json'; + + // Parse the cookie value for a CSRF token + var csrftoken; + var cookies = ("; " + document.cookie).split("; csrftoken="); + if (cookies.length == 2) { + csrftoken = cookies.pop().split(";").shift(); + } else { + csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value; + } if (csrftoken) { headers['X-CSRFToken'] = csrftoken } + return fetch(fetchURL, { method: "post", headers: headers, From 60b30320146313f25d072394252990f2e8115cde Mon Sep 17 00:00:00 2001 From: belkka Date: Sat, 24 Sep 2022 16:00:12 +0300 Subject: [PATCH 25/38] Fix type hint for DjangoObjectTypeOptions.model (#1269) Proper type is `typing.Type[Model]`, not `Model` --- 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 c256f1d..0ebb7d3 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -122,7 +122,7 @@ def validate_fields(type_, model, fields, only_fields, exclude_fields): class DjangoObjectTypeOptions(ObjectTypeOptions): - model = None # type: Model + model = None # type: Type[Model] registry = None # type: Registry connection = None # type: Type[Connection] From 97442f9ceeb1061d24e022bd7044a63d4230d53b Mon Sep 17 00:00:00 2001 From: belkka Date: Sat, 24 Sep 2022 16:00:22 +0300 Subject: [PATCH 26/38] Fix code examples in queries.rst (#1265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix code examples in queries.rst Code example in Arguments section doesn't work as stated in its comment — if "foo" or "bar" are not declare in the graphql query, it will be an error, not they become None. Code example in Info section has invalid indentation, `resolve_questions()` seems to be a `Query` method, but it's indented as module-level function. * Fix indentation in query examples * Enable syntax highlight for graphql queries --- docs/queries.rst | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/queries.rst b/docs/queries.rst index 1e1ba82..8b85d45 100644 --- a/docs/queries.rst +++ b/docs/queries.rst @@ -151,7 +151,7 @@ For example the following ``Model`` and ``DjangoObjectType``: Results in the following GraphQL schema definition: -.. code:: +.. code:: graphql type Pet { id: ID! @@ -178,7 +178,7 @@ You can disable this automatic conversion by setting fields = ("id", "kind",) convert_choices_to_enum = False -.. code:: +.. code:: graphql type Pet { id: ID! @@ -313,7 +313,7 @@ Additionally, Resolvers will receive **any arguments declared in the field defin bar=graphene.Int() ) - def resolve_question(root, info, foo, bar): + def resolve_question(root, info, foo=None, bar=None): # If `foo` or `bar` are declared in the GraphQL query they will be here, else None. return Question.objects.filter(foo=foo, bar=bar).first() @@ -336,12 +336,12 @@ of Django's ``HTTPRequest`` in your resolve methods, such as checking for authen class Query(graphene.ObjectType): questions = graphene.List(QuestionType) - def resolve_questions(root, info): - # See if a user is authenticated - if info.context.user.is_authenticated(): - return Question.objects.all() - else: - return Question.objects.none() + def resolve_questions(root, info): + # See if a user is authenticated + if info.context.user.is_authenticated(): + return Question.objects.all() + else: + return Question.objects.none() DjangoObjectTypes @@ -418,29 +418,29 @@ the core graphene pages for more information on customizing the Relay experience You can now execute queries like: -.. code:: python +.. code:: graphql { questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") { pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage + startCursor + endCursor + hasNextPage + hasPreviousPage } edges { - cursor - node { - id - question_text - } + cursor + node { + id + question_text + } } } } Which returns: -.. code:: python +.. code:: json { "data": { From 0b2cc4ecb2c8ad4a12c21ab1384d2ce0c89295cb Mon Sep 17 00:00:00 2001 From: Forest Anderson Date: Sat, 24 Sep 2022 09:00:45 -0400 Subject: [PATCH 27/38] Fixed deprecation warning (#1313) --- graphene_django/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 3c48595..05a7010 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -2,7 +2,7 @@ from functools import partial from django.db.models.query import QuerySet -from graphql_relay.connection.array_connection import ( +from graphql_relay import ( connection_from_array_slice, cursor_to_offset, get_offset_with_default, From 9a60589732411b1b11a190114bc2a412c02d5fe2 Mon Sep 17 00:00:00 2001 From: Gabriel Lacroix Date: Sat, 24 Sep 2022 09:02:33 -0400 Subject: [PATCH 28/38] Make instructions runnable without tweaking (#1224) Introduces two changes to make sure the instructions in the tutorial don't require debugging: - Add `cd ..` when first syncing the database so that `manage.py` is accessible in the working directory. - Change `cookbook.ingredients.apps.IngredientsConfig.name` to `cookbook.ingredients` from `ingredients` to prevent the following exception: ```python django.core.exceptions.ImproperlyConfigured: Cannot import 'ingredients'. Check that 'cookbook.ingredients.apps.IngredientsConfig.name' is correct. ``` --- docs/tutorial-plain.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 45927a5..43b6da9 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -35,6 +35,7 @@ Now sync your database for the first time: .. code:: bash + cd .. python manage.py migrate Let's create a few simple models... @@ -77,6 +78,18 @@ Add ingredients as INSTALLED_APPS: "cookbook.ingredients", ] +Make sure the app name in ``cookbook.ingredients.apps.IngredientsConfig`` is set to ``cookbook.ingredients``. + +.. code:: python + + # cookbook/ingredients/apps.py + + from django.apps import AppConfig + + + class IngredientsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'cookbook.ingredients' Don't forget to create & run migrations: From 07940aa5f537bb5e38af3bd38bc1b770bf678cd1 Mon Sep 17 00:00:00 2001 From: Alan Rivas Date: Sat, 24 Sep 2022 10:03:45 -0300 Subject: [PATCH 29/38] Update tutorial-relay.rst (#1220) From a78114ada346fb2c639ea88fe81d0407cf5ed448 Mon Sep 17 00:00:00 2001 From: Suyeol Jeon Date: Sat, 24 Sep 2022 23:41:14 +0900 Subject: [PATCH 30/38] Add support to persist GraphQL headers in GraphiQL (#1209) --- docs/settings.rst | 19 +++++++++++++++++++ graphene_django/settings.py | 1 + .../static/graphene_django/graphiql.js | 1 + .../templates/graphene/graphiql.html | 1 + graphene_django/views.py | 1 + 5 files changed, 23 insertions(+) diff --git a/docs/settings.rst b/docs/settings.rst index ff1c05e..1984a15 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -207,3 +207,22 @@ Default: ``True`` GRAPHENE = { 'GRAPHIQL_HEADER_EDITOR_ENABLED': True, } + + +``GRAPHIQL_SHOULD_PERSIST_HEADERS`` +--------------------- + +Set to ``True`` if you want to persist GraphiQL headers after refreshing the page. + +This setting is passed to ``shouldPersistHeaders`` GraphiQL options, for details refer to GraphiQLDocs_. + +.. _GraphiQLDocs: https://github.com/graphql/graphiql/tree/main/packages/graphiql#options + + +Default: ``False`` + +.. code:: python + + GRAPHENE = { + 'GRAPHIQL_SHOULD_PERSIST_HEADERS': False, + } diff --git a/graphene_django/settings.py b/graphene_django/settings.py index 467c6a3..0fd70a7 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -41,6 +41,7 @@ DEFAULTS = { # This sets headerEditorEnabled GraphiQL option, for details go to # https://github.com/graphql/graphiql/tree/main/packages/graphiql#options "GRAPHIQL_HEADER_EDITOR_ENABLED": True, + "GRAPHIQL_SHOULD_PERSIST_HEADERS": False, "ATOMIC_MUTATIONS": False, } diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js index f457f65..f6be32c 100644 --- a/graphene_django/static/graphene_django/graphiql.js +++ b/graphene_django/static/graphene_django/graphiql.js @@ -178,6 +178,7 @@ onEditVariables: onEditVariables, onEditOperationName: onEditOperationName, headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, + shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders, query: parameters.query, }; if (parameters.variables) { diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index cec4893..3685692 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -46,6 +46,7 @@ add "&raw" to the end of the URL within a browser. subscriptionPath: "{{subscription_path}}", {% endif %} graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }}, + graphiqlShouldPersistHeaders: {{ graphiql_should_persist_headers|yesno:"true,false" }}, }; diff --git a/graphene_django/views.py b/graphene_django/views.py index f533f70..bf333a9 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -162,6 +162,7 @@ class GraphQLView(View): subscription_path=self.subscription_path, # GraphiQL headers tab, graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED, + graphiql_should_persist_headers=graphene_settings.GRAPHIQL_SHOULD_PERSIST_HEADERS, ) if self.batch: From c697e5c8c1a805b09239c0e282c6b44c31f6cac1 Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Mon, 26 Sep 2022 01:27:22 +0300 Subject: [PATCH 31/38] Bump version to v3.0.0b9 (#1353) --- 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 93a697a..a02b156 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,7 +1,7 @@ from .fields import DjangoConnectionField, DjangoListField from .types import DjangoObjectType -__version__ = "3.0.0b8" +__version__ = "3.0.0b9" __all__ = [ "__version__", From bb03306075f0a9b40ecf21b0f5aaf02f876d5bb6 Mon Sep 17 00:00:00 2001 From: Syberen van Munster <34777982+syberen@users.noreply.github.com> Date: Mon, 26 Sep 2022 00:56:22 +0200 Subject: [PATCH 32/38] Move testing endpoint to settings (#1105) * Import testing endpoint from graphene settings * Add documentation for TESTING_ENDPOINT setting * Remove empty lines * Run formatter Co-authored-by: Firas K <3097061+firaskafri@users.noreply.github.com> --- docs/settings.rst | 16 +++++++++++++++- docs/testing.rst | 3 ++- graphene_django/settings.py | 1 + graphene_django/utils/testing.py | 6 ++++-- graphene_django/utils/tests/test_testing.py | 9 +++++++++ 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 1984a15..5bffd08 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -189,7 +189,7 @@ Default: ``None`` ``GRAPHIQL_HEADER_EDITOR_ENABLED`` ---------------------- +---------------------------------- GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables. @@ -209,6 +209,20 @@ Default: ``True`` } +``TESTING_ENDPOINT`` +-------------------- + +Define the graphql endpoint url used for the `GraphQLTestCase` class. + +Default: ``/graphql`` + +.. code:: python + + GRAPHENE = { + 'TESTING_ENDPOINT': '/customEndpoint' + } + + ``GRAPHIQL_SHOULD_PERSIST_HEADERS`` --------------------- diff --git a/docs/testing.rst b/docs/testing.rst index fb0a85d..1b32352 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -6,7 +6,8 @@ Using unittest If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`. -Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLTestCase`. The default endpoint is `GRAPHQL_URL = "/graphql/"`. +The default endpoint for testing is `/graphql`. You can override this in the `settings `__. + Usage: diff --git a/graphene_django/settings.py b/graphene_django/settings.py index 0fd70a7..6f62326 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -43,6 +43,7 @@ DEFAULTS = { "GRAPHIQL_HEADER_EDITOR_ENABLED": True, "GRAPHIQL_SHOULD_PERSIST_HEADERS": False, "ATOMIC_MUTATIONS": False, + "TESTING_ENDPOINT": "/graphql", } if settings.DEBUG: diff --git a/graphene_django/utils/testing.py b/graphene_django/utils/testing.py index f94c385..ca0d185 100644 --- a/graphene_django/utils/testing.py +++ b/graphene_django/utils/testing.py @@ -3,6 +3,8 @@ import warnings from django.test import Client, TestCase, TransactionTestCase +from graphene_django.settings import graphene_settings + DEFAULT_GRAPHQL_URL = "/graphql" @@ -40,7 +42,7 @@ def graphql_query( if client is None: client = Client() if not graphql_url: - graphql_url = DEFAULT_GRAPHQL_URL + graphql_url = graphene_settings.TESTING_ENDPOINT body = {"query": query} if operation_name: @@ -69,7 +71,7 @@ class GraphQLTestMixin(object): """ # URL to graphql endpoint - GRAPHQL_URL = DEFAULT_GRAPHQL_URL + GRAPHQL_URL = graphene_settings.TESTING_ENDPOINT def query( self, query, operation_name=None, input_data=None, variables=None, headers=None diff --git a/graphene_django/utils/tests/test_testing.py b/graphene_django/utils/tests/test_testing.py index 2ef78f9..de56158 100644 --- a/graphene_django/utils/tests/test_testing.py +++ b/graphene_django/utils/tests/test_testing.py @@ -2,6 +2,7 @@ import pytest from .. import GraphQLTestCase from ...tests.test_types import with_local_registry +from ...settings import graphene_settings from django.test import Client @@ -43,3 +44,11 @@ def test_graphql_test_case_deprecated_client_setter(): with pytest.warns(PendingDeprecationWarning): tc._client = Client() + + +def test_graphql_test_case_imports_endpoint(): + """ + GraphQLTestCase class should import the default endpoint from settings file + """ + + assert GraphQLTestCase.GRAPHQL_URL == graphene_settings.TESTING_ENDPOINT From ed7c995d8c13c4b64f2064776d127d6d4302c17b Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:08:32 +0300 Subject: [PATCH 33/38] =?UTF-8?q?=E2=98=82=EF=B8=8F=20v3.0.0=20=E2=98=82?= =?UTF-8?q?=EF=B8=8F=20(#1355)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 a02b156..7a413fc 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,7 +1,7 @@ from .fields import DjangoConnectionField, DjangoListField from .types import DjangoObjectType -__version__ = "3.0.0b9" +__version__ = "3.0.0" __all__ = [ "__version__", From f24cbd5148493f4bd0683801c89743ce57b19beb Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 17 Oct 2022 16:57:24 +0200 Subject: [PATCH 34/38] Fix custom foreignkey resolvers (#1361) * Fix custom foreignkey resolvers * Fixed assert name conversion * Fix lint --- graphene_django/converter.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 338ab6d..d9345d8 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -26,7 +26,13 @@ from graphene import ( from graphene.types.json import JSONString from graphene.types.scalars import BigInt from graphene.utils.str_converters import to_camel_case -from graphql import GraphQLError, assert_valid_name +from graphql import GraphQLError + +try: + from graphql import assert_name +except ImportError: + # Support for older versions of graphql + from graphql import assert_valid_name as assert_name from graphql.pyutils import register_description from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField @@ -56,7 +62,7 @@ class BlankValueField(Field): def convert_choice_name(name): name = to_const(force_str(name)) try: - assert_valid_name(name) + assert_name(name) except GraphQLError: name = "A_%s" % name return name @@ -311,17 +317,19 @@ def convert_field_to_djangomodel(field, registry=None): class CustomField(Field): def wrap_resolve(self, parent_resolver): """ - Implements a custom resolver which go through the `get_node` method to insure that + Implements a custom resolver which go through the `get_node` method to ensure that it goes through the `get_queryset` method of the DjangoObjectType. """ resolver = super().wrap_resolve(parent_resolver) def custom_resolver(root, info, **args): fk_obj = resolver(root, info, **args) - if fk_obj is None: - return None - else: - return _type.get_node(info, fk_obj.pk) + if not isinstance(fk_obj, model): + # In case the resolver is a custom one that overwrites + # the default Django resolver + # This happens, for example, when using custom awaitable resolvers. + return fk_obj + return _type.get_node(info, fk_obj.pk) return custom_resolver From 4517e3222499a1aca7c1110204ce5b27d9c911be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolai=20R=C3=B8ed=20Kristiansen?= Date: Wed, 19 Oct 2022 16:10:30 +0200 Subject: [PATCH 35/38] =?UTF-8?q?=F0=9F=91=B7=20Add=20pre-commit=20(#1336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 Add pre-commit config Similar to graphene and graphene-sqlalchemy * ⬆ Bump black * 👷 Lint on CI * ⬆ Bump flake8-black * 🔧 Keep excluding migrations * ⬆ Bump flake8 * 🔧 Remove black and flake8 from tox config * ⬆ Update pre-commit versions * Upgrade syntax to python 3.7+ * Format with pre-commit dedent docs/schema.py to allow formatting * Fix tests on python 3.7 --- .github/ISSUE_TEMPLATE/bug_report.md | 6 +- .github/workflows/lint.yml | 4 +- .pre-commit-config.yaml | 30 ++ CONTRIBUTING.md | 2 +- MANIFEST.in | 2 +- Makefile | 2 +- docs/schema.py | 79 +++-- docs/tutorial-plain.rst | 4 +- .../ingredients/fixtures/ingredients.json | 53 ++- .../ingredients/migrations/0001_initial.py | 44 ++- .../migrations/0002_auto_20161104_0050.py | 8 +- .../migrations/0003_auto_20181018_1746.py | 6 +- .../cookbook/ingredients/schema.py | 2 +- .../recipes/migrations/0001_initial.py | 60 +++- .../migrations/0002_auto_20161104_0106.py | 24 +- .../migrations/0003_auto_20181018_1728.py | 16 +- .../cookbook-plain/cookbook/recipes/schema.py | 2 +- .../ingredients/fixtures/ingredients.json | 53 ++- .../ingredients/migrations/0001_initial.py | 44 ++- .../migrations/0002_auto_20161104_0050.py | 8 +- .../cookbook/cookbook/ingredients/schema.py | 2 +- .../recipes/migrations/0001_initial.py | 60 +++- .../migrations/0002_auto_20161104_0106.py | 24 +- examples/cookbook/cookbook/recipes/schema.py | 2 +- examples/cookbook/dummy_data.json | 303 +++++++++++++++++- examples/starwars/models.py | 2 - graphene_django/compat.py | 2 +- graphene_django/converter.py | 11 +- graphene_django/debug/middleware.py | 4 +- graphene_django/debug/sql/tracking.py | 7 +- graphene_django/debug/tests/test_query.py | 2 +- graphene_django/fields.py | 6 +- graphene_django/filter/fields.py | 6 +- .../filter/filters/array_filter.py | 2 +- .../filter/filters/global_id_filter.py | 4 +- graphene_django/filter/filters/list_filter.py | 2 +- .../filter/filters/typed_filter.py | 2 +- graphene_django/filter/filterset.py | 4 +- graphene_django/filter/tests/conftest.py | 2 +- graphene_django/filter/tests/test_fields.py | 6 +- .../filter/tests/test_in_filter.py | 22 +- .../filter/tests/test_typed_filter.py | 2 +- graphene_django/forms/mutation.py | 8 +- .../management/commands/graphql_schema.py | 8 +- graphene_django/registry.py | 2 +- graphene_django/rest_framework/mutation.py | 2 +- .../rest_framework/serializer_converter.py | 2 +- graphene_django/settings.py | 5 +- graphene_django/tests/models.py | 10 +- graphene_django/tests/test_command.py | 2 +- graphene_django/tests/test_query.py | 6 +- graphene_django/tests/test_types.py | 6 +- graphene_django/tests/test_utils.py | 10 +- graphene_django/tests/test_views.py | 4 +- graphene_django/types.py | 10 +- graphene_django/utils/testing.py | 2 +- graphene_django/views.py | 2 +- setup.cfg | 2 +- tox.ini | 16 +- 59 files changed, 779 insertions(+), 244 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2c933d7..26d84aa 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -27,8 +27,8 @@ a github repo, https://repl.it or similar (you can use this template as a starti * **Please tell us about your environment:** - - - Version: - - Platform: + + - Version: + - Platform: * **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 559326c..9f1c3ab 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: run: | python -m pip install --upgrade pip pip install tox - - name: Run lint 💅 + - name: Run pre-commit 💅 run: tox env: - TOXENV: flake8 + TOXENV: pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..829cb81 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +default_language_version: + python: python3.9 +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-merge-conflict + - id: check-json + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + exclude: ^docs/.*$ + - id: pretty-format-json + args: + - --autofix + - id: trailing-whitespace + exclude: README.md +- repo: https://github.com/asottile/pyupgrade + rev: v2.37.3 + hooks: + - id: pyupgrade + args: [--py37-plus] +- repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black +- repo: https://github.com/PyCQA/flake8 + rev: 5.0.4 + hooks: + - id: flake8 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f9428e9..6a226ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,4 +59,4 @@ Then to produce a HTML version of the documentation: ```sh make html -``` \ No newline at end of file +``` diff --git a/MANIFEST.in b/MANIFEST.in index 045af08..1ede730 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,4 @@ recursive-include graphene_django/templates * recursive-include graphene_django/static * include examples/cookbook/cookbook/ingredients/fixtures/ingredients.json -include examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json \ No newline at end of file +include examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json diff --git a/Makefile b/Makefile index d8ceaef..391c454 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ tests: .PHONY: format ## Format code format: - black --exclude "/migrations/" graphene_django examples setup.py + black graphene_django examples setup.py .PHONY: lint ## Lint code lint: diff --git a/docs/schema.py b/docs/schema.py index 1de92ed..058a587 100644 --- a/docs/schema.py +++ b/docs/schema.py @@ -1,60 +1,57 @@ - import graphene +import graphene - from graphene_django.types import DjangoObjectType +from graphene_django.types import DjangoObjectType - from cookbook.ingredients.models import Category, Ingredient +from cookbook.ingredients.models import Category, Ingredient - class CategoryType(DjangoObjectType): - class Meta: - model = Category - fields = '__all__' +class CategoryType(DjangoObjectType): + class Meta: + model = Category + fields = "__all__" - class IngredientType(DjangoObjectType): - class Meta: - model = Ingredient - fields = '__all__' +class IngredientType(DjangoObjectType): + class Meta: + model = Ingredient + fields = "__all__" - class Query(object): - category = graphene.Field(CategoryType, - id=graphene.Int(), - name=graphene.String()) - all_categories = graphene.List(CategoryType) +class Query: + 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() + ) + all_ingredients = graphene.List(IngredientType) - ingredient = graphene.Field(IngredientType, - id=graphene.Int(), - name=graphene.String()) - all_ingredients = graphene.List(IngredientType) + def resolve_all_categories(self, info, **kwargs): + return Category.objects.all() - def resolve_all_categories(self, info, **kwargs): - return Category.objects.all() + def resolve_all_ingredients(self, info, **kwargs): + return Ingredient.objects.all() - def resolve_all_ingredients(self, info, **kwargs): - return Ingredient.objects.all() + def resolve_category(self, info, **kwargs): + id = kwargs.get("id") + name = kwargs.get("name") - def resolve_category(self, info, **kwargs): - id = kwargs.get('id') - name = kwargs.get('name') + if id is not None: + return Category.objects.get(pk=id) - if id is not None: - return Category.objects.get(pk=id) + if name is not None: + return Category.objects.get(name=name) - if name is not None: - return Category.objects.get(name=name) + return None - return None + def resolve_ingredient(self, info, **kwargs): + id = kwargs.get("id") + name = kwargs.get("name") - def resolve_ingredient(self, info, **kwargs): - id = kwargs.get('id') - name = kwargs.get('name') + if id is not None: + return Ingredient.objects.get(pk=id) - if id is not None: - return Ingredient.objects.get(pk=id) + if name is not None: + return Ingredient.objects.get(name=name) - if name is not None: - return Ingredient.objects.get(name=name) - - return None \ No newline at end of file + return None diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 43b6da9..9073c82 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -85,8 +85,8 @@ Make sure the app name in ``cookbook.ingredients.apps.IngredientsConfig`` is set # cookbook/ingredients/apps.py from django.apps import AppConfig - - + + class IngredientsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'cookbook.ingredients' diff --git a/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json b/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json index 8625d3c..213bd8b 100644 --- a/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json +++ b/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json @@ -1 +1,52 @@ -[{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}] \ No newline at end of file +[ + { + "fields": { + "name": "Dairy" + }, + "model": "ingredients.category", + "pk": 1 + }, + { + "fields": { + "name": "Meat" + }, + "model": "ingredients.category", + "pk": 2 + }, + { + "fields": { + "category": 1, + "name": "Eggs", + "notes": "Good old eggs" + }, + "model": "ingredients.ingredient", + "pk": 1 + }, + { + "fields": { + "category": 1, + "name": "Milk", + "notes": "Comes from a cow" + }, + "model": "ingredients.ingredient", + "pk": 2 + }, + { + "fields": { + "category": 2, + "name": "Beef", + "notes": "Much like milk, this comes from a cow" + }, + "model": "ingredients.ingredient", + "pk": 3 + }, + { + "fields": { + "category": 2, + "name": "Chicken", + "notes": "Definitely doesn't come from a cow" + }, + "model": "ingredients.ingredient", + "pk": 4 + } +] diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py index 0494923..345cadb 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9 on 2015-12-04 18:15 -from __future__ import unicode_literals import django.db.models.deletion from django.db import migrations, models @@ -10,24 +8,46 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Category', + name="Category", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), ], ), migrations.CreateModel( - name='Ingredient', + name="Ingredient", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('notes', models.TextField()), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("notes", models.TextField()), + ( + "category", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ingredients", + to="ingredients.Category", + ), + ), ], ), ] diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py index 359d4fc..00fe255 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9 on 2016-11-04 00:50 -from __future__ import unicode_literals from django.db import migrations, models @@ -8,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('ingredients', '0001_initial'), + ("ingredients", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='ingredient', - name='notes', + model_name="ingredient", + name="notes", field=models.TextField(blank=True, null=True), ), ] diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py index 184e79e..8015d1f 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py @@ -6,12 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('ingredients', '0002_auto_20161104_0050'), + ("ingredients", "0002_auto_20161104_0050"), ] operations = [ migrations.AlterModelOptions( - name='category', - options={'verbose_name_plural': 'Categories'}, + name="category", + options={"verbose_name_plural": "Categories"}, ), ] diff --git a/examples/cookbook-plain/cookbook/ingredients/schema.py b/examples/cookbook-plain/cookbook/ingredients/schema.py index 24a5e95..b8de8f9 100644 --- a/examples/cookbook-plain/cookbook/ingredients/schema.py +++ b/examples/cookbook-plain/cookbook/ingredients/schema.py @@ -16,7 +16,7 @@ class IngredientType(DjangoObjectType): fields = "__all__" -class Query(object): +class Query: category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) all_categories = graphene.List(CategoryType) diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py index 338c71a..fceeb9b 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9 on 2015-12-04 18:20 -from __future__ import unicode_literals import django.db.models.deletion from django.db import migrations, models @@ -11,26 +9,62 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('ingredients', '0001_initial'), + ("ingredients", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Recipe', + name="Recipe", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100)), - ('instructions', models.TextField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100)), + ("instructions", models.TextField()), ], ), migrations.CreateModel( - name='RecipeIngredient', + name="RecipeIngredient", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('amount', models.FloatField()), - ('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)), - ('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')), - ('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("amount", models.FloatField()), + ( + "unit", + models.CharField( + choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")], + max_length=20, + ), + ), + ( + "ingredient", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="used_by", + to="ingredients.Ingredient", + ), + ), + ( + "recipes", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="amounts", + to="recipes.Recipe", + ), + ), ], ), ] diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py b/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py index f135392..0156920 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9 on 2016-11-04 01:06 -from __future__ import unicode_literals from django.db import migrations, models @@ -8,18 +6,26 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('recipes', '0001_initial'), + ("recipes", "0001_initial"), ] operations = [ migrations.RenameField( - model_name='recipeingredient', - old_name='recipes', - new_name='recipe', + model_name="recipeingredient", + old_name="recipes", + new_name="recipe", ), migrations.AlterField( - model_name='recipeingredient', - name='unit', - field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20), + model_name="recipeingredient", + name="unit", + field=models.CharField( + choices=[ + (b"unit", b"Units"), + (b"kg", b"Kilograms"), + (b"l", b"Litres"), + (b"st", b"Shots"), + ], + max_length=20, + ), ), ] diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py index 7a8df49..c54855b 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py @@ -6,13 +6,21 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('recipes', '0002_auto_20161104_0106'), + ("recipes", "0002_auto_20161104_0106"), ] operations = [ migrations.AlterField( - model_name='recipeingredient', - name='unit', - field=models.CharField(choices=[('unit', 'Units'), ('kg', 'Kilograms'), ('l', 'Litres'), ('st', 'Shots')], max_length=20), + model_name="recipeingredient", + name="unit", + field=models.CharField( + choices=[ + ("unit", "Units"), + ("kg", "Kilograms"), + ("l", "Litres"), + ("st", "Shots"), + ], + max_length=20, + ), ), ] diff --git a/examples/cookbook-plain/cookbook/recipes/schema.py b/examples/cookbook-plain/cookbook/recipes/schema.py index aa7fd2d..7f40d51 100644 --- a/examples/cookbook-plain/cookbook/recipes/schema.py +++ b/examples/cookbook-plain/cookbook/recipes/schema.py @@ -16,7 +16,7 @@ class RecipeIngredientType(DjangoObjectType): fields = "__all__" -class Query(object): +class Query: recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String()) all_recipes = graphene.List(RecipeType) diff --git a/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json b/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json index 8625d3c..213bd8b 100644 --- a/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json +++ b/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json @@ -1 +1,52 @@ -[{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}] \ No newline at end of file +[ + { + "fields": { + "name": "Dairy" + }, + "model": "ingredients.category", + "pk": 1 + }, + { + "fields": { + "name": "Meat" + }, + "model": "ingredients.category", + "pk": 2 + }, + { + "fields": { + "category": 1, + "name": "Eggs", + "notes": "Good old eggs" + }, + "model": "ingredients.ingredient", + "pk": 1 + }, + { + "fields": { + "category": 1, + "name": "Milk", + "notes": "Comes from a cow" + }, + "model": "ingredients.ingredient", + "pk": 2 + }, + { + "fields": { + "category": 2, + "name": "Beef", + "notes": "Much like milk, this comes from a cow" + }, + "model": "ingredients.ingredient", + "pk": 3 + }, + { + "fields": { + "category": 2, + "name": "Chicken", + "notes": "Definitely doesn't come from a cow" + }, + "model": "ingredients.ingredient", + "pk": 4 + } +] diff --git a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py index 0494923..345cadb 100644 --- a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py +++ b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9 on 2015-12-04 18:15 -from __future__ import unicode_literals import django.db.models.deletion from django.db import migrations, models @@ -10,24 +8,46 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Category', + name="Category", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), ], ), migrations.CreateModel( - name='Ingredient', + name="Ingredient", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('notes', models.TextField()), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("notes", models.TextField()), + ( + "category", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ingredients", + to="ingredients.Category", + ), + ), ], ), ] diff --git a/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py b/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py index 359d4fc..00fe255 100644 --- a/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py +++ b/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9 on 2016-11-04 00:50 -from __future__ import unicode_literals from django.db import migrations, models @@ -8,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('ingredients', '0001_initial'), + ("ingredients", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='ingredient', - name='notes', + model_name="ingredient", + name="notes", field=models.TextField(blank=True, null=True), ), ] diff --git a/examples/cookbook/cookbook/ingredients/schema.py b/examples/cookbook/cookbook/ingredients/schema.py index 43b6118..4ed9eff 100644 --- a/examples/cookbook/cookbook/ingredients/schema.py +++ b/examples/cookbook/cookbook/ingredients/schema.py @@ -28,7 +28,7 @@ class IngredientNode(DjangoObjectType): } -class Query(object): +class Query: category = Node.Field(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode) diff --git a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py index 338c71a..fceeb9b 100644 --- a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py +++ b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9 on 2015-12-04 18:20 -from __future__ import unicode_literals import django.db.models.deletion from django.db import migrations, models @@ -11,26 +9,62 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('ingredients', '0001_initial'), + ("ingredients", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Recipe', + name="Recipe", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100)), - ('instructions', models.TextField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100)), + ("instructions", models.TextField()), ], ), migrations.CreateModel( - name='RecipeIngredient', + name="RecipeIngredient", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('amount', models.FloatField()), - ('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)), - ('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')), - ('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("amount", models.FloatField()), + ( + "unit", + models.CharField( + choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")], + max_length=20, + ), + ), + ( + "ingredient", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="used_by", + to="ingredients.Ingredient", + ), + ), + ( + "recipes", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="amounts", + to="recipes.Recipe", + ), + ), ], ), ] diff --git a/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py b/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py index f135392..0156920 100644 --- a/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py +++ b/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9 on 2016-11-04 01:06 -from __future__ import unicode_literals from django.db import migrations, models @@ -8,18 +6,26 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('recipes', '0001_initial'), + ("recipes", "0001_initial"), ] operations = [ migrations.RenameField( - model_name='recipeingredient', - old_name='recipes', - new_name='recipe', + model_name="recipeingredient", + old_name="recipes", + new_name="recipe", ), migrations.AlterField( - model_name='recipeingredient', - name='unit', - field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20), + model_name="recipeingredient", + name="unit", + field=models.CharField( + choices=[ + (b"unit", b"Units"), + (b"kg", b"Kilograms"), + (b"l", b"Litres"), + (b"st", b"Shots"), + ], + max_length=20, + ), ), ] diff --git a/examples/cookbook/cookbook/recipes/schema.py b/examples/cookbook/cookbook/recipes/schema.py index c7298aa..ea5ed38 100644 --- a/examples/cookbook/cookbook/recipes/schema.py +++ b/examples/cookbook/cookbook/recipes/schema.py @@ -25,7 +25,7 @@ class RecipeIngredientNode(DjangoObjectType): } -class Query(object): +class Query: recipe = Node.Field(RecipeNode) all_recipes = DjangoFilterConnectionField(RecipeNode) diff --git a/examples/cookbook/dummy_data.json b/examples/cookbook/dummy_data.json index f541da5..c585bfc 100644 --- a/examples/cookbook/dummy_data.json +++ b/examples/cookbook/dummy_data.json @@ -1 +1,302 @@ -[{"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$24000$0SgBlSlnbv5c$ijVQipm2aNDlcrTL8Qi3SVNHphTm4HIsDfUi4kn9tog=", "last_login": "2016-11-04T00:46:58Z", "is_superuser": true, "username": "admin", "first_name": "", "last_name": "", "email": "asdf@example.com", "is_staff": true, "is_active": true, "date_joined": "2016-11-03T18:24:40Z", "groups": [], "user_permissions": []}}, {"model": "recipes.recipe", "pk": 1, "fields": {"title": "Cheerios With a Shot of Vermouth", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 2, "fields": {"title": "Quail Eggs in Whipped Cream and MSG", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 3, "fields": {"title": "Deep Fried Skittles", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 4, "fields": {"title": "Newt ala Doritos", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 5, "fields": {"title": "Fruit Salad", "instructions": "Chop up and add together"}}, {"model": "recipes.recipeingredient", "pk": 1, "fields": {"recipes": 5, "ingredient": 9, "amount": 1.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 2, "fields": {"recipes": 5, "ingredient": 10, "amount": 2.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 3, "fields": {"recipes": 5, "ingredient": 7, "amount": 3.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 4, "fields": {"recipes": 5, "ingredient": 8, "amount": 4.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 5, "fields": {"recipes": 4, "ingredient": 5, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 6, "fields": {"recipes": 4, "ingredient": 6, "amount": 2.0, "unit": "l"}}, {"model": "recipes.recipeingredient", "pk": 7, "fields": {"recipes": 3, "ingredient": 4, "amount": 1.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 8, "fields": {"recipes": 2, "ingredient": 2, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 9, "fields": {"recipes": 2, "ingredient": 11, "amount": 2.0, "unit": "l"}}, {"model": "recipes.recipeingredient", "pk": 10, "fields": {"recipes": 2, "ingredient": 12, "amount": 3.0, "unit": "st"}}, {"model": "recipes.recipeingredient", "pk": 11, "fields": {"recipes": 1, "ingredient": 1, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 12, "fields": {"recipes": 1, "ingredient": 3, "amount": 1.0, "unit": "st"}}, {"model": "ingredients.category", "pk": 1, "fields": {"name": "fruit"}}, {"model": "ingredients.category", "pk": 3, "fields": {"name": "xkcd"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Cheerios", "notes": "this is a note", "category": 3}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Quail Eggs", "notes": "has more notes", "category": 3}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Vermouth", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Skittles", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 5, "fields": {"name": "Newt", "notes": "Braised and Confuesd", "category": 3}}, {"model": "ingredients.ingredient", "pk": 6, "fields": {"name": "Doritos", "notes": "Crushed", "category": 3}}, {"model": "ingredients.ingredient", "pk": 7, "fields": {"name": "Apple", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 8, "fields": {"name": "Orange", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 9, "fields": {"name": "Banana", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 10, "fields": {"name": "Grapes", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 11, "fields": {"name": "Whipped Cream", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 12, "fields": {"name": "MSG", "notes": "", "category": 3}}] \ No newline at end of file +[ + { + "fields": { + "date_joined": "2016-11-03T18:24:40Z", + "email": "asdf@example.com", + "first_name": "", + "groups": [], + "is_active": true, + "is_staff": true, + "is_superuser": true, + "last_login": "2016-11-04T00:46:58Z", + "last_name": "", + "password": "pbkdf2_sha256$24000$0SgBlSlnbv5c$ijVQipm2aNDlcrTL8Qi3SVNHphTm4HIsDfUi4kn9tog=", + "user_permissions": [], + "username": "admin" + }, + "model": "auth.user", + "pk": 1 + }, + { + "fields": { + "instructions": "https://xkcd.com/720/", + "title": "Cheerios With a Shot of Vermouth" + }, + "model": "recipes.recipe", + "pk": 1 + }, + { + "fields": { + "instructions": "https://xkcd.com/720/", + "title": "Quail Eggs in Whipped Cream and MSG" + }, + "model": "recipes.recipe", + "pk": 2 + }, + { + "fields": { + "instructions": "https://xkcd.com/720/", + "title": "Deep Fried Skittles" + }, + "model": "recipes.recipe", + "pk": 3 + }, + { + "fields": { + "instructions": "https://xkcd.com/720/", + "title": "Newt ala Doritos" + }, + "model": "recipes.recipe", + "pk": 4 + }, + { + "fields": { + "instructions": "Chop up and add together", + "title": "Fruit Salad" + }, + "model": "recipes.recipe", + "pk": 5 + }, + { + "fields": { + "amount": 1.0, + "ingredient": 9, + "recipes": 5, + "unit": "unit" + }, + "model": "recipes.recipeingredient", + "pk": 1 + }, + { + "fields": { + "amount": 2.0, + "ingredient": 10, + "recipes": 5, + "unit": "unit" + }, + "model": "recipes.recipeingredient", + "pk": 2 + }, + { + "fields": { + "amount": 3.0, + "ingredient": 7, + "recipes": 5, + "unit": "unit" + }, + "model": "recipes.recipeingredient", + "pk": 3 + }, + { + "fields": { + "amount": 4.0, + "ingredient": 8, + "recipes": 5, + "unit": "unit" + }, + "model": "recipes.recipeingredient", + "pk": 4 + }, + { + "fields": { + "amount": 1.0, + "ingredient": 5, + "recipes": 4, + "unit": "kg" + }, + "model": "recipes.recipeingredient", + "pk": 5 + }, + { + "fields": { + "amount": 2.0, + "ingredient": 6, + "recipes": 4, + "unit": "l" + }, + "model": "recipes.recipeingredient", + "pk": 6 + }, + { + "fields": { + "amount": 1.0, + "ingredient": 4, + "recipes": 3, + "unit": "unit" + }, + "model": "recipes.recipeingredient", + "pk": 7 + }, + { + "fields": { + "amount": 1.0, + "ingredient": 2, + "recipes": 2, + "unit": "kg" + }, + "model": "recipes.recipeingredient", + "pk": 8 + }, + { + "fields": { + "amount": 2.0, + "ingredient": 11, + "recipes": 2, + "unit": "l" + }, + "model": "recipes.recipeingredient", + "pk": 9 + }, + { + "fields": { + "amount": 3.0, + "ingredient": 12, + "recipes": 2, + "unit": "st" + }, + "model": "recipes.recipeingredient", + "pk": 10 + }, + { + "fields": { + "amount": 1.0, + "ingredient": 1, + "recipes": 1, + "unit": "kg" + }, + "model": "recipes.recipeingredient", + "pk": 11 + }, + { + "fields": { + "amount": 1.0, + "ingredient": 3, + "recipes": 1, + "unit": "st" + }, + "model": "recipes.recipeingredient", + "pk": 12 + }, + { + "fields": { + "name": "fruit" + }, + "model": "ingredients.category", + "pk": 1 + }, + { + "fields": { + "name": "xkcd" + }, + "model": "ingredients.category", + "pk": 3 + }, + { + "fields": { + "category": 3, + "name": "Cheerios", + "notes": "this is a note" + }, + "model": "ingredients.ingredient", + "pk": 1 + }, + { + "fields": { + "category": 3, + "name": "Quail Eggs", + "notes": "has more notes" + }, + "model": "ingredients.ingredient", + "pk": 2 + }, + { + "fields": { + "category": 3, + "name": "Vermouth", + "notes": "" + }, + "model": "ingredients.ingredient", + "pk": 3 + }, + { + "fields": { + "category": 3, + "name": "Skittles", + "notes": "" + }, + "model": "ingredients.ingredient", + "pk": 4 + }, + { + "fields": { + "category": 3, + "name": "Newt", + "notes": "Braised and Confuesd" + }, + "model": "ingredients.ingredient", + "pk": 5 + }, + { + "fields": { + "category": 3, + "name": "Doritos", + "notes": "Crushed" + }, + "model": "ingredients.ingredient", + "pk": 6 + }, + { + "fields": { + "category": 1, + "name": "Apple", + "notes": "" + }, + "model": "ingredients.ingredient", + "pk": 7 + }, + { + "fields": { + "category": 1, + "name": "Orange", + "notes": "" + }, + "model": "ingredients.ingredient", + "pk": 8 + }, + { + "fields": { + "category": 1, + "name": "Banana", + "notes": "" + }, + "model": "ingredients.ingredient", + "pk": 9 + }, + { + "fields": { + "category": 1, + "name": "Grapes", + "notes": "" + }, + "model": "ingredients.ingredient", + "pk": 10 + }, + { + "fields": { + "category": 3, + "name": "Whipped Cream", + "notes": "" + }, + "model": "ingredients.ingredient", + "pk": 11 + }, + { + "fields": { + "category": 3, + "name": "MSG", + "notes": "" + }, + "model": "ingredients.ingredient", + "pk": 12 + } +] diff --git a/examples/starwars/models.py b/examples/starwars/models.py index 03e06a2..fb76b03 100644 --- a/examples/starwars/models.py +++ b/examples/starwars/models.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.db import models diff --git a/graphene_django/compat.py b/graphene_django/compat.py index 1956786..b0e4753 100644 --- a/graphene_django/compat.py +++ b/graphene_django/compat.py @@ -1,4 +1,4 @@ -class MissingType(object): +class MissingType: def __init__(self, *args, **kwargs): pass diff --git a/graphene_django/converter.py b/graphene_django/converter.py index d9345d8..386103a 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -74,8 +74,7 @@ def get_choices(choices): choices = choices.items() for value, help_text in choices: if isinstance(help_text, (tuple, list)): - for choice in get_choices(help_text): - yield choice + yield from get_choices(help_text) else: name = convert_choice_name(value) while name in converted_names: @@ -92,7 +91,7 @@ def convert_choices_to_named_enum_with_descriptions(name, choices): named_choices = [(c[0], c[1]) for c in choices] named_choices_descriptions = {c[0]: c[2] for c in choices} - class EnumWithDescriptionsType(object): + class EnumWithDescriptionsType: @property def description(self): return str(named_choices_descriptions[self.name]) @@ -109,7 +108,7 @@ def generate_enum_name(django_model_meta, field): ) name = custom_func(field) elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V2_NAMING is True: - name = to_camel_case("{}_{}".format(django_model_meta.object_name, field.name)) + name = to_camel_case(f"{django_model_meta.object_name}_{field.name}") else: name = "{app_label}{object_name}{field_name}Choices".format( app_label=to_camel_case(django_model_meta.app_label.title()), @@ -155,7 +154,9 @@ def get_django_field_description(field): @singledispatch def convert_django_field(field, registry=None): raise Exception( - "Don't know how to convert the Django field %s (%s)" % (field, field.__class__) + "Don't know how to convert the Django field {} ({})".format( + field, field.__class__ + ) ) diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index 804e7c8..5b9d82e 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -7,7 +7,7 @@ from .exception.formating import wrap_exception from .types import DjangoDebug -class DjangoDebugContext(object): +class DjangoDebugContext: def __init__(self): self.debug_promise = None self.promises = [] @@ -46,7 +46,7 @@ class DjangoDebugContext(object): unwrap_cursor(connection) -class DjangoDebugMiddleware(object): +class DjangoDebugMiddleware: def resolve(self, next, root, info, **args): context = info.context django_debug = getattr(context, "django_debug", None) diff --git a/graphene_django/debug/sql/tracking.py b/graphene_django/debug/sql/tracking.py index f7346e6..bf0ea36 100644 --- a/graphene_django/debug/sql/tracking.py +++ b/graphene_django/debug/sql/tracking.py @@ -1,5 +1,4 @@ # Code obtained from django-debug-toolbar sql panel tracking -from __future__ import absolute_import, unicode_literals import json from threading import local @@ -50,7 +49,7 @@ def unwrap_cursor(connection): del connection._graphene_cursor -class ExceptionCursorWrapper(object): +class ExceptionCursorWrapper: """ Wraps a cursor and raises an exception on any operation. Used in Templates panel. @@ -63,7 +62,7 @@ class ExceptionCursorWrapper(object): raise SQLQueryTriggered() -class NormalCursorWrapper(object): +class NormalCursorWrapper: """ Wraps a cursor and logs queries. """ @@ -85,7 +84,7 @@ class NormalCursorWrapper(object): if not params: return params if isinstance(params, dict): - return dict((key, self._quote_expr(value)) for key, value in params.items()) + return {key: self._quote_expr(value) for key, value in params.items()} return list(map(self._quote_expr, params)) def _decode(self, param): diff --git a/graphene_django/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py index eae94dc..1ea86b1 100644 --- a/graphene_django/debug/tests/test_query.py +++ b/graphene_django/debug/tests/test_query.py @@ -8,7 +8,7 @@ from ..middleware import DjangoDebugMiddleware from ..types import DjangoDebug -class context(object): +class context: pass diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 05a7010..0fe123d 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -28,7 +28,7 @@ class DjangoListField(Field): _type = _type.of_type # Django would never return a Set of None vvvvvvv - super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs) + super().__init__(List(NonNull(_type)), *args, **kwargs) assert issubclass( self._underlying_type, DjangoObjectType @@ -63,7 +63,7 @@ class DjangoListField(Field): return queryset def wrap_resolve(self, parent_resolver): - resolver = super(DjangoListField, self).wrap_resolve(parent_resolver) + resolver = super().wrap_resolve(parent_resolver) _type = self.type if isinstance(_type, NonNull): _type = _type.of_type @@ -87,7 +87,7 @@ class DjangoConnectionField(ConnectionField): graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST, ) kwargs.setdefault("offset", Int()) - super(DjangoConnectionField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) @property def type(self): diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index eeb197e..cdb8f85 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -44,7 +44,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): self._filtering_args = None self._extra_filter_meta = extra_filter_meta self._base_args = None - super(DjangoFilterConnectionField, self).__init__(type_, *args, **kwargs) + super().__init__(type_, *args, **kwargs) @property def args(self): @@ -90,9 +90,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): kwargs[k] = convert_enum(v) return kwargs - qs = super(DjangoFilterConnectionField, cls).resolve_queryset( - connection, iterable, info, args - ) + qs = super().resolve_queryset(connection, iterable, info, args) filterset = filterset_class( data=filter_kwargs(), queryset=qs, request=info.context diff --git a/graphene_django/filter/filters/array_filter.py b/graphene_django/filter/filters/array_filter.py index e886cff..b6f4808 100644 --- a/graphene_django/filter/filters/array_filter.py +++ b/graphene_django/filter/filters/array_filter.py @@ -22,6 +22,6 @@ class ArrayFilter(TypedFilter): return qs if self.distinct: qs = qs.distinct() - lookup = "%s__%s" % (self.field_name, self.lookup_expr) + lookup = f"{self.field_name}__{self.lookup_expr}" qs = self.get_method(qs)(**{lookup: value}) return qs diff --git a/graphene_django/filter/filters/global_id_filter.py b/graphene_django/filter/filters/global_id_filter.py index da16585..37877d5 100644 --- a/graphene_django/filter/filters/global_id_filter.py +++ b/graphene_django/filter/filters/global_id_filter.py @@ -17,7 +17,7 @@ class GlobalIDFilter(Filter): _id = None if value is not None: _, _id = from_global_id(value) - return super(GlobalIDFilter, self).filter(qs, _id) + return super().filter(qs, _id) class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter): @@ -25,4 +25,4 @@ class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter): def filter(self, qs, value): gids = [from_global_id(v)[1] for v in value] - return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids) + return super().filter(qs, gids) diff --git a/graphene_django/filter/filters/list_filter.py b/graphene_django/filter/filters/list_filter.py index 9689be3..6689877 100644 --- a/graphene_django/filter/filters/list_filter.py +++ b/graphene_django/filter/filters/list_filter.py @@ -23,4 +23,4 @@ class ListFilter(TypedFilter): else: return qs.none() else: - return super(ListFilter, self).filter(qs, value) + return super().filter(qs, value) diff --git a/graphene_django/filter/filters/typed_filter.py b/graphene_django/filter/filters/typed_filter.py index 2c813e4..76f903a 100644 --- a/graphene_django/filter/filters/typed_filter.py +++ b/graphene_django/filter/filters/typed_filter.py @@ -12,7 +12,7 @@ class TypedFilter(Filter): def __init__(self, input_type=None, *args, **kwargs): self._input_type = input_type - super(TypedFilter, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) @property def input_type(self): diff --git a/graphene_django/filter/filterset.py b/graphene_django/filter/filterset.py index 57c35af..fa91477 100644 --- a/graphene_django/filter/filterset.py +++ b/graphene_django/filter/filterset.py @@ -31,7 +31,7 @@ class GrapheneFilterSetMixin(BaseFilterSet): def setup_filterset(filterset_class): """Wrap a provided filterset in Graphene-specific functionality""" return type( - "Graphene{}".format(filterset_class.__name__), + f"Graphene{filterset_class.__name__}", (filterset_class, GrapheneFilterSetMixin), {}, ) @@ -40,7 +40,7 @@ def setup_filterset(filterset_class): def custom_filterset_factory(model, filterset_base_class=FilterSet, **meta): """Create a filterset for the given model using the provided meta data""" meta.update({"model": model}) - meta_class = type(str("Meta"), (object,), meta) + meta_class = type("Meta", (object,), meta) filterset = type( str("%sFilterSet" % model._meta.object_name), (filterset_base_class, GrapheneFilterSetMixin), diff --git a/graphene_django/filter/tests/conftest.py b/graphene_django/filter/tests/conftest.py index e2bba68..a11831c 100644 --- a/graphene_django/filter/tests/conftest.py +++ b/graphene_django/filter/tests/conftest.py @@ -1,4 +1,4 @@ -from mock import MagicMock +from unittest.mock import MagicMock import pytest from django.db import models diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index fe4ae87..bee3c6c 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -67,7 +67,7 @@ def assert_arguments(field, *arguments): actual = [name for name in args if name not in ignore and not name.startswith("_")] assert set(arguments) == set( actual - ), "Expected arguments ({}) did not match actual ({})".format(arguments, actual) + ), f"Expected arguments ({arguments}) did not match actual ({actual})" def assert_orderable(field): @@ -141,7 +141,7 @@ def test_filter_shortcut_filterset_context(): @property def qs(self): - qs = super(ArticleContextFilter, self).qs + qs = super().qs return qs.filter(reporter=self.request.reporter) class Query(ObjectType): @@ -166,7 +166,7 @@ def test_filter_shortcut_filterset_context(): editor=r2, ) - class context(object): + class context: reporter = r2 query = """ diff --git a/graphene_django/filter/tests/test_in_filter.py b/graphene_django/filter/tests/test_in_filter.py index 7ad0286..a69d6f5 100644 --- a/graphene_django/filter/tests/test_in_filter.py +++ b/graphene_django/filter/tests/test_in_filter.py @@ -349,19 +349,19 @@ def test_fk_id_in_filter(query): schema = Schema(query=query) query = """ - query { - articles (reporter_In: [%s, %s]) { - edges { - node { + query {{ + articles (reporter_In: [{}, {}]) {{ + edges {{ + node {{ headline - reporter { + reporter {{ lastName - } - } - } - } - } - """ % ( + }} + }} + }} + }} + }} + """.format( john_doe.id, jean_bon.id, ) diff --git a/graphene_django/filter/tests/test_typed_filter.py b/graphene_django/filter/tests/test_typed_filter.py index a7edc56..f22138f 100644 --- a/graphene_django/filter/tests/test_typed_filter.py +++ b/graphene_django/filter/tests/test_typed_filter.py @@ -98,7 +98,7 @@ def test_typed_filter_schema(schema): ) for filter_field, gql_type in filters.items(): - assert "{}: {}".format(filter_field, gql_type) in all_articles_filters + assert f"{filter_field}: {gql_type}" in all_articles_filters def test_typed_filters_work(schema): diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 13e9863..3d59464 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -95,7 +95,7 @@ class DjangoFormMutation(BaseDjangoFormMutation): _meta.fields = yank_fields_from_attrs(output_fields, _as=Field) input_fields = yank_fields_from_attrs(input_fields, _as=InputField) - super(DjangoFormMutation, cls).__init_subclass_with_meta__( + super().__init_subclass_with_meta__( _meta=_meta, input_fields=input_fields, **options ) @@ -127,7 +127,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): return_field_name=None, only_fields=(), exclude_fields=(), - **options + **options, ): if not form_class: @@ -147,7 +147,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): registry = get_global_registry() model_type = registry.get_type_for_model(model) if not model_type: - raise Exception("No type registered for model: {}".format(model.__name__)) + raise Exception(f"No type registered for model: {model.__name__}") if not return_field_name: model_name = model.__name__ @@ -163,7 +163,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): _meta.fields = yank_fields_from_attrs(output_fields, _as=Field) input_fields = yank_fields_from_attrs(input_fields, _as=InputField) - super(DjangoModelFormMutation, cls).__init_subclass_with_meta__( + super().__init_subclass_with_meta__( _meta=_meta, input_fields=input_fields, **options ) diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index 4165430..bedddba 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -73,16 +73,12 @@ class Command(CommandArguments): elif file_extension == ".json": self.save_json_file(out, schema_dict, indent) else: - raise CommandError( - 'Unrecognised file format "{}"'.format(file_extension) - ) + raise CommandError(f'Unrecognised file format "{file_extension}"') style = getattr(self, "style", None) success = getattr(style, "SUCCESS", lambda x: x) - self.stdout.write( - success("Successfully dumped GraphQL schema to {}".format(out)) - ) + self.stdout.write(success(f"Successfully dumped GraphQL schema to {out}")) def handle(self, *args, **options): options_schema = options.get("schema") diff --git a/graphene_django/registry.py b/graphene_django/registry.py index 50a8ae5..4708637 100644 --- a/graphene_django/registry.py +++ b/graphene_django/registry.py @@ -1,4 +1,4 @@ -class Registry(object): +class Registry: def __init__(self): self._registry = {} self._field_registry = {} diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 000b21e..c01d915 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -114,7 +114,7 @@ class SerializerMutation(ClientIDMutation): _meta.fields = yank_fields_from_attrs(output_fields, _as=Field) input_fields = yank_fields_from_attrs(input_fields, _as=InputField) - super(SerializerMutation, cls).__init_subclass_with_meta__( + super().__init_subclass_with_meta__( _meta=_meta, input_fields=input_fields, **options ) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 9835475..1d850f0 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -72,7 +72,7 @@ def convert_serializer_to_input_type(serializer_class): for name, field in serializer.fields.items() } ret_type = type( - "{}Input".format(serializer.__class__.__name__), + f"{serializer.__class__.__name__}Input", (graphene.InputObjectType,), items, ) diff --git a/graphene_django/settings.py b/graphene_django/settings.py index 6f62326..9c7dc38 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -11,7 +11,6 @@ This module provides the `graphene_settings` object, that is used to access Graphene settings, checking for user settings first, then falling back to the defaults. """ -from __future__ import unicode_literals from django.conf import settings from django.test.signals import setting_changed @@ -78,7 +77,7 @@ def import_from_string(val, setting_name): module = importlib.import_module(module_path) return getattr(module, class_name) except (ImportError, AttributeError) as e: - msg = "Could not import '%s' for Graphene setting '%s'. %s: %s." % ( + msg = "Could not import '{}' for Graphene setting '{}'. {}: {}.".format( val, setting_name, e.__class__.__name__, @@ -87,7 +86,7 @@ def import_from_string(val, setting_name): raise ImportError(msg) -class GrapheneSettings(object): +class GrapheneSettings: """ A settings object, that allows API settings to be accessed as properties. For example: diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index c26a6d8..636f74c 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.db import models from django.utils.translation import gettext_lazy as _ @@ -37,7 +35,7 @@ class Film(models.Model): class DoeReporterManager(models.Manager): def get_queryset(self): - return super(DoeReporterManager, self).get_queryset().filter(last_name="Doe") + return super().get_queryset().filter(last_name="Doe") class Reporter(models.Model): @@ -57,7 +55,7 @@ class Reporter(models.Model): ) def __str__(self): # __unicode__ on Python 2 - return "%s %s" % (self.first_name, self.last_name) + return f"{self.first_name} {self.last_name}" def __init__(self, *args, **kwargs): """ @@ -67,7 +65,7 @@ class Reporter(models.Model): when a CNNReporter is pulled from the database, it is still of type Reporter. This was added to test proxy model support. """ - super(Reporter, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if self.reporter_type == 2: # quick and dirty way without enums self.__class__ = CNNReporter @@ -77,7 +75,7 @@ class Reporter(models.Model): class CNNReporterManager(models.Manager): def get_queryset(self): - return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2) + return super().get_queryset().filter(reporter_type=2) class CNNReporter(Reporter): diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py index 11a15bc..a281abb 100644 --- a/graphene_django/tests/test_command.py +++ b/graphene_django/tests/test_command.py @@ -2,7 +2,7 @@ from textwrap import dedent from django.core import management from io import StringIO -from mock import mock_open, patch +from unittest.mock import mock_open, patch from graphene import ObjectType, Schema, String diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index e6ae64f..504bb0e 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1151,9 +1151,9 @@ def test_connection_should_limit_after_to_list_length(): REPORTERS = [ dict( - first_name="First {}".format(i), - last_name="Last {}".format(i), - email="johndoe+{}@example.com".format(i), + first_name=f"First {i}", + last_name=f"Last {i}", + email=f"johndoe+{i}@example.com", a_choice=1, ) for i in range(6) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 4885917..fad26e2 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -3,7 +3,7 @@ from textwrap import dedent import pytest from django.db import models -from mock import patch +from unittest.mock import patch from graphene import Connection, Field, Interface, ObjectType, Schema, String from graphene.relay import Node @@ -104,7 +104,7 @@ def test_django_objecttype_with_custom_meta(): @classmethod def __init_subclass_with_meta__(cls, **options): options.setdefault("_meta", ArticleTypeOptions(cls)) - super(ArticleType, cls).__init_subclass_with_meta__(**options) + super().__init_subclass_with_meta__(**options) class Article(ArticleType): class Meta: @@ -484,7 +484,7 @@ def test_django_objecttype_neither_fields_nor_exclude(): def custom_enum_name(field): - return "CustomEnum{}".format(field.name.title()) + return f"CustomEnum{field.name.title()}" class TestDjangoObjectType: diff --git a/graphene_django/tests/test_utils.py b/graphene_django/tests/test_utils.py index adad00e..fa269b4 100644 --- a/graphene_django/tests/test_utils.py +++ b/graphene_django/tests/test_utils.py @@ -2,7 +2,7 @@ import json import pytest from django.utils.translation import gettext_lazy -from mock import patch +from unittest.mock import patch from ..utils import camelize, get_model_fields, GraphQLTestCase from .models import Film, Reporter @@ -11,11 +11,11 @@ from ..utils.testing import graphql_query def test_get_model_fields_no_duplication(): reporter_fields = get_model_fields(Reporter) - reporter_name_set = set([field[0] for field in reporter_fields]) + reporter_name_set = {field[0] for field in reporter_fields} assert len(reporter_fields) == len(reporter_name_set) film_fields = get_model_fields(Film) - film_name_set = set([field[0] for field in film_fields]) + film_name_set = {field[0] for field in film_fields} assert len(film_fields) == len(film_name_set) @@ -54,7 +54,7 @@ def test_graphql_test_case_operation_name(post_mock): tc._pre_setup() tc.setUpClass() tc.query("query { }", operation_name="QueryName") - body = json.loads(post_mock.call_args.args[1]) + body = json.loads(post_mock.call_args[0][1]) # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request assert ( "operationName", @@ -66,7 +66,7 @@ def test_graphql_test_case_operation_name(post_mock): @patch("graphene_django.utils.testing.Client.post") def test_graphql_query_case_operation_name(post_mock): graphql_query("query { }", operation_name="QueryName") - body = json.loads(post_mock.call_args.args[1]) + body = json.loads(post_mock.call_args[0][1]) # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request assert ( "operationName", diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py index c2f18c3..5cadefe 100644 --- a/graphene_django/tests/test_views.py +++ b/graphene_django/tests/test_views.py @@ -2,7 +2,7 @@ import json import pytest -from mock import patch +from unittest.mock import patch from django.db import connection @@ -507,7 +507,7 @@ def test_handles_invalid_json_bodies(client): def test_handles_django_request_error(client, monkeypatch): def mocked_read(*args): - raise IOError("foo-bar") + raise OSError("foo-bar") monkeypatch.setattr("django.http.request.HttpRequest.read", mocked_read) diff --git a/graphene_django/types.py b/graphene_django/types.py index 0ebb7d3..a6e54af 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -168,10 +168,8 @@ class DjangoObjectType(ObjectType): 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" - ) + "Can only set filter_fields or filterset_class if " + "Django-Filter is installed" ) assert not (fields and exclude), ( @@ -228,7 +226,7 @@ class DjangoObjectType(ObjectType): if use_connection is None and interfaces: use_connection = any( - (issubclass(interface, Node) for interface in interfaces) + issubclass(interface, Node) for interface in interfaces ) if use_connection and not connection: @@ -255,7 +253,7 @@ class DjangoObjectType(ObjectType): _meta.fields = django_fields _meta.connection = connection - super(DjangoObjectType, cls).__init_subclass_with_meta__( + super().__init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options ) diff --git a/graphene_django/utils/testing.py b/graphene_django/utils/testing.py index ca0d185..ad9ff35 100644 --- a/graphene_django/utils/testing.py +++ b/graphene_django/utils/testing.py @@ -65,7 +65,7 @@ def graphql_query( return resp -class GraphQLTestMixin(object): +class GraphQLTestMixin: """ Based on: https://www.sam.today/blog/testing-graphql-with-graphene-django/ """ diff --git a/graphene_django/views.py b/graphene_django/views.py index bf333a9..e772d53 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -26,7 +26,7 @@ class HttpError(Exception): def __init__(self, response, message=None, *args, **kwargs): self.response = response self.message = message = message or response.content.decode() - super(HttpError, self).__init__(message, *args, **kwargs) + super().__init__(message, *args, **kwargs) def get_accepted_content_types(request): diff --git a/setup.cfg b/setup.cfg index 52f6bf6..c725df1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ test=pytest universal=1 [flake8] -exclude = docs,graphene_django/debug/sql/*,migrations +exclude = docs,graphene_django/debug/sql/* max-line-length = 120 select = # Dictionary key repeated diff --git a/tox.ini b/tox.ini index 11b4893..47fd1e5 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = py{37,38,39,310}-django32, py{38,39,310}-django{40,41,main}, - black,flake8 + pre-commit [gh-actions] python = @@ -32,14 +32,8 @@ deps = djangomain: https://github.com/django/django/archive/main.zip commands = {posargs:py.test --cov=graphene_django graphene_django examples} -[testenv:black] -basepython = python3.9 -deps = -e.[dev] -commands = - black --exclude "/migrations/" graphene_django examples setup.py --check - -[testenv:flake8] -basepython = python3.9 -deps = -e.[dev] +[testenv:pre-commit] +skip_install = true +deps = pre-commit commands = - flake8 graphene_django examples setup.py + pre-commit run --all-files --show-diff-on-failure From 86c5309c4537b8f898a4cf8f2b516efa14be18f8 Mon Sep 17 00:00:00 2001 From: Yuekui Date: Mon, 14 Nov 2022 02:56:18 -0800 Subject: [PATCH 36/38] Fix broken UT due to pytest import error (#1368) --- graphene_django/forms/tests/test_converter.py | 2 +- graphene_django/forms/tests/test_mutation.py | 2 +- graphene_django/rest_framework/tests/test_field_converter.py | 2 +- graphene_django/rest_framework/tests/test_mutation.py | 2 +- graphene_django/tests/issues/test_520.py | 4 ++-- graphene_django/tests/test_converter.py | 2 +- graphene_django/tests/test_forms.py | 2 +- graphene_django/tests/test_query.py | 2 +- graphene_django/tests/test_schema.py | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/graphene_django/forms/tests/test_converter.py b/graphene_django/forms/tests/test_converter.py index 05584a5..b61227b 100644 --- a/graphene_django/forms/tests/test_converter.py +++ b/graphene_django/forms/tests/test_converter.py @@ -1,5 +1,5 @@ from django import forms -from py.test import raises +from pytest import raises import graphene from graphene import ( diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 0770acb..14c407c 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -1,7 +1,7 @@ import pytest from django import forms from django.core.exceptions import ValidationError -from py.test import raises +from pytest import raises from graphene import Field, ObjectType, Schema, String from graphene_django import DjangoObjectType diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 4858365..8da8377 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -3,7 +3,7 @@ import copy import graphene from django.db import models from graphene import InputObjectType -from py.test import raises +from pytest import raises from rest_framework import serializers from ..serializer_converter import convert_serializer_field diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index e0e5602..5de8237 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -1,6 +1,6 @@ import datetime -from py.test import raises +from pytest import raises from rest_framework import serializers from graphene import Field, ResolveInfo diff --git a/graphene_django/tests/issues/test_520.py b/graphene_django/tests/issues/test_520.py index 60c5b54..4e55f96 100644 --- a/graphene_django/tests/issues/test_520.py +++ b/graphene_django/tests/issues/test_520.py @@ -8,8 +8,8 @@ import graphene from graphene import Field, ResolveInfo from graphene.types.inputobjecttype import InputObjectType -from py.test import raises -from py.test import mark +from pytest import raises +from pytest import mark from rest_framework import serializers from ...types import DjangoObjectType diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 9158b12..4996505 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -3,7 +3,7 @@ from collections import namedtuple import pytest from django.db import models from django.utils.translation import gettext_lazy as _ -from py.test import raises +from pytest import raises import graphene from graphene import NonNull diff --git a/graphene_django/tests/test_forms.py b/graphene_django/tests/test_forms.py index fa6628d..a42fcee 100644 --- a/graphene_django/tests/test_forms.py +++ b/graphene_django/tests/test_forms.py @@ -1,5 +1,5 @@ from django.core.exceptions import ValidationError -from py.test import raises +from pytest import raises from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 504bb0e..df339d8 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -6,7 +6,7 @@ from django.db import models from django.db.models import Q from django.utils.functional import SimpleLazyObject from graphql_relay import to_global_id -from py.test import raises +from pytest import raises import graphene from graphene.relay import Node diff --git a/graphene_django/tests/test_schema.py b/graphene_django/tests/test_schema.py index 1c889f1..ff2d8a6 100644 --- a/graphene_django/tests/test_schema.py +++ b/graphene_django/tests/test_schema.py @@ -1,4 +1,4 @@ -from py.test import raises +from pytest import raises from ..registry import Registry from ..types import DjangoObjectType From a000d58514b1fd28b701cf3d159f8e5cafb28cae Mon Sep 17 00:00:00 2001 From: Omar Mirza Date: Fri, 7 Oct 2022 18:11:27 -0400 Subject: [PATCH 37/38] Clarify cookbook example READMEs Currently the relay cookbook's readme has a link to the plain tutorial page. The plain cookbook readme also instructs the user to change directory into the directory for the relay example. This change fixes both issues. Also changed the title for the relay example to specify that it uses relay. --- examples/cookbook-plain/README.md | 2 +- examples/cookbook/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/cookbook-plain/README.md b/examples/cookbook-plain/README.md index 0ec906b..dcd2420 100644 --- a/examples/cookbook-plain/README.md +++ b/examples/cookbook-plain/README.md @@ -14,7 +14,7 @@ whole Graphene repository: ```bash # Get the example project code git clone https://github.com/graphql-python/graphene-django.git -cd graphene-django/examples/cookbook +cd graphene-django/examples/cookbook-plain ``` It is good idea (but not required) to create a virtual environment diff --git a/examples/cookbook/README.md b/examples/cookbook/README.md index 0ec906b..098b119 100644 --- a/examples/cookbook/README.md +++ b/examples/cookbook/README.md @@ -1,4 +1,4 @@ -Cookbook Example Django Project +Cookbook Example (Relay) Django Project =============================== This example project demos integration between Graphene and Django. @@ -60,5 +60,5 @@ Now you should be ready to start the server: Now head on over to [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) and run some queries! -(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema) +(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-relay/#testing-our-graphql-schema) for some example queries) From daa0ab046bf0b22b36b45b2fde0471997cdd7044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Clgen=20Sar=C4=B1kavak?= Date: Wed, 23 Nov 2022 01:16:14 +0300 Subject: [PATCH 38/38] Update pre-commit tools (#1364) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 829cb81..e64c4e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,12 +16,12 @@ repos: - id: trailing-whitespace exclude: README.md - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v3.2.0 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.10.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8