Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Async Django #1394

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6a5b28d
Resolve DjangoObjectType getNode when in an async context
jaw9c Mar 30, 2023
74998af
Support Django Connection resolving in an async context
jaw9c Mar 30, 2023
f04f0d3
Support foriegn key connections running async
jaw9c Mar 30, 2023
e78fb86
handle regualr django lists
jaw9c Mar 30, 2023
28846f9
drop in an async view
jaw9c Mar 30, 2023
7ddaf9f
Handle coroutine results from resolvers in connections and filter con…
jaw9c Mar 31, 2023
ebbc578
Strange scope
jaw9c Mar 31, 2023
66938e9
async hates csrf
jaw9c Mar 31, 2023
1b2d5e0
handle async serlizer mutations
jaw9c Mar 31, 2023
0a84a6e
Handle async get_node
jaw9c Apr 2, 2023
64d311d
Copy tests for query to test async execution
jaw9c Apr 2, 2023
bdb8e84
Update cookbook for async testing
jaw9c May 4, 2023
4d5132d
Remove tests for now
jaw9c May 4, 2023
4e5862f
Add logging of errors in execution
jaw9c May 4, 2023
c10753d
most recent changes
jaw9c May 4, 2023
e9d5e88
Handle the default django list field and test the async execution of …
jaw9c May 5, 2023
76eeea4
Update tests for queries
jaw9c May 5, 2023
c501fdb
swap back to python 3.11
jaw9c May 5, 2023
58b92e6
linting
jaw9c May 5, 2023
791209f
Rejig concept to use middleware
jaw9c May 9, 2023
b69476f
improve async detection
jaw9c May 9, 2023
b134ab0
Handle custom Djangoconnectionresolvers
jaw9c May 9, 2023
8c068fb
Update to pull out mutations and ensure that DjangoFields don't get d…
jaw9c May 10, 2023
d3f8fcf
handle connections without middleware
jaw9c May 16, 2023
930248f
Refactor out async helper functions
jaw9c May 16, 2023
c27dd6c
cleanup middleware
jaw9c May 16, 2023
fba274d
refactor middleware
jaw9c May 16, 2023
84ba7d7
Remove unused path
jaw9c May 16, 2023
848536e
Clean up wrapped list resolver
jaw9c May 16, 2023
2659d67
updates tests
jaw9c May 16, 2023
c16276c
Merge branch 'main' into support-async
jaw9c May 16, 2023
37ebd63
follow main removing validate call
jaw9c May 16, 2023
5a19111
Fix graphiql for async
pcraciunoiu May 25, 2023
2ae927f
Merge branch 'main' into support-async
firaskafri Aug 10, 2023
28bc858
resolve conflicts and fix format
firaskafri Aug 10, 2023
45fb299
Fix bug when running sync view under asgi
jaw9c Aug 16, 2023
b35f3b0
Merge branch 'main' into support-async
firaskafri Feb 9, 2024
c2d601c
Fix newline
firaskafri Feb 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Handle the default django list field and test the async execution of …
…the fields
jaw9c committed May 5, 2023
commit e9d5e88ea25b68c57c4a07a576010ea60f2dbfbe
52 changes: 39 additions & 13 deletions graphene_django/fields.py
Original file line number Diff line number Diff line change
@@ -53,20 +53,41 @@ def get_manager(self):
def list_resolver(
django_object_type, resolver, default_manager, root, info, **args
):
queryset = maybe_queryset(resolver(root, info, **args))
iterable = resolver(root, info, **args)

if info.is_awaitable(iterable):

async def resolve_list_async(iterable):
queryset = maybe_queryset(await iterable)
if queryset is None:
queryset = maybe_queryset(default_manager)

if isinstance(queryset, QuerySet):
# Pass queryset to the DjangoObjectType get_queryset method
queryset = maybe_queryset(
await sync_to_async(django_object_type.get_queryset)(
queryset, info
)
)

