From ee50031cf6e04d1c816a67f287fdfa5546054dd6 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Wed, 10 Apr 2019 11:28:33 -0400 Subject: [PATCH 1/5] Add DataLoaderField --- graphene_django/fields.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index e3129c6..5de947f 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,13 +1,11 @@ from functools import partial from django.db.models.query import QuerySet - -from promise import Promise - from graphene.types import Field, List from graphene.relay import ConnectionField, PageInfo from graphene.utils.get_unbound_function import get_unbound_function from graphql_relay.connection.arrayconnection import connection_from_list_slice +from promise import Promise from .settings import graphene_settings from .utils import maybe_queryset, auth_resolver @@ -170,3 +168,27 @@ class DjangoField(Field): return partial(get_unbound_function(self.permissions_resolver), parent_resolver, self.permissions, None, None, True) return parent_resolver + + +class DataLoaderField(DjangoField): + """Class to manage access to dataloader when resolve the field""" + + def __init__(self, data_loader, source_loader, type, *args, **kwargs): + """ + Initialization of dataloader to resolve field + :param data_loader: dataloader to resolve field + :param source_loader: field to obtain the key for dataloading + :param kwargs: Extra arguments + """ + self.data_loader = data_loader + self.source_loader = source_loader + + super(DataLoaderField, self).__init__(type, *args, **kwargs) + + # If no resolver is explicitly provided, use dataloader + self.resolver = self.resolver or self.resolver_data_loader + + def resolver_data_loader(self, root, info, *args): + """Resolve field through dataloader""" + source_loader = getattr(root, self.source_loader) + return self.data_loader.load(source_loader) From 581dbe7416b52c8810fb19f45e58d359be7f1a42 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Wed, 10 Apr 2019 11:28:40 -0400 Subject: [PATCH 2/5] Add tests to DataLoaderField --- graphene_django/tests/test_fields.py | 71 +++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index e0478bd..8abd9b3 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -1,15 +1,26 @@ +from mock import mock from unittest import TestCase from django.core.exceptions import PermissionDenied -from graphene_django.fields import DjangoField +from graphene_django.fields import DjangoField, DataLoaderField +from promise.dataloader import DataLoader +from promise import Promise class MyInstance(object): value = "value" + key = 1 def resolver(self): return "resolver method" +def batch_load_fn(keys): + return Promise.all(keys) + + +data_loader = DataLoader(batch_load_fn=batch_load_fn) + + class PermissionFieldTests(TestCase): def test_permission_field(self): @@ -21,12 +32,9 @@ class PermissionFieldTests(TestCase): def has_perm(self, perm): return perm == 'perm2' - class Info(object): - class Context(object): - user = Viewer() - context = Context() + info = mock.Mock(context=mock.Mock(user=Viewer())) - self.assertEqual(resolver(MyInstance(), Info()), MyInstance().resolver()) + self.assertEqual(resolver(MyInstance(), info), MyInstance().resolver()) def test_permission_field_without_permission(self): MyType = object() @@ -37,10 +45,51 @@ class PermissionFieldTests(TestCase): def has_perm(self, perm): return False - class Info(object): - class Context(object): - user = Viewer() - context = Context() + info = mock.Mock(context=mock.Mock(user=Viewer())) with self.assertRaises(PermissionDenied): - resolver(MyInstance(), Info()) + resolver(MyInstance(), info) + + +class DataLoaderFieldTests(TestCase): + + def test_dataloaderfield(self): + MyType = object() + data_loader_field = DataLoaderField(data_loader=data_loader, source_loader='key', type=MyType) + + resolver = data_loader_field.get_resolver(None) + instance = MyInstance() + + self.assertEqual(resolver(instance, None).get(), instance.key) + + def test_dataloaderfield_permissions(self): + MyType = object() + data_loader_field = DataLoaderField(data_loader=data_loader, source_loader='key', type=MyType, + permissions=['perm1', 'perm2']) + + resolver = data_loader_field.get_resolver(None) + instance = MyInstance() + + class Viewer(object): + def has_perm(self, perm): + return perm == 'perm2' + + info = mock.Mock(context=mock.Mock(user=Viewer())) + + self.assertEqual(resolver(instance, info).get(), instance.key) + + def test_dataloaderfield_without_permissions(self): + MyType = object() + data_loader_field = DataLoaderField(data_loader=data_loader, source_loader='key', type=MyType, + permissions=['perm1', 'perm2']) + + resolver = data_loader_field.get_resolver(None) + instance = MyInstance() + + class Viewer(object): + def has_perm(self, perm): + return False + + info = mock.Mock(context=mock.Mock(user=Viewer())) + with self.assertRaises(PermissionDenied): + resolver(instance, info) From 9caa57a7ff1d4852dff640b07c3b1a9772e55e74 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Wed, 10 Apr 2019 14:16:08 -0400 Subject: [PATCH 3/5] Accept source_loader from kwargs in DataLoaderField resolver --- graphene_django/fields.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 5de947f..2de0659 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -188,7 +188,10 @@ class DataLoaderField(DjangoField): # If no resolver is explicitly provided, use dataloader self.resolver = self.resolver or self.resolver_data_loader - def resolver_data_loader(self, root, info, *args): + def resolver_data_loader(self, root, info, *args, **kwargs): """Resolve field through dataloader""" - source_loader = getattr(root, self.source_loader) + if root: + source_loader = getattr(root, self.source_loader) + else: + source_loader = kwargs.get(self.source_loader) return self.data_loader.load(source_loader) From 912888935358c7823266936a0866654451098d7a Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 11 Apr 2019 09:03:17 -0400 Subject: [PATCH 4/5] Add load-many support --- graphene_django/fields.py | 17 +++++++++----- graphene_django/tests/test_fields.py | 33 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 2de0659..c9b4942 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -171,17 +171,19 @@ class DjangoField(Field): class DataLoaderField(DjangoField): - """Class to manage access to dataloader when resolve the field""" + """Class to manage access to data-loader when resolve the field""" - def __init__(self, data_loader, source_loader, type, *args, **kwargs): + def __init__(self, type, data_loader, source_loader, load_many=False, *args, **kwargs): """ - Initialization of dataloader to resolve field - :param data_loader: dataloader to resolve field - :param source_loader: field to obtain the key for dataloading + Initialization of data-loader to resolve field + :param data_loader: data-loader to resolve field + :param source_loader: field to obtain the key for data-loading + :param load_many: Whether the resolver should try tu obtain one element or multiple elements :param kwargs: Extra arguments """ self.data_loader = data_loader self.source_loader = source_loader + self.load_many = load_many super(DataLoaderField, self).__init__(type, *args, **kwargs) @@ -191,7 +193,10 @@ class DataLoaderField(DjangoField): def resolver_data_loader(self, root, info, *args, **kwargs): """Resolve field through dataloader""" if root: - source_loader = getattr(root, self.source_loader) + source_loader = reduce(lambda x, y: getattr(x, y), self.source_loader.split('.'), root) else: source_loader = kwargs.get(self.source_loader) + + if self.load_many: + return self.data_loader.load_many(source_loader) return self.data_loader.load(source_loader) diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 8abd9b3..0978110 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -9,6 +9,11 @@ from promise import Promise class MyInstance(object): value = "value" key = 1 + keys = [1, 2, 3] + + class InnerClass(object): + key = 2 + keys = [4, 5, 6] def resolver(self): return "resolver method" @@ -62,6 +67,34 @@ class DataLoaderFieldTests(TestCase): self.assertEqual(resolver(instance, None).get(), instance.key) + def test_dataloaderfield_many(self): + MyType = object() + data_loader_field = DataLoaderField(data_loader=data_loader, source_loader='keys', type=MyType, load_many=True) + + resolver = data_loader_field.get_resolver(None) + instance = MyInstance() + + self.assertEqual(resolver(instance, None).get(), instance.keys) + + def test_dataloaderfield_inner_prop(self): + MyType = object() + data_loader_field = DataLoaderField(data_loader=data_loader, source_loader='InnerClass.key', type=MyType) + + resolver = data_loader_field.get_resolver(None) + instance = MyInstance() + + self.assertEqual(resolver(instance, None).get(), instance.InnerClass.key) + + def test_dataloaderfield_many_inner_prop(self): + MyType = object() + data_loader_field = DataLoaderField(data_loader=data_loader, source_loader='InnerClass.keys', type=MyType, + load_many=True) + + resolver = data_loader_field.get_resolver(None) + instance = MyInstance() + + self.assertEqual(resolver(instance, None).get(), instance.InnerClass.keys) + def test_dataloaderfield_permissions(self): MyType = object() data_loader_field = DataLoaderField(data_loader=data_loader, source_loader='key', type=MyType, From 923585be8739b240e9dc9149d5b3f4d668f58fd2 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 11 Apr 2019 15:36:44 -0400 Subject: [PATCH 5/5] Check none values before data-loading ut --- graphene_django/fields.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index c9b4942..6383258 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -199,4 +199,6 @@ class DataLoaderField(DjangoField): if self.load_many: return self.data_loader.load_many(source_loader) - return self.data_loader.load(source_loader) + if source_loader: + return self.data_loader.load(source_loader) + return None