diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py new file mode 100644 index 00000000..e1e54d04 --- /dev/null +++ b/graphene/types/mutation.py @@ -0,0 +1,28 @@ +from functools import partial +import six + +from .objecttype import ObjectTypeMeta, ObjectType +from .field import Field + +from ..utils.props import props + + +class MutationMeta(ObjectTypeMeta): + + def construct_field(cls, field_attrs): + resolver = getattr(cls, 'mutate', None) + assert resolver, 'All mutations must define a mutate method in it' + return partial(Field, cls, args=field_attrs, resolver=resolver) + + def construct(cls, bases, attrs): + super(MutationMeta, cls).construct(bases, attrs) + if not cls._meta.abstract: + Input = attrs.pop('Input', None) + field_attrs = props(Input) if Input else {} + cls.Field = cls.construct_field(field_attrs) + return cls + + +class Mutation(six.with_metaclass(MutationMeta, ObjectType)): + class Meta: + abstract = True diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py new file mode 100644 index 00000000..97ac39df --- /dev/null +++ b/graphene/types/tests/test_mutation.py @@ -0,0 +1,234 @@ +import pytest + +from graphql import GraphQLObjectType, GraphQLField, GraphQLString, GraphQLInterfaceType + +from ..schema import Schema +from ..objecttype import ObjectType +from ..mutation import Mutation +from ..interface import Interface +from ..scalars import String +from ..field import Field +from ..argument import Argument + + +def test_generate_mutation_no_args(): + class MyMutation(Mutation): + '''Documentation''' + @classmethod + def mutate(cls, *args, **kwargs): + pass + + assert issubclass(MyMutation, ObjectType) + graphql_type = MyMutation._meta.graphql_type + assert isinstance(graphql_type, GraphQLObjectType) + assert graphql_type.name == "MyMutation" + assert graphql_type.description == "Documentation" + + +def test_generate_mutation_with_args(): + class MyMutation(Mutation): + '''Documentation''' + class Input: + s = String() + + @classmethod + def mutate(cls, *args, **kwargs): + pass + + graphql_type = MyMutation._meta.graphql_type + field = MyMutation.Field() + assert isinstance(graphql_type, GraphQLObjectType) + assert graphql_type.name == "MyMutation" + assert graphql_type.description == "Documentation" + assert isinstance(field, Field) + assert field.type == MyMutation._meta.graphql_type + assert 's' in field.args + assert field.args['s'].type == GraphQLString + + +def test_generate_mutation_with_meta(): + class MyMutation(Mutation): + class Meta: + name = 'MyOtherMutation' + description = 'Documentation' + + @classmethod + def mutate(cls, *args, **kwargs): + pass + + graphql_type = MyMutation._meta.graphql_type + assert isinstance(graphql_type, GraphQLObjectType) + assert graphql_type.name == "MyOtherMutation" + assert graphql_type.description == "Documentation" + + +def test_empty_mutation_has_meta(): + class MyMutation(Mutation): + @classmethod + def mutate(cls, *args, **kwargs): + pass + + assert MyMutation._meta + + +def test_mutation_raises_exception_if_no_mutate(): + with pytest.raises(AssertionError) as excinfo: + class MyMutation(Mutation): + pass + + assert "All mutations must define a mutate method in it" == str(excinfo.value) + +# def test_objecttype_inheritance(): +# class MyInheritedObjectType(ObjectType): +# inherited = Field(GraphQLString) + +# class MyObjectType(MyInheritedObjectType): +# field = Field(GraphQLString) + +# graphql_type = MyObjectType._meta.graphql_type +# fields = graphql_type.get_fields() +# assert 'field' in fields +# assert 'inherited' in fields +# assert fields['field'] > fields['inherited'] + + +# def test_objecttype_as_container_only_args(): +# container = Container("1", "2") +# assert container.field1 == "1" +# assert container.field2 == "2" + + +# def test_objecttype_as_container_args_kwargs(): +# container = Container("1", field2="2") +# assert container.field1 == "1" +# assert container.field2 == "2" + + +# def test_objecttype_as_container_few_kwargs(): +# container = Container(field2="2") +# assert container.field2 == "2" + + +# def test_objecttype_as_container_all_kwargs(): +# container = Container(field1="1", field2="2") +# assert container.field1 == "1" +# assert container.field2 == "2" + + +# def test_objecttype_as_container_extra_args(): +# with pytest.raises(IndexError) as excinfo: +# Container("1", "2", "3") + +# assert "Number of args exceeds number of fields" == str(excinfo.value) + + +# def test_objecttype_as_container_invalid_kwargs(): +# with pytest.raises(TypeError) as excinfo: +# Container(unexisting_field="3") + +# assert "'unexisting_field' is an invalid keyword argument for this function" == str(excinfo.value) + + +# def test_objecttype_reuse_graphql_type(): +# MyGraphQLType = GraphQLObjectType('MyGraphQLType', fields={ +# 'field': GraphQLField(GraphQLString) +# }) + +# class GrapheneObjectType(ObjectType): +# class Meta: +# graphql_type = MyGraphQLType + +# graphql_type = GrapheneObjectType._meta.graphql_type +# assert graphql_type == MyGraphQLType +# instance = GrapheneObjectType(field="A") +# assert instance.field == "A" + + +# def test_objecttype_add_fields_in_reused_graphql_type(): +# MyGraphQLType = GraphQLObjectType('MyGraphQLType', fields={ +# 'field': GraphQLField(GraphQLString) +# }) + +# with pytest.raises(AssertionError) as excinfo: +# class GrapheneObjectType(ObjectType): +# field = Field(GraphQLString) + +# class Meta: +# graphql_type = MyGraphQLType + +# assert """Field "MyGraphQLType.field" can only be mounted in ObjectType or Interface, received GrapheneObjectType.""" == str(excinfo.value) + + +# def test_objecttype_graphql_interface(): +# MyInterface = GraphQLInterfaceType('MyInterface', fields={ +# 'field': GraphQLField(GraphQLString) +# }) + +# class GrapheneObjectType(ObjectType): +# class Meta: +# interfaces = [MyInterface] + +# graphql_type = GrapheneObjectType._meta.graphql_type +# assert graphql_type.get_interfaces() == (MyInterface, ) +# # assert graphql_type.is_type_of(MyInterface, None, None) +# fields = graphql_type.get_fields() +# assert 'field' in fields + + +# def test_objecttype_graphene_interface(): +# class GrapheneInterface(Interface): +# field = Field(GraphQLString) + +# class GrapheneObjectType(ObjectType): +# class Meta: +# interfaces = [GrapheneInterface] + +# graphql_type = GrapheneObjectType._meta.graphql_type +# assert graphql_type.get_interfaces() == (GrapheneInterface._meta.graphql_type, ) +# assert graphql_type.is_type_of(GrapheneObjectType(), None, None) +# fields = graphql_type.get_fields() +# assert 'field' in fields + + +# def test_objecttype_graphene_interface_extended(): +# class GrapheneInterface(Interface): +# field = Field(GraphQLString) + +# class GrapheneObjectType(ObjectType): +# class Meta: +# interfaces = [GrapheneInterface] + +# schema = Schema(query=GrapheneObjectType) +# assert str(schema) == """ +# schema { +# query: GrapheneObjectType +# } + +# interface GrapheneInterface { +# field: String +# } + +# type GrapheneObjectType implements GrapheneInterface { +# field: String +# } +# """.lstrip() +# GrapheneInterface._meta.graphql_type.add_field(Field(String, name='dynamic')) +# # GrapheneObjectType._meta.graphql_type._field_map = None +# assert GrapheneInterface._meta.graphql_type.get_fields().keys() == ['field', 'dynamic'] +# assert GrapheneObjectType._meta.graphql_type.get_fields().keys() == ['field', 'dynamic'] +# schema.rebuild() +# assert str(schema) == """ +# schema { +# query: GrapheneObjectType +# } + +# interface GrapheneInterface { +# field: String +# dynamic: String +# } + +# type GrapheneObjectType implements GrapheneInterface { +# field: String +# dynamic: String +# } +# """.lstrip() diff --git a/graphene/utils/props.py b/graphene/utils/props.py new file mode 100644 index 00000000..5162720e --- /dev/null +++ b/graphene/utils/props.py @@ -0,0 +1,15 @@ +class _OldClass: + pass + + +class _NewClass(object): + pass + + +_all_vars = set(dir(_OldClass) + dir(_NewClass)) + + +def props(x): + return { + key: value for key, value in vars(x).items() if key not in _all_vars + }