diff --git a/graphene/__init__.py b/graphene/__init__.py index 123bebbc..ad3b3b10 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -14,7 +14,8 @@ from graphene.core.schema import ( from graphene.core.types import ( ObjectType, - Interface + Interface, + Mutation, ) from graphene.core.fields import ( @@ -31,3 +32,8 @@ from graphene.core.fields import ( from graphene.decorators import ( resolve_only_args ) + +__all__ = ['Enum', 'Argument', 'String', 'Int', 'ID', 'signals', 'Schema', + 'ObjectType', 'Interface', 'Mutation', 'Field', 'StringField', + 'IntField', 'BooleanField', 'IDField', 'ListField', 'NonNullField', + 'FloatField', 'resolve_only_args'] diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 61a1c640..677c7b65 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -51,14 +51,18 @@ class Field(object): cls._meta.add_field(self) def resolve(self, instance, args, info): - resolve_fn = self.get_resolve_fn() + 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) else: return getattr(instance, self.field_name, None) - def get_resolve_fn(self): - if self.resolve_fn: + def get_resolve_fn(self, schema): + object_type = self.get_object_type(schema) + if object_type and object_type._meta.mutation: + return object_type.mutate + elif self.resolve_fn: return self.resolve_fn else: custom_resolve_fn_name = 'resolve_%s' % self.field_name @@ -125,7 +129,7 @@ class Field(object): raise Exception("Internal type for field %s is None" % self) description = self.description - resolve_fn = self.get_resolve_fn() + resolve_fn = self.get_resolve_fn(schema) if resolve_fn: description = resolve_fn.__doc__ or description diff --git a/graphene/core/options.py b/graphene/core/options.py index a4625889..1c81f1cb 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -1,7 +1,7 @@ from graphene.utils import cached_property from collections import OrderedDict, namedtuple -DEFAULT_NAMES = ('description', 'name', 'interface', +DEFAULT_NAMES = ('description', 'name', 'interface', 'mutation', 'type_name', 'interfaces', 'proxy') @@ -11,6 +11,7 @@ class Options(object): self.meta = meta self.local_fields = [] self.interface = False + self.mutation = False self.proxy = False self.interfaces = [] self.parents = [] diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 817bbc8c..8fa3d340 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -43,6 +43,15 @@ class Schema(object): self._query = query self._query_type = query and query.internal_type(self) + @property + def mutation(self): + return self._mutation + + @mutation.setter + def mutation(self, mutation): + self._mutation = mutation + self._mutation_type = mutation and mutation.internal_type(self) + @property def executor(self): if not self._executor: @@ -57,7 +66,7 @@ class Schema(object): def schema(self): if not self._query_type: raise Exception('You have to define a base query type') - return GraphQLSchema(self, query=self._query_type, mutation=self.mutation) + return GraphQLSchema(self, query=self._query_type, mutation=self._mutation_type) def associate_internal_type(self, internal_type, object_type): self._internal_types[internal_type.name] = object_type diff --git a/graphene/core/types.py b/graphene/core/types.py index 00f19640..0de9ead4 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -55,6 +55,10 @@ class ObjectTypeMeta(type): # Add all attributes to the class. for obj_name, obj in attrs.items(): new_class.add_to_class(obj_name, obj) + + if new_class._meta.mutation: + assert hasattr(new_class, 'mutate'), "All mutations must implement mutate method" + new_class.add_extra_fields() new_fields = new_class._meta.local_fields diff --git a/tests/core/test_fields.py b/tests/core/test_fields.py index 10f1d859..c03a2532 100644 --- a/tests/core/test_fields.py +++ b/tests/core/test_fields.py @@ -87,7 +87,7 @@ def test_field_resolve(): f = StringField(required=True, resolve=lambda *args: 'RESOLVED') f.contribute_to_class(ot, 'field_name') field_type = f.internal_field(schema) - assert 'RESOLVED' == field_type.resolver(ot, 2, 3) + assert 'RESOLVED' == field_type.resolver(ot, None, None) def test_field_resolve_type_custom(): diff --git a/tests/core/test_mutations.py b/tests/core/test_mutations.py new file mode 100644 index 00000000..25b94161 --- /dev/null +++ b/tests/core/test_mutations.py @@ -0,0 +1,54 @@ +import graphene +from py.test import raises +from graphene.core.schema import Schema + +my_id = 0 + + +class Query(graphene.ObjectType): + base = graphene.StringField() + + +class ChangeNumber(graphene.Mutation): + '''Result mutation''' + class Input: + id = graphene.IntField(required=True) + + result = graphene.StringField() + + @classmethod + def mutate(cls, instance, args, info): + global my_id + my_id = 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_mutate(): + query = ''' + mutation M{ + first: changeNumber { + result + }, + second: changeNumber { + result + } + } + ''' + expected = { + 'first': { + 'result': '1', + }, + 'second': { + 'result': '2', + } + } + result = schema.execute(query, root=object()) + assert not result.errors + assert result.data == expected diff --git a/tests/core/test_query.py b/tests/core/test_query.py index 2f46f534..33cc60cf 100644 --- a/tests/core/test_query.py +++ b/tests/core/test_query.py @@ -50,7 +50,7 @@ Human_type = Human.internal_type(schema) def test_type(): - assert Human._meta.fields_map['name'].resolve(Human(object()), 1, 2) == 'Peter' + assert Human._meta.fields_map['name'].resolve(Human(object()), None, None) == 'Peter' def test_query():