Skip to content

Commit f4c1e71

Browse files
committed
Improved arguments received by proxying keys to snake_case. Added relay mutations
1 parent bd30bbb commit f4c1e71

File tree

6 files changed

+223
-4
lines changed

6 files changed

+223
-4
lines changed

graphene/core/fields.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
GraphQLFloat,
1414
GraphQLInputObjectField,
1515
)
16-
from graphene.utils import to_camel_case
16+
from graphene.utils import to_camel_case, ProxySnakeDict
1717
from graphene.core.types import BaseObjectType, InputObjectType
1818
from graphene.core.scalars import GraphQLSkipField
1919

@@ -59,7 +59,7 @@ def resolve(self, instance, args, info):
5959
schema = info and getattr(info.schema, 'graphene_schema', None)
6060
resolve_fn = self.get_resolve_fn(schema)
6161
if resolve_fn:
62-
return resolve_fn(instance, args, info)
62+
return resolve_fn(instance, ProxySnakeDict(args), info)
6363
else:
6464
return getattr(instance, self.field_name, self.get_default())
6565

graphene/relay/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
Node,
99
PageInfo,
1010
Edge,
11-
Connection
11+
Connection,
12+
ClientIDMutation
1213
)
1314

1415
from graphene.relay.utils import is_node

graphene/relay/types.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
to_global_id
33
)
44

5-
from graphene.core.types import Interface, ObjectType
5+
from graphene.core.types import Interface, ObjectType, Mutation, InputObjectType
66
from graphene.core.fields import BooleanField, StringField, ListField, Field
77
from graphene.relay.fields import GlobalIDField
88
from graphene.utils import memoize
@@ -84,3 +84,32 @@ def get_edge_type(cls):
8484
class Node(BaseNode, Interface):
8585
'''An object with an ID'''
8686
id = GlobalIDField()
87+
88+
89+
class MutationInputType(InputObjectType):
90+
client_mutation_id = StringField(required=True)
91+
92+
93+
class ClientIDMutation(Mutation):
94+
client_mutation_id = StringField(required=True)
95+
96+
@classmethod
97+
def _prepare_class(cls):
98+
input_type = getattr(cls, 'input_type', None)
99+
if input_type:
100+
assert hasattr(cls, 'mutate_and_get_payload'), 'You have to implement mutate_and_get_payload'
101+
new_input_inner_type = type('{}InnerInput'.format(cls._meta.type_name), (MutationInputType, input_type, ), {})
102+
items = {
103+
'input': Field(new_input_inner_type)
104+
}
105+
assert issubclass(new_input_inner_type, InputObjectType)
106+
input_type = type('{}Input'.format(cls._meta.type_name), (ObjectType, ), items)
107+
setattr(cls, 'input_type', input_type)
108+
109+
@classmethod
110+
def mutate(cls, instance, args, info):
111+
input = args.get('input')
112+
payload = cls.mutate_and_get_payload(input, info)
113+
client_mutation_id = input.get('client_mutation_id')
114+
setattr(payload, 'client_mutation_id', client_mutation_id)
115+
return payload

graphene/utils.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import collections
2+
import re
13
from functools import wraps
24

35

@@ -42,6 +44,73 @@ def to_camel_case(snake_str):
4244
return components[0] + "".join(x.title() for x in components[1:])
4345

4446

47+
# From this response in Stackoverflow
48+
# http://stackoverflow.com/a/1176023/1072990
49+
def to_snake_case(name):
50+
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
51+
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
52+
53+
54+
class ProxySnakeDict(collections.MutableMapping):
55+
__slots__ = ('data')
56+
57+
def __init__(self, data):
58+
self.data = data
59+
60+
def __contains__(self, key):
61+
return key in self.data or to_camel_case(key) in self.data
62+
63+
def get(self, key, default=None):
64+
try:
65+
return self.__getitem__(key)
66+
except KeyError:
67+
return default
68+
69+
def __iter__(self):
70+
return self.iterkeys()
71+
72+
def __len__(self):
73+
return len(self.data)
74+
75+
def __delitem__(self):
76+
raise TypeError('ProxySnakeDict does not support item deletion')
77+
78+
def __setitem__(self):
79+
raise TypeError('ProxySnakeDict does not support item assignment')
80+
81+
def __getitem__(self, key):
82+
if key in self.data:
83+
item = self.data[key]
84+
else:
85+
camel_key = to_camel_case(key)
86+
if camel_key in self.data:
87+
item = self.data[camel_key]
88+
else:
89+
raise KeyError(key, camel_key)
90+
91+
if isinstance(item, dict):
92+
return ProxySnakeDict(item)
93+
return item
94+
95+
def keys(self):
96+
return list(self.iterkeys())
97+
98+
def items(self):
99+
return list(self.iteritems())
100+
101+
def iterkeys(self):
102+
for k in self.data.keys():
103+
yield to_snake_case(k)
104+
return
105+
106+
def iteritems(self):
107+
for k in self.iterkeys():
108+
yield k, self[k]
109+
110+
def __repr__(self):
111+
return dict(self.iteritems()).__repr__()
112+
113+
45114
class LazyMap(object):
46115
def __init__(self, origin, _map, state=None):
47116
self._origin = origin