return await sync_to_async(list)(queryset)

return resolve_list_async(iterable)

queryset = maybe_queryset(iterable)
if queryset is None:
queryset = maybe_queryset(default_manager)

if isinstance(queryset, QuerySet):
# Pass queryset to the DjangoObjectType get_queryset method
queryset = maybe_queryset(django_object_type.get_queryset(queryset, info))

try:
try:
get_running_loop()
except RuntimeError:
pass
pass
else:
return queryset.aiterator()
return sync_to_async(list)(queryset)

return queryset

@@ -238,34 +259,39 @@ def connection_resolver(
# or a resolve_foo (does not accept queryset)

iterable = resolver(root, info, **args)

if info.is_awaitable(iterable):

async def resolve_connection_async(iterable):
iterable = await iterable
if iterable is None:
iterable = default_manager
## This could also be async
iterable = queryset_resolver(connection, iterable, info, args)

if info.is_awaitable(iterable):
iterable = await iterable

return await sync_to_async(cls.resolve_connection)(connection, args, iterable, max_limit=max_limit)

return await sync_to_async(cls.resolve_connection)(
connection, args, iterable, max_limit=max_limit
)

return resolve_connection_async(iterable)

if iterable is None:
iterable = default_manager
# thus the iterable gets refiltered by resolve_queryset
# but iterable might be promise
iterable = queryset_resolver(connection, iterable, info, args)

try:
try:
get_running_loop()
except RuntimeError:
pass
pass
else:
return sync_to_async(cls.resolve_connection)(connection, args, iterable, max_limit=max_limit)

return sync_to_async(cls.resolve_connection)(
connection, args, iterable, max_limit=max_limit
)

return cls.resolve_connection(connection, args, iterable, max_limit=max_limit)

6 changes: 6 additions & 0 deletions graphene_django/tests/async_test_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from asgiref.sync import async_to_sync


def assert_async_result_equal(schema, query, result):
async_result = async_to_sync(schema.execute_async)(query)
assert async_result == result
139 changes: 139 additions & 0 deletions graphene_django/tests/test_fields.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import re
from django.db.models import Count, Prefetch
from asgiref.sync import sync_to_async, async_to_sync

import pytest

@@ -14,6 +15,7 @@
FilmDetails as FilmDetailsModel,
Reporter as ReporterModel,
)
from .async_test_helper import assert_async_result_equal


class TestDjangoListField:
@@ -75,6 +77,7 @@ class Query(ObjectType):

result = schema.execute(query)

assert_async_result_equal(schema, query, result)
assert not result.errors
assert result.data == {
"reporters": [{"firstName": "Tara"}, {"firstName": "Debra"}]
@@ -102,6 +105,7 @@ class Query(ObjectType):
result = schema.execute(query)
assert not result.errors
assert result.data == {"reporters": []}
assert_async_result_equal(schema, query, result)

ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
@@ -112,6 +116,7 @@ class Query(ObjectType):
assert result.data == {
"reporters": [{"firstName": "Tara"}, {"firstName": "Debra"}]
}
assert_async_result_equal(schema, query, result)

def test_override_resolver(self):
class Reporter(DjangoObjectType):
@@ -139,6 +144,37 @@ def resolve_reporters(_, info):
ReporterModel.objects.create(first_name="Debra", last_name="Payne")

result = schema.execute(query)
assert not result.errors
assert result.data == {"reporters": [{"firstName": "Tara"}]}

def test_override_resolver_async_execution(self):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name",)

class Query(ObjectType):
reporters = DjangoListField(Reporter)

@staticmethod
@sync_to_async
def resolve_reporters(_, info):
return ReporterModel.objects.filter(first_name="Tara")

schema = Schema(query=Query)

query = """
query {
reporters {
firstName
}
}
"""

ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")

result = async_to_sync(schema.execute_async)(query)

assert not result.errors
assert result.data == {"reporters": [{"firstName": "Tara"}]}
@@ -203,6 +239,7 @@ class Query(ObjectType):
{"firstName": "Debra", "articles": []},
]
}
assert_async_result_equal(schema, query, result)

