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, GraphQLFloat,
GraphQLInputObjectField, 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.types import BaseObjectType, InputObjectType
from graphene.core.scalars import GraphQLSkipField from graphene.core.scalars import GraphQLSkipField
@ -59,7 +59,7 @@ class Field(object):
schema = info and getattr(info.schema, 'graphene_schema', None) schema = info and getattr(info.schema, 'graphene_schema', None)
resolve_fn = self.get_resolve_fn(schema) resolve_fn = self.get_resolve_fn(schema)
if resolve_fn: if resolve_fn:
return resolve_fn(instance, args, info) return resolve_fn(instance, ProxySnakeDict(args), info)
else: else:
return getattr(instance, self.field_name, self.get_default()) return getattr(instance, self.field_name, self.get_default())

View File

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

View File

@ -2,7 +2,7 @@ from graphql_relay.node.node import (
to_global_id 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.core.fields import BooleanField, StringField, ListField, Field
from graphene.relay.fields import GlobalIDField from graphene.relay.fields import GlobalIDField
from graphene.utils import memoize from graphene.utils import memoize
@ -84,3 +84,32 @@ class BaseNode(object):
class Node(BaseNode, Interface): class Node(BaseNode, Interface):
'''An object with an ID''' '''An object with an ID'''
id = GlobalIDField() 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 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:]) 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): class LazyMap(object):
def __init__(self, origin, _map, state=None): def __init__(self, origin, _map, state=None):
self._origin = origin 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