Improved arguments received by proxying keys to snake_case. Added relay mutations

This commit is contained in:
Syrus Akbary 2015-10-27 23:16:15 -07:00
parent bd30bbb322
commit f4c1e711cc
6 changed files with 223 additions and 4 deletions

View File

@ -13,7 +13,7 @@ from graphql.core.type import (
GraphQLFloat,
GraphQLInputObjectField,
)
from graphene.utils import to_camel_case
from graphene.utils import to_camel_case, ProxySnakeDict
from graphene.core.types import BaseObjectType, InputObjectType
from graphene.core.scalars import GraphQLSkipField
@ -59,7 +59,7 @@ class Field(object):
schema = info and getattr(info.schema, 'graphene_schema', None)
resolve_fn = self.get_resolve_fn(schema)
if resolve_fn:
return resolve_fn(instance, args, info)
return resolve_fn(instance, ProxySnakeDict(args), info)
else:
return getattr(instance, self.field_name, self.get_default())

View File

@ -8,7 +8,8 @@ from graphene.relay.types import (
Node,
PageInfo,
Edge,
Connection
Connection,
ClientIDMutation
)
from graphene.relay.utils import is_node

View File

@ -2,7 +2,7 @@ from graphql_relay.node.node import (
to_global_id
)
from graphene.core.types import Interface, ObjectType
from graphene.core.types import Interface, ObjectType, Mutation, InputObjectType
from graphene.core.fields import BooleanField, StringField, ListField, Field
from graphene.relay.fields import GlobalIDField
from graphene.utils import memoize
@ -84,3 +84,32 @@ class BaseNode(object):
class Node(BaseNode, Interface):
'''An object with an ID'''
id = GlobalIDField()
class MutationInputType(InputObjectType):
client_mutation_id = StringField(required=True)
class ClientIDMutation(Mutation):
client_mutation_id = StringField(required=True)
@classmethod
def _prepare_class(cls):
input_type = getattr(cls, 'input_type', None)
if input_type:
assert hasattr(cls, 'mutate_and_get_payload'), 'You have to implement mutate_and_get_payload'
new_input_inner_type = type('{}InnerInput'.format(cls._meta.type_name), (MutationInputType, input_type, ), {})
items = {
'input': Field(new_input_inner_type)
}
assert issubclass(new_input_inner_type, InputObjectType)
input_type = type('{}Input'.format(cls._meta.type_name), (ObjectType, ), items)
setattr(cls, 'input_type', input_type)
@classmethod
def mutate(cls, instance, args, info):
input = args.get('input')
payload = cls.mutate_and_get_payload(input, info)
client_mutation_id = input.get('client_mutation_id')
setattr(payload, 'client_mutation_id', client_mutation_id)
return payload

View File

@ -1,3 +1,5 @@
import collections
import re
from functools import wraps
@ -42,6 +44,73 @@ def to_camel_case(snake_str):
return components[0] + "".join(x.title() for x in components[1:])
# From this response in Stackoverflow
# http://stackoverflow.com/a/1176023/1072990
def to_snake_case(name):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
class ProxySnakeDict(collections.MutableMapping):
__slots__ = ('data')
def __init__(self, data):
self.data = data
def __contains__(self, key):
return key in self.data or to_camel_case(key) in self.data
def get(self, key, default=None):
try:
return self.__getitem__(key)
except KeyError:
return default
def __iter__(self):
return self.iterkeys()
def __len__(self):
return len(self.data)
def __delitem__(self):
raise TypeError('ProxySnakeDict does not support item deletion')
def __setitem__(self):
raise TypeError('ProxySnakeDict does not support item assignment')
def __getitem__(self, key):
if key in self.data:
item = self.data[key]
else:
camel_key = to_camel_case(key)
if camel_key in self.data:
item = self.data[camel_key]
else:
raise KeyError(key, camel_key)
if isinstance(item, dict):
return ProxySnakeDict(item)
return item
def keys(self):
return list(self.iterkeys())
def items(self):
return list(self.iteritems())
def iterkeys(self):
for k in self.data.keys():
yield to_snake_case(k)
return
def iteritems(self):
for k in self.iterkeys():
yield k, self[k]
def __repr__(self):
return dict(self.iteritems()).__repr__()
class LazyMap(object):
def __init__(self, origin, _map, state=None):
self._origin = origin