def test_override_resolver_nested_list_field(self):
class Article(DjangoObjectType):
@@ -261,6 +298,7 @@ class Query(ObjectType):
{"firstName": "Debra", "articles": []},
]
}
assert_async_result_equal(schema, query, result)

def test_get_queryset_filter(self):
class Reporter(DjangoObjectType):
@@ -306,6 +344,7 @@ def resolve_reporters(_, info):

assert not result.errors
assert result.data == {"reporters": [{"firstName": "Tara"}]}
assert_async_result_equal(schema, query, result)

def test_resolve_list(self):
"""Resolving a plain list should work (and not call get_queryset)"""
@@ -354,6 +393,55 @@ def resolve_reporters(_, info):
assert not result.errors
assert result.data == {"reporters": [{"firstName": "Debra"}]}

def test_resolve_list_async(self):
"""Resolving a plain list should work (and not call get_queryset) when running under async"""

class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name", "articles")

@classmethod
def get_queryset(cls, queryset, info):
# Only get reporters with at least 1 article
return queryset.annotate(article_count=Count("articles")).filter(
article_count__gt=0
)

class Query(ObjectType):
reporters = DjangoListField(Reporter)

@staticmethod
@sync_to_async
def resolve_reporters(_, info):
return [ReporterModel.objects.get(first_name="Debra")]

schema = Schema(query=Query)

query = """
query {
reporters {
firstName
}
}
"""

r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
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=r1,
)

result = async_to_sync(schema.execute_async)(query)

assert not result.errors
assert result.data == {"reporters": [{"firstName": "Debra"}]}

def test_get_queryset_foreign_key(self):
class Article(DjangoObjectType):
class Meta:
@@ -413,6 +501,7 @@ class Query(ObjectType):
{"firstName": "Debra", "articles": []},
]
}
assert_async_result_equal(schema, query, result)

def test_resolve_list_external_resolver(self):
"""Resolving a plain list from external resolver should work (and not call get_queryset)"""
@@ -461,6 +550,54 @@ class Query(ObjectType):
assert not result.errors
assert result.data == {"reporters": [{"firstName": "Debra"}]}

def test_resolve_list_external_resolver_async(self):
"""Resolving a plain list from external resolver should work (and not call get_queryset)"""

class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name", "articles")

@classmethod
def get_queryset(cls, queryset, info):
# Only get reporters with at least 1 article
return queryset.annotate(article_count=Count("articles")).filter(
article_count__gt=0
)

@sync_to_async
def resolve_reporters(_, info):
return [ReporterModel.objects.get(first_name="Debra")]

class Query(ObjectType):
reporters = DjangoListField(Reporter, resolver=resolve_reporters)

schema = Schema(query=Query)

query = """
query {
reporters {
firstName
}
}
"""

r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
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=r1,
)

result = async_to_sync(schema.execute_async)(query)

assert not result.errors
assert result.data == {"reporters": [{"firstName": "Debra"}]}

def test_get_queryset_filter_external_resolver(self):
class Reporter(DjangoObjectType):
class Meta:
@@ -505,6 +642,7 @@ class Query(ObjectType):

assert not result.errors
assert result.data == {"reporters": [{"firstName": "Tara"}]}
assert_async_result_equal(schema, query, result)

def test_select_related_and_prefetch_related_are_respected(
self, django_assert_num_queries
@@ -647,3 +785,4 @@ def resolve_articles(root, info):
r'SELECT .* FROM "tests_film" INNER JOIN "tests_film_reporters" .* LEFT OUTER JOIN "tests_filmdetails"',
captured.captured_queries[1]["sql"],
)
assert_async_result_equal(schema, query, result)