Skip to content

Commit ee9f214

Browse files
committed
Initial implementation of mutations.
1 parent 512619b commit ee9f214

File tree

6 files changed

+157
-4
lines changed

6 files changed

+157
-4
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,10 @@ Epoxy also supports defining mutations. Making a Mutation a Relay mutation is as
263263
```python
264264

265265
class AddFriend(R.Mutation):
266-
class Input(R.InputType):
266+
class Input:
267267
human_to_add = R.ID.NonNull
268268

269-
class Output(R.OutputType):
269+
class Output:
270270
new_friends_list = R.Human.List
271271

272272
@R.resolve_with_args

epoxy/argument.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from graphql.core.type import GraphQLArgument
2+
from .utils.gen_id import gen_id
3+
4+
5+
class Argument(object):
6+
def __init__(self, type, description=None, default_value=None, name=None, _counter=None):
7+
self.name = name
8+
self.type = type
9+
self.description = description
10+
self.default_value = default_value
11+
self._counter = _counter or gen_id()
12+
13+
def to_argument(self, registry):
14+
return GraphQLArgument(registry[self.type](), self.default_value, self.description)

epoxy/bases/mutation.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class MutationBase(object):
2+
def __init__(self):
3+
pass

epoxy/metaclasses/mutation.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from graphql.core.type import GraphQLField, GraphQLNonNull
2+
from graphql.core.type.definition import GraphQLArgument
3+
4+
5+
class MutationMeta(type):
6+
def __new__(mcs, name, bases, attrs):
7+
if attrs.pop('abstract', False):
8+
return super(MutationMeta, mcs).__new__(mcs, name, bases, attrs)
9+
10+
registry = mcs._get_registry()
11+
12+
input = attrs.pop('Input')
13+
output = attrs.pop('Output')
14+
15+
assert input and not hasattr(input, 'T'), 'A mutation must define a class named "Input" inside of it that ' \
16+
'does not subclass an R.InputType'
17+
assert output and not hasattr(output, 'T'), 'A mutation must define a class named "Output" inside of it that ' \
18+
'does not subclass an R.ObjectType'
19+
20+
Input = type(name + 'Input', (registry.InputType,), dict(vars(input)))
21+
Output = type(name + 'Payload', (registry.ObjectType,), dict(vars(output)))
22+
attrs['Input'] = Input
23+
attrs['Output'] = Output
24+
25+
cls = super(MutationMeta, mcs).__new__(mcs, name, bases, attrs)
26+
cls._registry = registry
27+
instance = cls()
28+
resolver = getattr(instance, 'execute')
29+
assert resolver and callable(resolver), 'A mutation must define a function named "execute" that will execute ' \
30+
'the mutation.'
31+
32+
mutation_name = name[0].lower() + name[1:]
33+
34+
mcs._register(mutation_name, registry.with_resolved_types(lambda R: GraphQLField(
35+
type=R[Output],
36+
args={
37+
'input': GraphQLArgument(GraphQLNonNull(R[Input]))
38+
},
39+
resolver=lambda obj, args, info: resolver(obj, Input(args.get('input')), info)
40+
)))
41+
42+
@staticmethod
43+
def _register(mutation_name, mutation):
44+
raise NotImplementedError('_register must be implemented in the sub-metaclass')
45+
46+
@staticmethod
47+
def _get_registry():
48+
raise NotImplementedError('_get_registry must be implemented in the sub-metaclass')

epoxy/registry.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections import defaultdict
1+
from collections import defaultdict, OrderedDict
22
from enum import Enum
33
from functools import partial
44
from graphql.core.type import (
@@ -21,6 +21,7 @@
2121
from .bases.class_type_creator import ClassTypeCreator
2222
from .bases.input_type import InputTypeBase
2323
from .bases.object_type import ObjectTypeBase
24+
from epoxy.metaclasses.mutation import MutationMeta
2425
from .field import Field, InputField
2526
from .metaclasses.input_type import InputTypeMeta
2627
from .metaclasses.interface import InterfaceMeta
@@ -46,6 +47,8 @@ class TypeRegistry(object):
4647
'ObjectType', 'InputType', 'Union' 'Interface', 'Implements',
4748
# Functions
4849
'Schema', 'Register', 'Mixin',
50+
# Mutations
51+
'Mutation', 'Mutations',
4952
# Fields
5053
'Field', 'InputField',
5154
])
@@ -65,6 +68,8 @@ def __init__(self):
6568
self.Implements = ClassTypeCreator(self, self._create_object_type_class)
6669
self.Union = ClassTypeCreator(self, self._create_union_type_class)
6770
self.Interface = self._create_interface_type_class()
71+
self.Mutation = self._create_mutation_type_class()
72+
self._mutations = {}
6873

6974
for type in builtin_scalars:
7075
self.Register(type)
@@ -111,6 +116,9 @@ def _resolve_type(self, item):
111116
return value
112117

113118
def __getattr__(self, item):
119+
if item.startswith('_'):
120+
raise AttributeError(item)
121+
114122
return RootTypeThunk(self, self._resolve_type, item)
115123

116124
def __getitem__(self, item):
@@ -205,6 +213,44 @@ class InputType(InputTypeBase):
205213

206214
return InputType
207215

216+
def _create_mutation_type_class(self):
217+
registry = self
218+
219+
class RegistryInputTypeMeta(MutationMeta):
220+
@staticmethod
221+
def _register(mutation_name, mutation):
222+
registry._register_mutation(mutation_name, mutation)
223+
224+
@staticmethod
225+
def _get_registry():
226+
return registry
227+
228+
@six.add_metaclass(RegistryInputTypeMeta)
229+
class InputType(InputTypeBase):
230+
abstract = True
231+
232+
return InputType
233+
234+
def _register_mutation(self, mutation_name, mutation):
235+
assert mutation_name not in self._mutations, \
236+
'There is already a registered mutation named "{}".'.format(mutation_name)
237+
238+
self._mutations[mutation_name] = mutation
239+
240+
@property
241+
def Mutations(self):
242+
if not self._mutations:
243+
raise TypeError("No mutations have been registered.")
244+
245+
mutations = OrderedDict()
246+
for k in sorted(self._mutations.keys()):
247+
mutations[k] = self._mutations[k]()
248+
249+
return GraphQLObjectType(
250+
name='Mutations',
251+
fields=mutations
252+
)
253+
208254
def _create_is_type_of(self, type):
209255
return partial(self._is_type_of, type)
210256

@@ -261,7 +307,7 @@ def types(self, *names):
261307
return self[names]
262308

263309
def with_resolved_types(self, thunk):
264-
assert isinstance(thunk, callable)
310+
assert callable(thunk)
265311
return partial(thunk, self._proxy)
266312

267313

@@ -273,6 +319,9 @@ def __getitem__(self, item):
273319
return self._registry[item]()
274320

275321
def __getattr__(self, item):
322+
if item.startswith('_'):
323+
raise AttributeError(item)
324+
276325
return self._registry[item]()
277326

278327

tests/test_mutation.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from graphql.core import graphql
2+
from epoxy import TypeRegistry
3+
4+
5+
def test_simple_mutation():
6+
R = TypeRegistry()
7+
8+
class SimpleAddition(R.Mutation):
9+
class Input:
10+
a = R.Int
11+
b = R.Int
12+
13+
class Output:
14+
sum = R.Int
15+
16+
def execute(self, obj, input, info):
17+
return self.Output(sum=input.a + input.b)
18+
19+
# Dummy query -- does nothing.
20+
class Query(R.ObjectType):
21+
foo = R.String
22+
23+
Schema = R.Schema(R.Query, R.Mutations)
24+
25+
mutation_query = '''
26+
mutation testSimpleAdd {
27+
simpleAddition(input: {a: 5, b: 10}) {
28+
sum
29+
}
30+
}
31+
'''
32+
33+
result = graphql(Schema, mutation_query)
34+
assert not result.errors
35+
assert result.data == {
36+
'simpleAddition': {
37+
'sum': 15
38+
}
39+
}

0 commit comments

Comments
 (0)