mirror of
https://github.com/graphql-python/graphene.git
synced 2025-02-02 12:44:15 +03:00
Improved arguments received by proxying keys to snake_case. Added relay mutations
This commit is contained in:
parent
bd30bbb322
commit
f4c1e711cc
|
@ -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())
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@ from graphene.relay.types import (
|
|||
Node,
|
||||
PageInfo,
|
||||
Edge,
|
||||
Connection
|
||||
Connection,
|
||||
ClientIDMutation
|
||||
)
|
||||
|
||||
from graphene.relay.utils import is_node
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
84
tests/relay/test_relay_mutations.py
Normal file
84
tests/relay/test_relay_mutations.py
Normal 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
36
tests/utils/test_utils.py
Normal 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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user