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,
|
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())
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
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