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

Commit 521e1cc

Browse files
committed
Merge pull request #14 from graphql-python/feature/improved_key_property_behaviour
Improved conversion of ndb.KeyProperty to GraphQL
2 parents e75e1be + a95d6f9 commit 521e1cc

File tree

8 files changed

+229
-71
lines changed

8 files changed

+229
-71
lines changed

HISTORY.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
History
44
-------
55

6-
0.1.4 (TBD)
6+
0.1.5 (2016-06-08)
7+
---------------------
8+
* Fixed behavior of ndb.KeyProperty ([PR #14](https://github.com/graphql-python/graphene-gae/pull/14))
9+
10+
0.1.4 (2016-06-02)
711
---------------------
812
* NdbConnectionField added arguments that can be used in quert:
913
* keys_only - to execute a keys only query
@@ -14,6 +18,7 @@ History
1418
map to a Field(SomethingType) - SomethingType has to be part of the schema.
1519
* Support for `repeated` and `required` propeties.
1620

21+
1722
0.1.3 (2016-05-27)
1823
---------------------
1924
* Added `graphene_gae.webapp2.GraphQLHandler` - a basic HTTP Handler to process GraphQL requests

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.4'
15+
__version__ = '0.1.5'
1616

1717
__all__ = [
1818
NdbObjectType,

graphene_gae/ndb/converter.py

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from graphene.core.types.definitions import List
99
from graphene.core.types.scalars import String, Boolean, Int, Float
1010
from graphene.core.types.custom_scalars import JSONString, DateTime
11-
from graphene_gae.ndb.fields import NdbKeyField
11+
from graphene_gae.ndb.fields import NdbKeyField, NdbKeyStringField
1212

1313
__author__ = 'ekampf'
1414

@@ -17,16 +17,16 @@
1717
p = inflect.engine()
1818

1919

20-
def convert_ndb_scalar_property(graphene_type, ndb_prop):
20+
def convert_ndb_scalar_property(graphene_type, ndb_prop, **kwargs):
2121
description = "%s %s property" % (ndb_prop._name, graphene_type)
22+
result = graphene_type(description=description, **kwargs)
2223
if ndb_prop._repeated:
23-
l = graphene_type(description=description).List
24-
return l if not ndb_prop._required else l.NonNull
24+
result = result.List
2525

2626
if ndb_prop._required:
27-
return graphene_type(description=description).NonNull
27+
result = result.NonNull
2828

29-
return graphene_type(description=description)
29+
return result
3030

3131

3232
def convert_ndb_string_property(ndb_prop, meta):
@@ -54,22 +54,69 @@ def convert_ndb_datetime_property(ndb_prop, meta):
5454

5555

5656
def convert_ndb_key_propety(ndb_key_prop, meta):
57-
remove_key_suffix = meta.remove_key_property_suffix if meta else True
57+
"""
58+
Two conventions for handling KeyProperties:
59+
#1.
60+
Given:
61+
store_key = ndb.KeyProperty(...)
5862
63+
Result is 2 fields:
64+
store_key = graphene.String() -> resolves to store_key.urlsafe()
65+
store = NdbKeyField() -> resolves to entity
66+
67+
#2.
68+
Given:
69+
store = ndb.KeyProperty(...)
70+
71+
Result is 2 fields:
72+
store_key = graphene.String() -> resolves to store_key.urlsafe()
73+
store = NdbKeyField() -> resolves to entity
74+
75+
"""
5976
name = ndb_key_prop._code_name
60-
if remove_key_suffix:
61-
if name.endswith('_key'):
62-
name = name[:-4]
6377

64-
if name.endswith('_keys'):
65-
name = name[:-5]
66-
name = p.plural(name)
78+
if name.endswith('_key') or name.endswith('_keys'):
79+
# Case #1 - name is of form 'store_key' or 'store_keys'
80+
string_prop_name = name
81+
resolved_prop_name = name[:-4] if name.endswith('_key') else p.plural(name[:-5])
82+
else:
83+
# Case #2 - name is of form 'store'
84+
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'
86+
resolved_prop_name = name
6787

68-
field = NdbKeyField(ndb_key_prop._code_name, ndb_key_prop._kind)
69-
if ndb_key_prop._repeated:
70-
field = field.List
88+
string_field = NdbKeyStringField(name)
89+
resolved_field = NdbKeyField(name, ndb_key_prop._kind)
7190

72-
return ConversionResult(name=name, field=field)
91+
if ndb_key_prop._repeated:
92+
string_field = string_field.List
93+
resolved_field = resolved_field.List
94+
95+
if ndb_key_prop._required:
96+
string_field = string_field.NonNull
97+
resolved_field = resolved_field.NonNull
98+
99+
string_key_field_result = ConversionResult(name=string_prop_name, field=string_field)
100+
resolve_key_field_result = ConversionResult(name=resolved_prop_name, field=resolved_field)
101+
102+
return [
103+
string_key_field_result,
104+
resolve_key_field_result
105+
]
106+
107+
# if remove_key_suffix:
108+
# if name.endswith('_key'):
109+
# name = name[:-4]
110+
#
111+
# if name.endswith('_keys'):
112+
# name = name[:-5]
113+
# name = p.plural(name)
114+
#
115+
# field = NdbKeyField(ndb_key_prop._code_name, ndb_key_prop._kind)
116+
# if ndb_key_prop._repeated:
117+
# field = field.List
118+
#
119+
# return ConversionResult(name=name, field=field)
73120

74121

75122
def convert_local_structured_property(ndb_structured_prop, meta):
@@ -114,7 +161,7 @@ def convert_ndb_property(prop, meta=None):
114161
if not result:
115162
raise Exception("Failed to convert NDB field %s (%s)" % (prop._code_name, prop))
116163

117-
if isinstance(result, ConversionResult):
164+
if isinstance(result, (list, ConversionResult,)):
118165
return result
119166

120167
return ConversionResult(name=prop._code_name, field=result)

graphene_gae/ndb/fields.py

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from graphene import relay
55
from graphene.core.exceptions import SkipField
66
from graphene.core.types.base import FieldType
7-
from graphene.core.types.scalars import Boolean, Int
7+
from graphene.core.types.scalars import Boolean, Int, String
88

99
__author__ = 'ekampf'
1010

@@ -89,6 +89,25 @@ def model(self):
8989
return self.type._meta.model
9090

9191

92+
class NdbKeyStringField(String):
93+
def __init__(self, name, *args, **kwargs):
94+
self.name = name
95+
96+
if 'resolver' not in kwargs:
97+
kwargs['resolver'] = self.default_resolver
98+
99+
super(NdbKeyStringField, self).__init__(*args, **kwargs)
100+
101+
def default_resolver(self, node, args, info):
102+
entity = node.instance
103+
key = getattr(entity, self.name)
104+
105+
if isinstance(key, list):
106+
return [k.urlsafe() for k in key]
107+
108+
return key.urlsafe() if key else None
109+
110+
92111
class NdbKeyField(FieldType):
93112
def __init__(self, name, kind, *args, **kwargs):
94113
self.name = name
@@ -107,7 +126,7 @@ def internal_type(self, schema):
107126
"You can either register the type manually "
108127
"using @schema.register. "
109128
"Or disable the field in %s" % (
110-
self.model,
129+
self.kind,
111130
self.parent,
112131
)
113132
)
@@ -131,30 +150,7 @@ def default_resolver(self, node, args, info):
131150
key = getattr(entity, self.name)
132151

133152
if isinstance(key, list):
134-
return self.__auto_resolve_repeated(entity, key)
135-
136-
return self.__auto_resolve_key(entity, key)
137-
138-
def __auto_resolve_repeated(self, entity, keys):
139-
if not self.name.endswith('_keys'):
140-
return ndb.get_multi(keys)
141-
142-
cache_name = self.name[:-4] # TODO: pluralise
143-
if hasattr(entity, cache_name):
144-
return getattr(entity, cache_name)
145-
146-
values = ndb.get_multi(keys)
147-
setattr(entity, cache_name, values)
148-
return values
149-
150-
def __auto_resolve_key(self, entity, key):
151-
if not self.name.endswith('_key'):
152-
return key.get()
153-
154-
cache_name = self.name[:-4]
155-
if hasattr(entity, cache_name):
156-
return getattr(entity, cache_name)
153+
entities = ndb.get_multi(key)
154+
return entities
157155

158-
value = key.get()
159-
setattr(entity, cache_name, value)
160-
return value
156+
return key.get()

graphene_gae/ndb/options.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ class NdbOptions(ObjectTypeOptions):
1111
* model - which model to convert
1212
* only_fields - only convert the following property names
1313
* exclude_fields - exclude specified properties from conversion
14-
* remove_key_property_suffix - remove '_key' suffix from KeyProperty
15-
* user_key => user
16-
* user_keys => users
1714
1815
"""
1916

@@ -25,7 +22,6 @@ def __init__(self, *args, **kwargs):
2522
self.valid_attrs += self.VALID_ATTRS
2623
self.only_fields = None
2724
self.exclude_fields = []
28-
self.remove_key_property_suffix = True
2925

3026
def contribute_to_class(self, cls, name):
3127
super(NdbOptions, self).contribute_to_class(cls, name)

graphene_gae/ndb/types.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ def construct_fields(cls):
3131
if is_not_in_only or is_excluded:
3232
continue
3333

34-
conversion_result = convert_ndb_property(prop, cls._meta)
35-
cls.add_to_class(conversion_result.name, conversion_result.field)
34+
conversion_results = convert_ndb_property(prop, cls._meta)
35+
if not isinstance(conversion_results, list):
36+
conversion_results = [conversion_results]
37+
38+
for r in conversion_results:
39+
cls.add_to_class(r.name, r.field)
3640

3741
def construct(cls, *args, **kwargs):
3842
super(NdbObjectTypeMeta, cls).construct(*args, **kwargs)

tests/_ndb/test_converter.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
import graphene
77
from graphene.core.types.custom_scalars import DateTime, JSONString
8+
from graphene.core.types.definitions import List, NonNull
89

10+
from graphene_gae.ndb.fields import NdbKeyStringField, NdbKeyField
911
from graphene_gae.ndb.converter import convert_ndb_property
1012

1113
__author__ = 'ekampf'
@@ -73,30 +75,62 @@ def testDateTimeProperty_shouldConvertToString(self):
7375
def testJsonProperty_shouldConvertToString(self):
7476
self.__assert_conversion(ndb.JsonProperty, JSONString)
7577

76-
def testKeyProperty_withSuffixRemoval_removesSuffix(self):
78+
def testKeyProperty_withSuffix(self):
7779
prop = ndb.KeyProperty()
78-
prop._code_name = "user_key"
80+
prop._code_name = 'user_key'
7981

8082
conversion = convert_ndb_property(prop)
81-
self.assertEqual(conversion.name, "user")
8283

83-
def testKeyProperty_repeatedPlural_withSuffixRemoval_removesSuffixAndPluralName(self):
84-
prop = ndb.KeyProperty()
85-
prop._code_name = "user_keys"
86-
conversion = convert_ndb_property(prop)
87-
self.assertEqual(conversion.name, "users")
84+
self.assertLength(conversion, 2)
85+
86+
self.assertEqual(conversion[0].name, 'user_key')
87+
self.assertIsInstance(conversion[0].field, NdbKeyStringField)
88+
89+
self.assertEqual(conversion[1].name, 'user')
90+
self.assertIsInstance(conversion[1].field, NdbKeyField)
91+
92+
def testKeyProperty_withSuffix_repeated(self):
93+
prop = ndb.KeyProperty(repeated=True)
94+
prop._code_name = 'user_keys'
8895

89-
prop = ndb.KeyProperty()
90-
prop._code_name = "tag_name_keys"
9196
conversion = convert_ndb_property(prop)
92-
self.assertEqual(conversion.name, "tag_names")
9397

94-
prop = ndb.KeyProperty()
95-
prop._code_name = "person_keys"
98+
self.assertLength(conversion, 2)
99+
100+
self.assertEqual(conversion[0].name, 'user_keys')
101+
self.assertIsInstance(conversion[0].field, List)
102+
self.assertIsInstance(conversion[0].field.of_type, NdbKeyStringField)
103+
104+
self.assertEqual(conversion[1].name, 'users')
105+
self.assertIsInstance(conversion[1].field, List)
106+
self.assertIsInstance(conversion[1].field.of_type, NdbKeyField)
107+
108+
def testKeyProperty_withSuffix_required(self):
109+
prop = ndb.KeyProperty(required=True)
110+
prop._code_name = 'user_key'
111+
96112
conversion = convert_ndb_property(prop)
97-
self.assertEqual(conversion.name, "people")
98113

114+
self.assertLength(conversion, 2)
115+
116+
self.assertEqual(conversion[0].name, 'user_key')
117+
self.assertIsInstance(conversion[0].field, NonNull)
118+
self.assertIsInstance(conversion[0].field.of_type, NdbKeyStringField)
119+
120+
self.assertEqual(conversion[1].name, 'user')
121+
self.assertIsInstance(conversion[1].field, NonNull)
122+
self.assertIsInstance(conversion[1].field.of_type, NdbKeyField)
123+
124+
def testKeyProperty_withoutSuffix(self):
99125
prop = ndb.KeyProperty()
100-
prop._code_name = "universal_category_keys"
126+
prop._code_name = 'user'
127+
101128
conversion = convert_ndb_property(prop)
102-
self.assertEqual(conversion.name, "universal_categories")
129+
130+
self.assertLength(conversion, 2)
131+
132+
self.assertEqual(conversion[0].name, 'user_key')
133+
self.assertIsInstance(conversion[0].field, NdbKeyStringField)
134+
135+
self.assertEqual(conversion[1].name, 'user')
136+
self.assertIsInstance(conversion[1].field, NdbKeyField)

0 commit comments

Comments
 (0)