View File

@ -0,0 +1,84 @@
from graphql.core.type import (
GraphQLInputObjectField
)
import graphene
from graphene import relay
from graphene.core.types import InputObjectType
from graphene.core.schema import Schema
my_id = 0
class Query(graphene.ObjectType):
base = graphene.StringField()
class ChangeNumber(relay.ClientIDMutation):
'''Result mutation'''
class Input:
to = graphene.IntField()
result = graphene.StringField()
@classmethod
def mutate_and_get_payload(cls, input, info):
global my_id
my_id = input.get('to', my_id + 1)
return ChangeNumber(result=my_id)
class MyResultMutation(graphene.ObjectType):
change_number = graphene.Field(ChangeNumber)
schema = Schema(query=Query, mutation=MyResultMutation)
def test_mutation_input():
assert ChangeNumber.input_type
assert ChangeNumber.input_type._meta.type_name == 'ChangeNumberInput'
assert list(ChangeNumber.input_type._meta.fields_map.keys()) == ['input']
_input = ChangeNumber.input_type._meta.fields_map['input']
inner_type = _input.get_object_type(schema)
client_mutation_id_field = inner_type._meta.fields_map['client_mutation_id']
assert issubclass(inner_type, InputObjectType)
assert isinstance(client_mutation_id_field, graphene.StringField)
assert client_mutation_id_field.object_type == inner_type
assert isinstance(client_mutation_id_field.internal_field(schema), GraphQLInputObjectField)
def test_execute_mutations():
query = '''
mutation M{
first: changeNumber(input: {clientMutationId: "mutation1"}) {
clientMutationId
result
},
second: changeNumber(input: {clientMutationId: "mutation2"}) {
clientMutationId
result
}
third: changeNumber(input: {clientMutationId: "mutation3", to: 5}) {
result
clientMutationId
}
}
'''
expected = {
'first': {
'clientMutationId': 'mutation1',
'result': '1',
},
'second': {
'clientMutationId': 'mutation2',
'result': '2',
},
'third': {
'clientMutationId': 'mutation3',
'result': '5',
}
}
result = schema.execute(query, root=object())
assert not result.errors
assert result.data == expected

36
tests/utils/test_utils.py Normal file
View File

@ -0,0 +1,36 @@
from graphene.utils import ProxySnakeDict, to_snake_case
def test_snake_case():
assert to_snake_case('snakesOnAPlane') == 'snakes_on_a_plane'
assert to_snake_case('SnakesOnAPlane') == 'snakes_on_a_plane'
assert to_snake_case('snakes_on_a_plane') == 'snakes_on_a_plane'
assert to_snake_case('IPhoneHysteria') == 'i_phone_hysteria'
assert to_snake_case('iPhoneHysteria') == 'i_phone_hysteria'
def test_proxy_snake_dict():
my_data = {'one': 1, 'two': 2, 'none': None, 'threeOrFor': 3, 'inside': {'otherCamelCase': 3}}
p = ProxySnakeDict(my_data)
assert 'one' in p
assert 'two' in p
assert 'threeOrFor' in p
assert 'none' in p
assert p['none'] is None
assert p.get('none') is None
assert p.get('none_existent') is None
assert 'three_or_for' in p
assert p.get('three_or_for') == 3
assert 'inside' in p
assert 'other_camel_case' in p['inside']
def test_proxy_snake_dict_as_kwargs():
my_data = {'myData': 1}
p = ProxySnakeDict(my_data)
def func(**kwargs):
return kwargs.get('my_data')
assert func(**p) == 1