tests/relay/test_relay_mutations.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from graphql.core.type import (
2+
GraphQLInputObjectField
3+
)
4+
5+
import graphene
6+
from graphene import relay
7+
from graphene.core.types import InputObjectType
8+
from graphene.core.schema import Schema
9+
10+
my_id = 0
11+
12+
13+
class Query(graphene.ObjectType):
14+
base = graphene.StringField()
15+
16+
17+
class ChangeNumber(relay.ClientIDMutation):
18+
'''Result mutation'''
19+
class Input:
20+
to = graphene.IntField()
21+
22+
result = graphene.StringField()
23+
24+
@classmethod
25+
def mutate_and_get_payload(cls, input, info):
26+
global my_id
27+
my_id = input.get('to', my_id + 1)
28+
return ChangeNumber(result=my_id)
29+
30+
31+
class MyResultMutation(graphene.ObjectType):
32+
change_number = graphene.Field(ChangeNumber)
33+
34+
35+
schema = Schema(query=Query, mutation=MyResultMutation)
36+
37+
38+
def test_mutation_input():
39+
assert ChangeNumber.input_type
40+
assert ChangeNumber.input_type._meta.type_name == 'ChangeNumberInput'
41+
assert list(ChangeNumber.input_type._meta.fields_map.keys()) == ['input']
42+
_input = ChangeNumber.input_type._meta.fields_map['input']
43+
inner_type = _input.get_object_type(schema)
44+
client_mutation_id_field = inner_type._meta.fields_map['client_mutation_id']
45+
assert issubclass(inner_type, InputObjectType)
46+
assert isinstance(client_mutation_id_field, graphene.StringField)
47+
assert client_mutation_id_field.object_type == inner_type
48+
assert isinstance(client_mutation_id_field.internal_field(schema), GraphQLInputObjectField)
49+
50+
51+
def test_execute_mutations():
52+
query = '''
53+
mutation M{
54+
first: changeNumber(input: {clientMutationId: "mutation1"}) {
55+
clientMutationId
56+
result
57+
},
58+
second: changeNumber(input: {clientMutationId: "mutation2"}) {
59+
clientMutationId
60+
result
61+
}
62+
third: changeNumber(input: {clientMutationId: "mutation3", to: 5}) {
63+
result
64+
clientMutationId
65+
}
66+
}
67+
'''
68+
expected = {
69+
'first': {
70+
'clientMutationId': 'mutation1',
71+
'result': '1',
72+
},
73+
'second': {
74+
'clientMutationId': 'mutation2',
75+
'result': '2',
76+
},
77+
'third': {
78+
'clientMutationId': 'mutation3',
79+
'result': '5',
80+
}
81+
}
82+
result = schema.execute(query, root=object())
83+
assert not result.errors
84+
assert result.data == expected

tests/utils/test_utils.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from graphene.utils import ProxySnakeDict, to_snake_case
2+
3+
4+
def test_snake_case():
5+
assert to_snake_case('snakesOnAPlane') == 'snakes_on_a_plane'
6+
assert to_snake_case('SnakesOnAPlane') == 'snakes_on_a_plane'
7+
assert to_snake_case('snakes_on_a_plane') == 'snakes_on_a_plane'
8+
assert to_snake_case('IPhoneHysteria') == 'i_phone_hysteria'
9+
assert to_snake_case('iPhoneHysteria') == 'i_phone_hysteria'
10+
11+
12+
def test_proxy_snake_dict():
13+
my_data = {'one': 1, 'two': 2, 'none': None, 'threeOrFor': 3, 'inside': {'otherCamelCase': 3}}
14+
p = ProxySnakeDict(my_data)
15+
assert 'one' in p
16+
assert 'two' in p
17+
assert 'threeOrFor' in p
18+
assert 'none' in p
19+
assert p['none'] is None
20+
assert p.get('none') is None
21+
assert p.get('none_existent') is None
22+
assert 'three_or_for' in p
23+
assert p.get('three_or_for') == 3
24+
assert 'inside' in p
25+
assert 'other_camel_case' in p['inside']
26+
27+
28+
def test_proxy_snake_dict_as_kwargs():
29+
my_data = {'myData': 1}
30+
p = ProxySnakeDict(my_data)
31+
32+
def func(**kwargs):
33+
return kwargs.get('my_data')
34+
assert func(**p) == 1
35+
36+

0 commit comments

Comments
 (0)