From 7e1a1d1fb8f34da68e8d0111ec3230521735b936 Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Fri, 21 Apr 2023 21:40:51 +0300 Subject: [PATCH 01/17] Update django-filter url --- docs/filtering.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/filtering.rst b/docs/filtering.rst index fb686a1..d850b69 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -2,7 +2,7 @@ Filtering ========= Graphene integrates with -`django-filter `__ to provide filtering of results. +`django-filter `__ to provide filtering of results. See the `usage documentation `__ for details on the format for ``filter_fields``. From df3c0bf75b3130825e9644bfd2325059d1191cf6 Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Fri, 21 Apr 2023 21:42:52 +0300 Subject: [PATCH 02/17] Update filtering.rst --- docs/filtering.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/filtering.rst b/docs/filtering.rst index d850b69..95576a0 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -3,7 +3,7 @@ Filtering Graphene integrates with `django-filter `__ to provide filtering of results. -See the `usage documentation `__ +See the `usage documentation `__ for details on the format for ``filter_fields``. This filtering is automatically available when implementing a ``relay.Node``. From a335042dbe5c9def356eee9e20cb1dad41da109c Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Sat, 29 Apr 2023 20:26:05 +0300 Subject: [PATCH 03/17] =?UTF-8?q?=E2=98=82=EF=B8=8F=20v3.0.1=20=E2=98=82?= =?UTF-8?q?=EF=B8=8F?= 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 7a413fc..755ed87 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.0" +__version__ = "3.1.0" __all__ = [ "__version__", From 34cc86063b4d01212d30c6244ae58cde78b6a139 Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Sat, 29 Apr 2023 20:26:39 +0300 Subject: [PATCH 04/17] =?UTF-8?q?=E2=98=82=EF=B8=8F=20v3.0.1=20=E2=98=82?= =?UTF-8?q?=EF=B8=8F?= 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 755ed87..82c4fb3 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.1.0" +__version__ = "3.0.1" __all__ = [ "__version__", From f67c5dbc8cfa6783247ed021b78dbda182614c00 Mon Sep 17 00:00:00 2001 From: Steven DeMartini Date: Sat, 29 Apr 2023 12:01:55 -0700 Subject: [PATCH 05/17] Revert field resolver logic to fix poor query performance This reverts the change to `convert_field_to_djangomodel` introduced in https://github.com/graphql-python/graphene-django/pull/1315 for the reasons discussed here https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857. As mentioned there, without reverting this code, "queries are forced every time an object is resolved, making an exponential number of queries when nesting without any possibility of optimizing". That regression prevented `graphene-django-optimizer` from working with `graphene-django` v3.0.0b9+ (where this change first was published), as discussed in https://github.com/graphql-python/graphene-django/issues/1356#issuecomment-1284718187, https://github.com/tfoxy/graphene-django-optimizer/issues/86, and https://github.com/tfoxy/graphene-django-optimizer/pull/83#issuecomment-1451987397. For now, this marks the two tests that depended on this problematic code as "expected to fail", and perhaps they can be reintroduced if there's a way to support this logic in a way that does not prevent `select_related` and `prefetch_related` query-optimization and introduce nested N+1s. As mentioned here https://github.com/graphql-python/graphene-django/pull/1315#issuecomment-1468594361, this is blocking upgrade to graphene-django v3 for many users, and fixing this would allow many to begin upgrading and contributing to keep graphene-django going. --- graphene_django/converter.py | 21 +-------------------- graphene_django/tests/test_get_queryset.py | 8 ++++++++ 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 386103a..9ad6c9d 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -315,26 +315,7 @@ def convert_field_to_djangomodel(field, registry=None): if not _type: return - class CustomField(Field): - def wrap_resolve(self, parent_resolver): - """ - 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 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 - - return CustomField( + return Field( _type, description=get_django_field_description(field), required=not field.null, diff --git a/graphene_django/tests/test_get_queryset.py b/graphene_django/tests/test_get_queryset.py index 91bdc70..63027b9 100644 --- a/graphene_django/tests/test_get_queryset.py +++ b/graphene_django/tests/test_get_queryset.py @@ -121,6 +121,10 @@ class TestShouldCallGetQuerySetOnForeignKey: assert not result.errors assert result.data == {"reporter": {"firstName": "Jane"}} + # TODO: This test is currently expected to fail because the logic it depended on has been + # removed, due to poor SQL performance and preventing query-optimization (see + # https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857) + @pytest.mark.xfail 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 = """ @@ -291,6 +295,10 @@ class TestShouldCallGetQuerySetOnForeignKeyNode: assert not result.errors assert result.data == {"reporter": {"firstName": "Jane"}} + # TODO: This test is currently expected to fail because the logic it depended on has been + # removed, due to poor SQL performance and preventing query-optimization (see + # https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857) + @pytest.mark.xfail 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 = """ From 9796e93fc7d9cd1d440408f59857ebcca2f0f344 Mon Sep 17 00:00:00 2001 From: Steven DeMartini Date: Mon, 1 May 2023 09:00:30 -0700 Subject: [PATCH 06/17] Remove obsolete tests and add note about rationale --- graphene_django/tests/test_get_queryset.py | 146 +-------------------- 1 file changed, 6 insertions(+), 140 deletions(-) diff --git a/graphene_django/tests/test_get_queryset.py b/graphene_django/tests/test_get_queryset.py index 63027b9..7cbaa54 100644 --- a/graphene_django/tests/test_get_queryset.py +++ b/graphene_django/tests/test_get_queryset.py @@ -16,6 +16,12 @@ class TestShouldCallGetQuerySetOnForeignKey: Check that the get_queryset method is called in both forward and reversed direction of a foreignkey on types. (see issue #1111) + + NOTE: For now, we do not expect this get_queryset method to be called for nested + objects, as the original attempt to do so prevented SQL query-optimization with + `select_related`/`prefetch_related` and caused N+1 queries. See discussions here + https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857 + and here https://github.com/graphql-python/graphene-django/pull/1401. """ @pytest.fixture(autouse=True) @@ -121,73 +127,6 @@ class TestShouldCallGetQuerySetOnForeignKey: assert not result.errors assert result.data == {"reporter": {"firstName": "Jane"}} - # TODO: This test is currently expected to fail because the logic it depended on has been - # removed, due to poor SQL performance and preventing query-optimization (see - # https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857) - @pytest.mark.xfail - 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: """ @@ -294,76 +233,3 @@ class TestShouldCallGetQuerySetOnForeignKeyNode: ) assert not result.errors assert result.data == {"reporter": {"firstName": "Jane"}} - - # TODO: This test is currently expected to fail because the logic it depended on has been - # removed, due to poor SQL performance and preventing query-optimization (see - # https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857) - @pytest.mark.xfail - 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"}}]}, - } From 20a6cecc4ca77b3866073ef66bb5ef63b4beab05 Mon Sep 17 00:00:00 2001 From: Steven DeMartini Date: Tue, 2 May 2023 09:00:22 -0700 Subject: [PATCH 07/17] Add test validating query performance with select_related + prefetch_related This test passes after reverting the `CustomField` resolver change introduced in https://github.com/graphql-python/graphene-django/pull/1315, but fails with that resolver code present. For instance, adding back the resolver code gives a test failure showing: ``` Failed: Expected to perform 2 queries but 11 were done ``` This should ensure there aren't regressions that prevent query-optimization in the future. --- graphene_django/tests/test_fields.py | 153 ++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 3 deletions(-) diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 835de78..8c7b78d 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -1,5 +1,6 @@ import datetime -from django.db.models import Count +import re +from django.db.models import Count, Prefetch import pytest @@ -7,8 +8,12 @@ from graphene import List, NonNull, ObjectType, Schema, String from ..fields import DjangoListField from ..types import DjangoObjectType -from .models import Article as ArticleModel -from .models import Reporter as ReporterModel +from .models import ( + Article as ArticleModel, + Film as FilmModel, + FilmDetails as FilmDetailsModel, + Reporter as ReporterModel, +) class TestDjangoListField: @@ -500,3 +505,145 @@ class TestDjangoListField: assert not result.errors assert result.data == {"reporters": [{"firstName": "Tara"}]} + + def test_select_related_and_prefetch_related_are_respected( + self, django_assert_num_queries + ): + class Article(DjangoObjectType): + class Meta: + model = ArticleModel + fields = ("headline", "editor", "reporter") + + class Film(DjangoObjectType): + class Meta: + model = FilmModel + fields = ("genre", "details") + + class FilmDetail(DjangoObjectType): + class Meta: + model = FilmDetailsModel + fields = ("location",) + + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + fields = ("first_name", "articles", "films") + + class Query(ObjectType): + articles = DjangoListField(Article) + + @staticmethod + def resolve_articles(root, info): + # Optimize for querying associated editors and reporters, and the films and film + # details of those reporters. This is similar to what would happen using a library + # like https://github.com/tfoxy/graphene-django-optimizer for a query like the one + # below (albeit simplified and hardcoded here). + return ArticleModel.objects.select_related( + "editor", "reporter" + ).prefetch_related( + Prefetch( + "reporter__films", + queryset=FilmModel.objects.select_related("details"), + ), + ) + + schema = Schema(query=Query) + + query = """ + query { + articles { + headline + + editor { + firstName + } + + reporter { + firstName + + films { + genre + + details { + location + } + } + } + } + } + """ + + r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") + r2 = ReporterModel.objects.create(first_name="Debra", last_name="Payne") + + ArticleModel.objects.create( + headline="Amazing news", + reporter=r1, + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + editor=r2, + ) + ArticleModel.objects.create( + headline="Not so good news", + reporter=r2, + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + editor=r1, + ) + + film1 = FilmModel.objects.create(genre="ac") + film2 = FilmModel.objects.create(genre="ot") + film3 = FilmModel.objects.create(genre="do") + FilmDetailsModel.objects.create(location="Hollywood", film=film1) + FilmDetailsModel.objects.create(location="Antarctica", film=film3) + r1.films.add(film1, film2) + r2.films.add(film3) + + # We expect 2 queries to be performed based on the above resolver definition: one for all + # articles joined with the reporters model (for associated editors and reporters), and one + # for the films prefetch (which includes its `select_related` JOIN logic in its queryset) + with django_assert_num_queries(2) as captured: + result = schema.execute(query) + + assert not result.errors + assert result.data == { + "articles": [ + { + "headline": "Amazing news", + "editor": {"firstName": "Debra"}, + "reporter": { + "firstName": "Tara", + "films": [ + {"genre": "AC", "details": {"location": "Hollywood"}}, + {"genre": "OT", "details": None}, + ], + }, + }, + { + "headline": "Not so good news", + "editor": {"firstName": "Tara"}, + "reporter": { + "firstName": "Debra", + "films": [ + {"genre": "DO", "details": {"location": "Antarctica"}}, + ], + }, + }, + ] + } + + assert len(captured.captured_queries) == 2 # Sanity-check + + # First we should have queried for all articles in a single query, joining on the reporters + # model (for the editors and reporters ForeignKeys) + assert re.match( + r'SELECT .* "tests_article" INNER JOIN "tests_reporter"', + captured.captured_queries[0]["sql"], + ) + + # Then we should have queried for all of the films of all reporters, joined with the film + # details for each film, using a single query + assert re.match( + r'SELECT .* FROM "tests_film" INNER JOIN "tests_film_reporters" .* LEFT OUTER JOIN "tests_filmdetails"', + captured.captured_queries[1]["sql"], + ) From a8ceca77ed96c46414099c7595449975a19954c8 Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Wed, 3 May 2023 11:45:56 +0300 Subject: [PATCH 08/17] Bump version --- 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 82c4fb3..12408a4 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.1" +__version__ = "3.0.2" __all__ = [ "__version__", From 95a064281869f23fac5b63f3ac11413a0bb2c483 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Wed, 3 May 2023 04:24:32 +0800 Subject: [PATCH 09/17] fix: fix graphiql request failure --- .../static/graphene_django/graphiql.js | 41 +++++++------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js index 106b470..5b9d96d 100644 --- a/graphene_django/static/graphene_django/graphiql.js +++ b/graphene_django/static/graphene_django/graphiql.js @@ -60,40 +60,27 @@ function trueLambda() { return true; }; - var fetcher = GraphiQL.createFetcher({ + var headers = {}; + 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 + } + + var graphQLFetcher = GraphiQL.createFetcher({ url: fetchURL, wsClient: graphqlWs.createClient({ url: subscribeURL, shouldRetry: trueLambda, lazy: true, - }) + }), + headers: headers }) - function graphQLFetcher(graphQLParams, opts) { - if (typeof opts === 'undefined') { - opts = {}; - } - 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 - } - - opts.headers = headers - - return fetcher(graphQLParams, opts) - } - // When the query and variables string is edited, update the URL bar so // that it can be easily shared. function onEditQuery(newQuery) { From c1a22bfd91aa3b298ea8f293b1a9f6ece7a46a8e Mon Sep 17 00:00:00 2001 From: Steven DeMartini Date: Tue, 2 May 2023 10:12:49 -0700 Subject: [PATCH 10/17] Add pre-commit to dev-setup pre-commit is currently configured nicely but hasn't been part of the Makefile setup and isn't mentioned in the contributing notes. This change makes it so that pre-commit is installed as a part of the dev setup, whereas before it had to be manually installed. --- Makefile | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 391c454..29c412b 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ help: .PHONY: dev-setup ## Install development dependencies dev-setup: pip install -e ".[dev]" + python -m pre_commit install .PHONY: tests ## Run unit tests tests: diff --git a/setup.py b/setup.py index d9aefef..cc9770c 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ dev_requires = [ "flake8==5.0.4", "flake8-black==0.3.3", "flake8-bugbear==22.9.11", + "pre-commit", ] + tests_require setup( From af8888f58e1004000ba90adfd42231546c08ccd2 Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Wed, 3 May 2023 13:25:16 +0300 Subject: [PATCH 11/17] Upgrade github actions versions, default python and dev dependencies (#1407) * Use Python 3.10 for deployments on PyPi * Update gh-action-pypi-publish version * Update python version * Update checkout and setup-python versions * Upgrade dev dependencies * fromat examples and few files to follow black new version * Upgrade pytest version --------- Co-authored-by: Firas Kafri --- .github/workflows/deploy.yml | 10 +++++----- .github/workflows/lint.yml | 8 ++++---- .github/workflows/tests.yml | 4 ++-- .pre-commit-config.yaml | 10 +++++----- .../cookbook/ingredients/migrations/0001_initial.py | 1 - .../ingredients/migrations/0002_auto_20161104_0050.py | 1 - .../ingredients/migrations/0003_auto_20181018_1746.py | 1 - .../cookbook/recipes/migrations/0001_initial.py | 1 - .../recipes/migrations/0002_auto_20161104_0106.py | 1 - .../recipes/migrations/0003_auto_20181018_1728.py | 1 - .../cookbook/ingredients/migrations/0001_initial.py | 1 - .../ingredients/migrations/0002_auto_20161104_0050.py | 1 - .../cookbook/recipes/migrations/0001_initial.py | 1 - .../recipes/migrations/0002_auto_20161104_0106.py | 1 - graphene_django/filter/tests/conftest.py | 1 - graphene_django/forms/mutation.py | 2 -- graphene_django/rest_framework/mutation.py | 1 - graphene_django/tests/schema_view.py | 1 - graphene_django/tests/test_query.py | 2 -- setup.py | 10 +++++----- 20 files changed, 21 insertions(+), 38 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 07c0766..a733c03 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,17 +10,17 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.10' - name: Build wheel and source tarball run: | pip install wheel python setup.py sdist bdist_wheel - name: Publish a Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@v1.1.0 + uses: pypa/gh-action-pypi-publish@v1.8.6 with: user: __token__ password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9f1c3ab..8cee90a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2cdc99..31b479e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,9 +14,9 @@ jobs: - django: "3.2" python-version: "3.7" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e64c4e1..adb54c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,8 @@ default_language_version: - python: python3.9 + python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-merge-conflict - id: check-json @@ -16,15 +16,15 @@ repos: - id: trailing-whitespace exclude: README.md - repo: https://github.com/asottile/pyupgrade - rev: v3.2.0 + rev: v3.3.2 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 23.3.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py index 345cadb..23d71e8 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [] 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 00fe255..5f9e7a0 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 @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("ingredients", "0001_initial"), ] 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 8015d1f..e823a2e 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 @@ -4,7 +4,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("ingredients", "0002_auto_20161104_0050"), ] diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py index fceeb9b..c415147 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ 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 0156920..f38bb69 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 @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("recipes", "0001_initial"), ] 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 c54855b..dacdb30 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 @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("recipes", "0002_auto_20161104_0106"), ] diff --git a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py index 345cadb..23d71e8 100644 --- a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py +++ b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [] 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 00fe255..5f9e7a0 100644 --- a/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py +++ b/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("ingredients", "0001_initial"), ] diff --git a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py index fceeb9b..c415147 100644 --- a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py +++ b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ 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 0156920..f38bb69 100644 --- a/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py +++ b/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("recipes", "0001_initial"), ] diff --git a/graphene_django/filter/tests/conftest.py b/graphene_django/filter/tests/conftest.py index a11831c..f8a65d7 100644 --- a/graphene_django/filter/tests/conftest.py +++ b/graphene_django/filter/tests/conftest.py @@ -87,7 +87,6 @@ def Query(EventType): events = DjangoFilterConnectionField(EventType) def resolve_events(self, info, **kwargs): - events = [ Event(name="Live Show", tags=["concert", "music", "rock"]), Event(name="Musical", tags=["movie", "music"]), diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 3d59464..40d1d3c 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -82,7 +82,6 @@ class DjangoFormMutation(BaseDjangoFormMutation): def __init_subclass_with_meta__( cls, form_class=None, only_fields=(), exclude_fields=(), **options ): - if not form_class: raise Exception("form_class is required for DjangoFormMutation") @@ -129,7 +128,6 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): exclude_fields=(), **options, ): - if not form_class: raise Exception("form_class is required for DjangoModelFormMutation") diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index c01d915..4062a44 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -72,7 +72,6 @@ class SerializerMutation(ClientIDMutation): _meta=None, **options ): - if not serializer_class: raise Exception("serializer_class is required for the SerializerMutation") diff --git a/graphene_django/tests/schema_view.py b/graphene_django/tests/schema_view.py index 8ed2ecf..4d538ba 100644 --- a/graphene_django/tests/schema_view.py +++ b/graphene_django/tests/schema_view.py @@ -5,7 +5,6 @@ from .mutations import PetFormMutation, PetMutation class QueryRoot(ObjectType): - thrower = graphene.String(required=True) request = graphene.String(required=True) test = graphene.String(who=graphene.String()) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index df339d8..383ff2e 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -780,7 +780,6 @@ def test_should_query_promise_connectionfields(): def test_should_query_connectionfields_with_last(): - r = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) @@ -818,7 +817,6 @@ def test_should_query_connectionfields_with_last(): def test_should_query_connectionfields_with_manager(): - r = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) diff --git a/setup.py b/setup.py index cc9770c..96da8ff 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ rest_framework_require = ["djangorestframework>=3.6.3"] tests_require = [ - "pytest>=7.1.3", + "pytest>=7.3.1", "pytest-cov", "pytest-random-order", "coveralls", @@ -26,10 +26,10 @@ tests_require = [ dev_requires = [ - "black==22.8.0", - "flake8==5.0.4", - "flake8-black==0.3.3", - "flake8-bugbear==22.9.11", + "black==23.3.0", + "flake8==6.0.0", + "flake8-black==0.3.6", + "flake8-bugbear==23.3.23", "pre-commit", ] + tests_require From 8540a9332cda7bcbac92643cacbb5146f35080e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Clgen=20Sar=C4=B1kavak?= Date: Thu, 4 May 2023 15:19:24 +0300 Subject: [PATCH 12/17] Add support for Python 3.11 (#1365) * Add support for Python 3.11 * Fix Python 3.11 compatibility matrix * Add temporary fix for default enum description --------- Co-authored-by: Firas Kafri --- .github/workflows/deploy.yml | 4 ++-- .github/workflows/lint.yml | 4 ++-- .github/workflows/tests.yml | 2 ++ .pre-commit-config.yaml | 2 +- graphene_django/converter.py | 7 ++++++- setup.py | 1 + tox.ini | 2 ++ 7 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a733c03..139c6f6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,10 +11,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Build wheel and source tarball run: | pip install wheel diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8cee90a..bfafa67 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,10 +8,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31b479e..2c5b755 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,6 +13,8 @@ jobs: include: - django: "3.2" python-version: "3.7" + - django: "4.1" + python-version: "3.11" steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index adb54c7..9214d35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.10 + python: python3.11 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 9ad6c9d..375d683 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -96,7 +96,12 @@ def convert_choices_to_named_enum_with_descriptions(name, choices): def description(self): return str(named_choices_descriptions[self.name]) - return_type = Enum(name, list(named_choices), type=EnumWithDescriptionsType) + return_type = Enum( + name, + list(named_choices), + type=EnumWithDescriptionsType, + description="An enumeration.", # Temporary fix until https://github.com/graphql-python/graphene/pull/1502 is merged + ) return return_type diff --git a/setup.py b/setup.py index 96da8ff..37b57a8 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ setup( "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: Django", "Framework :: Django :: 3.2", diff --git a/tox.ini b/tox.ini index 285d046..e186f30 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = py{37,38,39,310}-django32, py{38,39,310}-django{40,41,main}, + py311-django{41,main} pre-commit [gh-actions] @@ -10,6 +11,7 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 [gh-actions:env] DJANGO = From 52f992183fcdbb841930357be086b2488f9d8472 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Fri, 5 May 2023 03:06:10 +0800 Subject: [PATCH 13/17] Add GraphiQL Explorer plugin (#1397) --- .../static/graphene_django/graphiql.js | 52 +++++++++++++------ .../templates/graphene/graphiql.html | 3 ++ graphene_django/views.py | 5 ++ 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js index 5b9d96d..901c991 100644 --- a/graphene_django/static/graphene_django/graphiql.js +++ b/graphene_django/static/graphene_django/graphiql.js @@ -6,6 +6,7 @@ React, ReactDOM, graphqlWs, + GraphiQLPluginExplorer, fetch, history, location, @@ -98,24 +99,44 @@ function updateURL() { history.replaceState(null, null, locationQuery(parameters)); } - var options = { - fetcher: graphQLFetcher, - onEditQuery: onEditQuery, - onEditVariables: onEditVariables, - onEditOperationName: onEditOperationName, - isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, - shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders, - query: parameters.query, - }; - if (parameters.variables) { - options.variables = parameters.variables; - } - if (parameters.operation_name) { - options.operationName = parameters.operation_name; + + function GraphiQLWithExplorer() { + var [query, setQuery] = React.useState(parameters.query); + + function handleQuery(query) { + setQuery(query); + onEditQuery(query); + } + + var explorerPlugin = GraphiQLPluginExplorer.useExplorerPlugin({ + query: query, + onEdit: handleQuery, + }); + + var options = { + fetcher: graphQLFetcher, + plugins: [explorerPlugin], + defaultEditorToolsVisibility: true, + onEditQuery: handleQuery, + onEditVariables: onEditVariables, + onEditOperationName: onEditOperationName, + isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, + shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders, + query: query, + }; + if (parameters.variables) { + options.variables = parameters.variables; + } + if (parameters.operation_name) { + options.operationName = parameters.operation_name; + } + + return React.createElement(GraphiQL, options); } + // Render into the body. ReactDOM.render( - React.createElement(GraphiQL, options), + React.createElement(GraphiQLWithExplorer), document.getElementById("editor"), ); })( @@ -126,6 +147,7 @@ window.React, window.ReactDOM, window.graphqlWs, + window.GraphiQLPluginExplorer, window.fetch, window.history, window.location, diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index 8fb00c4..ddff8fc 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -36,6 +36,9 @@ add "&raw" to the end of the URL within a browser. +
diff --git a/graphene_django/views.py b/graphene_django/views.py index b29aeed..d4d98b7 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -76,6 +76,9 @@ class GraphQLView(View): "sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw=" ) + graphiql_plugin_explorer_version = "0.1.15" + graphiql_plugin_explorer_sri = "sha256-3hUuhBXdXlfCj6RTeEkJFtEh/kUG+TCDASFpFPLrzvE=" + schema = None graphiql = False middleware = None @@ -158,6 +161,8 @@ class GraphQLView(View): graphiql_css_sri=self.graphiql_css_sri, subscriptions_transport_ws_version=self.subscriptions_transport_ws_version, subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri, + graphiql_plugin_explorer_version=self.graphiql_plugin_explorer_version, + graphiql_plugin_explorer_sri=self.graphiql_plugin_explorer_sri, # The SUBSCRIPTION_PATH setting. subscription_path=self.subscription_path, # GraphiQL headers tab, From ce7492b5aef1770e7082a0be453d7a16a25ac6ce Mon Sep 17 00:00:00 2001 From: Firas Kafri <3097061+firaskafri@users.noreply.github.com> Date: Thu, 4 May 2023 23:46:15 +0300 Subject: [PATCH 14/17] Delete README.rst --- README.rst | 122 ----------------------------------------------------- 1 file changed, 122 deletions(-) delete mode 100644 README.rst diff --git a/README.rst b/README.rst deleted file mode 100644 index f8aa205..0000000 --- a/README.rst +++ /dev/null @@ -1,122 +0,0 @@ -Please read -`UPGRADE-v2.0.md `__ -to learn how to upgrade to Graphene ``2.0``. - --------------- - -|Graphene Logo| Graphene-Django |Build Status| |PyPI version| |Coverage Status| -=============================================================================== - -A `Django `__ integration for -`Graphene `__. - - -Documentation -------------- - -`Visit the documentation to get started! `__ - -Quickstart ----------- - -For installing graphene, just run this command in your shell - -.. code:: bash - - pip install "graphene-django>=3" - -Settings -~~~~~~~~ - -.. code:: python - - INSTALLED_APPS = ( - # ... - 'graphene_django', - ) - - GRAPHENE = { - 'SCHEMA': 'app.schema.schema' # Where your Graphene schema lives - } - -Urls -~~~~ - -We need to set up a ``GraphQL`` endpoint in our Django app, so we can -serve the queries. - -.. code:: python - - from django.conf.urls import url - from graphene_django.views import GraphQLView - - urlpatterns = [ - # ... - url(r'^graphql$', GraphQLView.as_view(graphiql=True)), - ] - -Examples --------- - -Here is a simple Django model: - -.. code:: python - - from django.db import models - - class UserModel(models.Model): - name = models.CharField(max_length=100) - last_name = models.CharField(max_length=100) - -To create a GraphQL schema for it you simply have to write the -following: - -.. code:: python - - from graphene_django import DjangoObjectType - import graphene - - class User(DjangoObjectType): - class Meta: - model = UserModel - - class Query(graphene.ObjectType): - users = graphene.List(User) - - @graphene.resolve_only_args - def resolve_users(self): - return UserModel.objects.all() - - schema = graphene.Schema(query=Query) - -Then you can simply query the schema: - -.. code:: python - - query = ''' - query { - users { - name, - lastName - } - } - ''' - result = schema.execute(query) - -To learn more check out the following `examples `__: - -- **Schema with Filtering**: `Cookbook example `__ -- **Relay Schema**: `Starwars Relay example `__ - -Contributing ------------- - -See `CONTRIBUTING.md `__. - -.. |Graphene Logo| image:: http://graphene-python.org/favicon.png -.. |Build Status| image:: https://github.com/graphql-python/graphene-django/workflows/Tests/badge.svg - :target: https://github.com/graphql-python/graphene-django/actions -.. |PyPI version| image:: https://badge.fury.io/py/graphene-django.svg - :target: https://badge.fury.io/py/graphene-django -.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene-django/badge.svg?branch=master&service=github - :target: https://coveralls.io/github/graphql-python/graphene-django?branch=master From 6f13d28b6ea58a37c97f17d4c63348c22c87861f Mon Sep 17 00:00:00 2001 From: Firas Kafri <3097061+firaskafri@users.noreply.github.com> Date: Thu, 4 May 2023 23:54:09 +0300 Subject: [PATCH 15/17] Update README.md (#1408) * Update README.md * Delete README.rst * Update long_description source to be from README.md --- README.md | 155 ++++++++++++++++++++++++++++++++---------------------- setup.py | 2 +- 2 files changed, 92 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 6f06ccc..2e3531f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # ![Graphene Logo](http://graphene-python.org/favicon.png) Graphene-Django - -A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/). - [![build][build-image]][build-url] [![pypi][pypi-image]][pypi-url] [![Anaconda-Server Badge][conda-image]][conda-url] @@ -17,107 +14,137 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra [conda-image]: https://img.shields.io/conda/vn/conda-forge/graphene-django.svg [conda-url]: https://anaconda.org/conda-forge/graphene-django -[💬 Join the community on Slack](https://join.slack.com/t/graphenetools/shared_invite/enQtOTE2MDQ1NTg4MDM1LTA4Nzk0MGU0NGEwNzUxZGNjNDQ4ZjAwNDJjMjY0OGE1ZDgxZTg4YjM2ZTc4MjE2ZTAzZjE2ZThhZTQzZTkyMmM) +Graphene-Django is an open-source library that provides seamless integration between Django, a high-level Python web framework, and Graphene, a library for building GraphQL APIs. The library allows developers to create GraphQL APIs in Django quickly and efficiently while maintaining a high level of performance. -## Documentation +## Features -[Visit the documentation to get started!](https://docs.graphene-python.org/projects/django/en/latest/) +* Seamless integration with Django models +* Automatic generation of GraphQL schema +* Integration with Django's authentication and permission system +* Easy querying and filtering of data +* Support for Django's pagination system +* Compatible with Django's form and validation system +* Extensive documentation and community support -## Quickstart +## Installation -For installing graphene, just run this command in your shell +To install Graphene-Django, run the following command: -```bash -pip install "graphene-django>=3" +``` +pip install graphene-django ``` -### Settings +## Configuration + +After installation, add 'graphene_django' to your Django project's `INSTALLED_APPS` list and define the GraphQL schema in your project's settings: ```python -INSTALLED_APPS = ( +INSTALLED_APPS = [ # ... - 'django.contrib.staticfiles', # Required for GraphiQL 'graphene_django', -) +] GRAPHENE = { - 'SCHEMA': 'app.schema.schema' # Where your Graphene schema lives + 'SCHEMA': 'myapp.schema.schema' } ``` -### Urls +## Usage -We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries. +To use Graphene-Django, create a `schema.py` file in your Django app directory and define your GraphQL types and queries: ```python -from django.urls import path -from graphene_django.views import GraphQLView - -urlpatterns = [ - # ... - path('graphql/', GraphQLView.as_view(graphiql=True)), -] -``` - -## Examples - -Here is a simple Django model: - -```python -from django.db import models - -class UserModel(models.Model): - name = models.CharField(max_length=100) - last_name = models.CharField(max_length=100) -``` - -To create a GraphQL schema for it you simply have to write the following: - -```python -from graphene_django import DjangoObjectType import graphene +from graphene_django import DjangoObjectType +from .models import MyModel -class User(DjangoObjectType): +class MyModelType(DjangoObjectType): class Meta: - model = UserModel + model = MyModel class Query(graphene.ObjectType): - users = graphene.List(User) + mymodels = graphene.List(MyModelType) - def resolve_users(self, info): - return UserModel.objects.all() + def resolve_mymodels(self, info, **kwargs): + return MyModel.objects.all() schema = graphene.Schema(query=Query) ``` -Then you can query the schema: +Then, expose the GraphQL API in your Django project's `urls.py` file: ```python -query = ''' - query { - users { - name, - lastName - } - } -''' -result = schema.execute(query) +from django.urls import path +from graphene_django.views import GraphQLView +from . import schema + +urlpatterns = [ + # ... + path('graphql/', GraphQLView.as_view(graphiql=True)), # Given that schema path is defined in GRAPHENE['SCHEMA'] in your settings.py +] ``` -To learn more check out the following [examples](examples/): +## Testing -* **Schema with Filtering**: [Cookbook example](examples/cookbook) -* **Relay Schema**: [Starwars Relay example](examples/starwars) +Graphene-Django provides support for testing GraphQL APIs using Django's test client. To create tests, create a `tests.py` file in your Django app directory and write your test cases: +```python +from django.test import TestCase +from graphene_django.utils.testing import GraphQLTestCase +from . import schema -## GraphQL testing clients - - [Firecamp](https://firecamp.io/graphql) - - [GraphiQL](https://github.com/graphql/graphiql) +class MyModelAPITestCase(GraphQLTestCase): + GRAPHENE_SCHEMA = schema.schema + def test_query_all_mymodels(self): + response = self.query( + ''' + query { + mymodels { + id + name + } + } + ''' + ) + + self.assertResponseNoErrors(response) + self.assertEqual(len(response.data['mymodels']), MyModel.objects.count()) +``` ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) +Contributions to Graphene-Django are always welcome! To get started, check the repository's [issue tracker](https://github.com/graphql-python/graphene-django/issues) and [contribution guidelines](https://github.com/graphql-python/graphene-django/blob/master/CONTRIBUTING.md). + +## License + +Graphene-Django is released under the [MIT License](https://github.com/graphql-python/graphene-django/blob/master/LICENSE). + +## Resources + +* [Official GitHub Repository](https://github.com/graphql-python/graphene-django) +* [Graphene Documentation](http://docs.graphene-python.org/en/latest/) +* [Django Documentation](https://docs.djangoproject.com/en/stable/) +* [GraphQL Specification](https://spec.graphql.org/) +* [GraphiQL](https://github.com/graphql/graphiql) - An in-browser IDE for exploring GraphQL APIs +* [Graphene-Django Community](https://spectrum.chat/graphene) - Join the community to discuss questions and share ideas related to Graphene-Django + +## Tutorials and Examples + +* [Official Graphene-Django Tutorial](https://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/) +* [Building a GraphQL API with Django and Graphene-Django](https://www.howtographql.com/graphql-python/0-introduction/) +* [Real-world example: Django, Graphene, and Relay](https://github.com/graphql-python/swapi-graphene) + +## Related Projects + +* [Graphene](https://github.com/graphql-python/graphene) - A library for building GraphQL APIs in Python +* [Graphene-SQLAlchemy](https://github.com/graphql-python/graphene-sqlalchemy) - Integration between Graphene and SQLAlchemy, an Object Relational Mapper (ORM) for Python +* [Graphene-File-Upload](https://github.com/lmcgartland/graphene-file-upload) - A package providing an Upload scalar for handling file uploads in Graphene +* [Graphene-Subscriptions](https://github.com/graphql-python/graphene-subscriptions) - A package for adding real-time subscriptions to Graphene-based GraphQL APIs + +## Support + +If you encounter any issues or have questions regarding Graphene-Django, feel free to [submit an issue](https://github.com/graphql-python/graphene-django/issues/new) on the official GitHub repository. You can also ask for help and share your experiences with the Graphene-Django community on [💬 Discord](https://discord.gg/Fftt273T79) ## Release Notes diff --git a/setup.py b/setup.py index 37b57a8..0fc0f88 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ setup( name="graphene-django", version=version, description="Graphene Django integration", - long_description=open("README.rst").read(), + long_description=open("README.md").read(), url="https://github.com/graphql-python/graphene-django", author="Syrus Akbary", author_email="me@syrusakbary.com", From 09f9b6d2f19eeec49b26d1105c0b5f34178e935a Mon Sep 17 00:00:00 2001 From: shukryzablah <28762146+shukryzablah@users.noreply.github.com> Date: Fri, 5 May 2023 06:04:22 -0400 Subject: [PATCH 16/17] Remove redundant call to validate (#1393) * Remove redundant call to validate The call to `validate` in the django view is redundant with the validation call in graphql-core. * Remove whitespace --------- Co-authored-by: Firas K <3097061+firaskafri@users.noreply.github.com> --- graphene_django/views.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index d4d98b7..bdc0fdb 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -9,7 +9,7 @@ from django.shortcuts import render from django.utils.decorators import method_decorator 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 import OperationType, get_operation_ast, parse from graphql.error import GraphQLError from graphql.execution import ExecutionResult @@ -308,11 +308,6 @@ class GraphQLView(View): ), ) ) - - validation_errors = validate(self.schema.graphql_schema, document) - if validation_errors: - return ExecutionResult(data=None, errors=validation_errors) - try: extra_options = {} if self.execution_context_class: From 72a3700856d19c056e515c4f51050acaadd7c574 Mon Sep 17 00:00:00 2001 From: Firas Kafri <3097061+firaskafri@users.noreply.github.com> Date: Fri, 5 May 2023 13:04:47 +0300 Subject: [PATCH 17/17] Update Development Status calassifier (#1409) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0fc0f88..7407b62 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ setup( author_email="me@syrusakbary.com", license="MIT", classifiers=[ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", "Programming Language :: Python :: 3",