Skip to content
This repository was archived by the owner on Sep 6, 2022. It is now read-only.

Commit 6a0787e

Browse files
authored
Merge pull request #16 from graphql-python/feature/fix_key_property_behavior
Breaking change: ndb KeyProperties should be externalised as Global IDs
2 parents ff31717 + 05e87d4 commit 6a0787e

File tree

7 files changed

+86
-22
lines changed

7 files changed

+86
-22
lines changed

HISTORY.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
History
44
-------
55

6+
0.1.7 (TBD)
7+
---------------------
8+
9+
610
0.1.6 (2016-06-10)
711
---------------------
812
* Changing development status to Beta

graphene_gae/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
)
1313

1414
__author__ = 'Eran Kampf'
15-
__version__ = '0.1.6'
15+
__version__ = '0.1.7'
1616

1717
__all__ = [
1818
NdbObjectType,

graphene_gae/ndb/converter.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
p = inflect.engine()
1818

1919

20+
def rreplace(s, old, new, occurrence):
21+
li = s.rsplit(old, occurrence)
22+
return new.join(li)
23+
24+
2025
def convert_ndb_scalar_property(graphene_type, ndb_prop, **kwargs):
2126
description = "%s %s property" % (ndb_prop._name, graphene_type)
2227
result = graphene_type(description=description, **kwargs)
@@ -61,31 +66,31 @@ def convert_ndb_key_propety(ndb_key_prop, meta):
6166
store_key = ndb.KeyProperty(...)
6267
6368
Result is 2 fields:
64-
store_key = graphene.String() -> resolves to store_key.urlsafe()
69+
store_id = graphene.String() -> resolves to store_key.urlsafe()
6570
store = NdbKeyField() -> resolves to entity
6671
6772
#2.
6873
Given:
6974
store = ndb.KeyProperty(...)
7075
7176
Result is 2 fields:
72-
store_key = graphene.String() -> resolves to store_key.urlsafe()
77+
store_id = graphene.String() -> resolves to store_key.urlsafe()
7378
store = NdbKeyField() -> resolves to entity
7479
7580
"""
7681
name = ndb_key_prop._code_name
7782

7883
if name.endswith('_key') or name.endswith('_keys'):
7984
# Case #1 - name is of form 'store_key' or 'store_keys'
80-
string_prop_name = name
85+
string_prop_name = rreplace(name, '_key', '_id', 1)
8186
resolved_prop_name = name[:-4] if name.endswith('_key') else p.plural(name[:-5])
8287
else:
8388
# Case #2 - name is of form 'store'
8489
singular_name = p.singular_noun(name) if p.singular_noun(name) else name
85-
string_prop_name = singular_name + '_keys' if ndb_key_prop._repeated else singular_name + '_key'
90+
string_prop_name = singular_name + '_ids' if ndb_key_prop._repeated else singular_name + '_id'
8691
resolved_prop_name = name
8792

88-
string_field = NdbKeyStringField(name)
93+
string_field = NdbKeyStringField(name, ndb_key_prop._kind)
8994
resolved_field = NdbKeyField(name, ndb_key_prop._kind)
9095

9196
if ndb_key_prop._repeated:
@@ -105,7 +110,6 @@ def convert_ndb_key_propety(ndb_key_prop, meta):
105110
]
106111

107112

108-
109113
def convert_local_structured_property(ndb_structured_prop, meta):
110114
is_required = ndb_structured_prop._required
111115
is_repeated = ndb_structured_prop._repeated

graphene_gae/ndb/fields.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from graphene.core.exceptions import SkipField
66
from graphene.core.types.base import FieldType
77
from graphene.core.types.scalars import Boolean, Int, String
8+
from graphql_relay import to_global_id
89

910
__author__ = 'ekampf'
1011

@@ -90,22 +91,55 @@ def model(self):
9091

9192

9293
class NdbKeyStringField(String):
93-
def __init__(self, name, *args, **kwargs):
94+
def __init__(self, name, kind, *args, **kwargs):
9495
self.name = name
96+
self.kind = kind
9597

9698
if 'resolver' not in kwargs:
9799
kwargs['resolver'] = self.default_resolver
98100

99101
super(NdbKeyStringField, self).__init__(*args, **kwargs)
100102

103+
def internal_type(self, schema):
104+
_type = self.get_object_type(schema)
105+
if not _type and self.parent._meta.only_fields:
106+
raise Exception(
107+
"Model %r is not accessible by the schema. "
108+
"You can either register the type manually "
109+
"using @schema.register. "
110+
"Or disable the field in %s" % (
111+
self.kind,
112+
self.parent,
113+
)
114+
)
115+
116+
if not _type:
117+
raise SkipField()
118+
119+
from graphql import GraphQLString
120+
return GraphQLString
121+
122+
def get_object_type(self, schema):
123+
for _type in schema.types.values():
124+
type_model = hasattr(_type, '_meta') and getattr(_type._meta, 'model', None)
125+
if not type_model:
126+
continue
127+
128+
if self.kind == type_model or self.kind == type_model.__name__:
129+
return _type
130+
101131
def default_resolver(self, node, args, info):
102132
entity = node.instance
103133
key = getattr(entity, self.name)
134+
if not key:
135+
return None
104136

105137
if isinstance(key, list):
106-
return [k.urlsafe() for k in key]
138+
t = self.get_object_type(info.schema.graphene_schema)._meta.type_name
139+
return [to_global_id(t, k.urlsafe()) for k in key]
107140

108-
return key.urlsafe() if key else None
141+
t = self.get_object_type(info.schema.graphene_schema)._meta.type_name
142+
return to_global_id(t, key.urlsafe()) if key else None
109143

110144

111145
class NdbKeyField(FieldType):

tests/_ndb/test_converter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def testKeyProperty_withSuffix(self):
8383

8484
self.assertLength(conversion, 2)
8585

86-
self.assertEqual(conversion[0].name, 'user_key')
86+
self.assertEqual(conversion[0].name, 'user_id')
8787
self.assertIsInstance(conversion[0].field, NdbKeyStringField)
8888

8989
self.assertEqual(conversion[1].name, 'user')
@@ -97,7 +97,7 @@ def testKeyProperty_withSuffix_repeated(self):
9797

9898
self.assertLength(conversion, 2)
9999

100-
self.assertEqual(conversion[0].name, 'user_keys')
100+
self.assertEqual(conversion[0].name, 'user_ids')
101101
self.assertIsInstance(conversion[0].field, List)
102102
self.assertIsInstance(conversion[0].field.of_type, NdbKeyStringField)
103103

@@ -113,7 +113,7 @@ def testKeyProperty_withSuffix_required(self):
113113

114114
self.assertLength(conversion, 2)
115115

116-
self.assertEqual(conversion[0].name, 'user_key')
116+
self.assertEqual(conversion[0].name, 'user_id')
117117
self.assertIsInstance(conversion[0].field, NonNull)
118118
self.assertIsInstance(conversion[0].field.of_type, NdbKeyStringField)
119119

@@ -129,7 +129,7 @@ def testKeyProperty_withoutSuffix(self):
129129

130130
self.assertLength(conversion, 2)
131131

132-
self.assertEqual(conversion[0].name, 'user_key')
132+
self.assertEqual(conversion[0].name, 'user_id')
133133
self.assertIsInstance(conversion[0].field, NdbKeyStringField)
134134

135135
self.assertEqual(conversion[1].name, 'user')

tests/_ndb/test_types.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from graphene_gae.ndb.fields import NdbKeyStringField
2+
from graphql_relay import to_global_id
13
from tests.base_test import BaseTest
24

35
import graphene
@@ -103,6 +105,27 @@ def resolve_articles(self):
103105

104106
self.assertIn("Model 'bar' is not accessible by the schema.", str(context.exception.message))
105107

108+
def testNdbObjectType_keyProperty_stringRepresentation_kindDoesntExist_raisesException(self):
109+
with self.assertRaises(Exception) as context:
110+
class ArticleType(NdbObjectType):
111+
class Meta:
112+
model = Article
113+
only_fields = ('prop',)
114+
115+
prop = NdbKeyStringField('foo', 'bar')
116+
117+
class QueryType(graphene.ObjectType):
118+
articles = graphene.List(ArticleType)
119+
120+
@graphene.resolve_only_args
121+
def resolve_articles(self):
122+
return Article.query()
123+
124+
schema = graphene.Schema(query=QueryType)
125+
schema.execute('query test { articles { prop } }')
126+
127+
self.assertIn("Model 'bar' is not accessible by the schema.", str(context.exception.message))
128+
106129
def testQuery_excludedField(self):
107130
Article(headline="h1", summary="s1").put()
108131

@@ -280,7 +303,7 @@ def testQuery_keyProperty(self):
280303
query ArticleWithAuthorID {
281304
articles {
282305
headline
283-
authorKey
306+
authorId
284307
author {
285308
name, email
286309
}
@@ -293,7 +316,8 @@ def testQuery_keyProperty(self):
293316
article = dict(result.data['articles'][0])
294317
author = dict(article['author'])
295318
self.assertDictEqual(author, {'name': u'john dow', 'email': u'[email protected]'})
296-
self.assertDictContainsSubset(dict(headline='h1', authorKey=author_key.urlsafe()), article)
319+
self.assertEqual('h1', article['headline'])
320+
self.assertEqual(to_global_id('AuthorType', author_key.urlsafe()), article['authorId'])
297321

298322
def testQuery_repeatedKeyProperty(self):
299323
tk1 = Tag(name="t1").put()
@@ -302,14 +326,12 @@ def testQuery_repeatedKeyProperty(self):
302326
tk4 = Tag(name="t4").put()
303327
Article(headline="h1", summary="s1", tags=[tk1, tk2, tk3, tk4]).put()
304328

305-
print str(schema)
306-
307329
result = schema.execute('''
308330
query ArticleWithAuthorID {
309331
articles {
310332
headline
311-
authorKey
312-
tagKeys
333+
authorId
334+
tagIds
313335
tags {
314336
name
315337
}
@@ -320,7 +342,7 @@ def testQuery_repeatedKeyProperty(self):
320342
self.assertEmpty(result.errors)
321343

322344
article = dict(result.data['articles'][0])
323-
self.assertListEqual(map(lambda k: k.urlsafe(), [tk1, tk2, tk3, tk4]), article['tagKeys'])
345+
self.assertListEqual(map(lambda k: to_global_id('TagType', k.urlsafe()), [tk1, tk2, tk3, tk4]), article['tagIds'])
324346

325347
self.assertLength(article['tags'], 4)
326348
for i in range(0, 3):

tests/base_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def get_filtered_tasks(self, url=None, name=None, queue_names=None):
6161

6262
# region Extra Assertions
6363
def assertEmpty(self, l, msg=None):
64-
self.assertEqual(0, len(list(l)), msg=msg)
64+
self.assertEqual(0, len(list(l)), msg=msg or str(l))
6565

6666
def assertLength(self, l, expectation, msg=None):
6767
self.assertEqual(len(l), expectation, msg)

0 commit comments

Comments
 (0)