mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-01-27 17:54:11 +03:00
Merge pull request #186 from patrick91/feature/rest-framework
Support for Django Rest Framework serializers
This commit is contained in:
commit
6ad64ddbef
|
@ -11,4 +11,5 @@ Contents:
|
|||
filtering
|
||||
authorization
|
||||
debug
|
||||
rest-framework
|
||||
introspection
|
||||
|
|
21
docs/rest-framework.rst
Normal file
21
docs/rest-framework.rst
Normal file
|
@ -0,0 +1,21 @@
|
|||
Integration with Django Rest Framework
|
||||
======================================
|
||||
|
||||
You can re-use your Django Rest Framework serializer with
|
||||
graphene django.
|
||||
|
||||
|
||||
Mutation
|
||||
--------
|
||||
|
||||
You can create a Mutation based on a serializer by using the
|
||||
`SerializerMutation` base class:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||
|
||||
class MyAwesomeMutation(SerializerMutation):
|
||||
class Meta:
|
||||
serializer_class = MySerializer
|
||||
|
0
graphene_django/rest_framework/__init__.py
Normal file
0
graphene_django/rest_framework/__init__.py
Normal file
129
graphene_django/rest_framework/mutation.py
Normal file
129
graphene_django/rest_framework/mutation.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
|
||||
import six
|
||||
import graphene
|
||||
from graphene.types import Argument, Field
|
||||
from graphene.types.mutation import Mutation, MutationMeta
|
||||
from graphene.types.objecttype import (
|
||||
ObjectTypeMeta,
|
||||
merge,
|
||||
yank_fields_from_attrs
|
||||
)
|
||||
from graphene.types.options import Options
|
||||
from graphene.types.utils import get_field_as
|
||||
from graphene.utils.is_base_type import is_base_type
|
||||
|
||||
from .serializer_converter import (
|
||||
convert_serializer_to_input_type,
|
||||
convert_serializer_field
|
||||
)
|
||||
from .types import ErrorType
|
||||
|
||||
|
||||
class SerializerMutationOptions(Options):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, serializer_class=None, **kwargs)
|
||||
|
||||
|
||||
class SerializerMutationMeta(MutationMeta):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
if not is_base_type(bases, SerializerMutationMeta):
|
||||
return type.__new__(cls, name, bases, attrs)
|
||||
|
||||
options = Options(
|
||||
attrs.pop('Meta', None),
|
||||
name=name,
|
||||
description=attrs.pop('__doc__', None),
|
||||
serializer_class=None,
|
||||
local_fields=None,
|
||||
only_fields=(),
|
||||
exclude_fields=(),
|
||||
interfaces=(),
|
||||
registry=None
|
||||
)
|
||||
|
||||
if not options.serializer_class:
|
||||
raise Exception('Missing serializer_class')
|
||||
|
||||
cls = ObjectTypeMeta.__new__(
|
||||
cls, name, bases, dict(attrs, _meta=options)
|
||||
)
|
||||
|
||||
serializer_fields = cls.fields_for_serializer(options)
|
||||
options.serializer_fields = yank_fields_from_attrs(
|
||||
serializer_fields,
|
||||
_as=Field,
|
||||
)
|
||||
|
||||
options.fields = merge(
|
||||
options.interface_fields, options.serializer_fields,
|
||||
options.base_fields, options.local_fields,
|
||||
{'errors': get_field_as(cls.errors, Field)}
|
||||
)
|
||||
|
||||
cls.Input = convert_serializer_to_input_type(options.serializer_class)
|
||||
|
||||
cls.Field = partial(
|
||||
Field,
|
||||
cls,
|
||||
resolver=cls.mutate,
|
||||
input=Argument(cls.Input, required=True)
|
||||
)
|
||||
|
||||
return cls
|
||||
|
||||
@staticmethod
|
||||
def fields_for_serializer(options):
|
||||
serializer = options.serializer_class()
|
||||
|
||||
only_fields = options.only_fields
|
||||
|
||||
already_created_fields = {
|
||||
name
|
||||
for name, _ in options.local_fields.items()
|
||||
}
|
||||
|
||||
fields = OrderedDict()
|
||||
for name, field in serializer.fields.items():
|
||||
is_not_in_only = only_fields and name not in only_fields
|
||||
is_excluded = (
|
||||
name in options.exclude_fields or
|
||||
name in already_created_fields
|
||||
)
|
||||
|
||||
if is_not_in_only or is_excluded:
|
||||
continue
|
||||
|
||||
fields[name] = convert_serializer_field(field, is_input=False)
|
||||
return fields
|
||||
|
||||
|
||||
class SerializerMutation(six.with_metaclass(SerializerMutationMeta, Mutation)):
|
||||
errors = graphene.List(
|
||||
ErrorType,
|
||||
description='May contain more than one error for '
|
||||
'same field.'
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def mutate(cls, instance, args, request, info):
|
||||
input = args.get('input')
|
||||
|
||||
serializer = cls._meta.serializer_class(data=dict(input))
|
||||
|
||||
if serializer.is_valid():
|
||||
return cls.perform_mutate(serializer, info)
|
||||
else:
|
||||
errors = [
|
||||
ErrorType(field=key, messages=value)
|
||||
for key, value in serializer.errors.items()
|
||||
]
|
||||
|
||||
return cls(errors=errors)
|
||||
|
||||
@classmethod
|
||||
def perform_mutate(cls, serializer, info):
|
||||
obj = serializer.save()
|
||||
|
||||
return cls(errors=[], **obj)
|
124
graphene_django/rest_framework/serializer_converter.py
Normal file
124
graphene_django/rest_framework/serializer_converter.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from rest_framework import serializers
|
||||
|
||||
import graphene
|
||||
|
||||
from ..registry import get_global_registry
|
||||
from ..utils import import_single_dispatch
|
||||
from .types import DictType
|
||||
|
||||
singledispatch = import_single_dispatch()
|
||||
|
||||
|
||||
def convert_serializer_to_input_type(serializer_class):
|
||||
serializer = serializer_class()
|
||||
|
||||
items = {
|
||||
name: convert_serializer_field(field)
|
||||
for name, field in serializer.fields.items()
|
||||
}
|
||||
|
||||
return type(
|
||||
'{}Input'.format(serializer.__class__.__name__),
|
||||
(graphene.InputObjectType, ),
|
||||
items
|
||||
)
|
||||
|
||||
|
||||
@singledispatch
|
||||
def get_graphene_type_from_serializer_field(field):
|
||||
raise ImproperlyConfigured(
|
||||
"Don't know how to convert the serializer field %s (%s) "
|
||||
"to Graphene type" % (field, field.__class__)
|
||||
)
|
||||
|
||||
|
||||
def convert_serializer_field(field, is_input=True):
|
||||
"""
|
||||
Converts a django rest frameworks field to a graphql field
|
||||
and marks the field as required if we are creating an input type
|
||||
and the field itself is required
|
||||
"""
|
||||
|
||||
graphql_type = get_graphene_type_from_serializer_field(field)
|
||||
|
||||
args = []
|
||||
kwargs = {
|
||||
'description': field.help_text,
|
||||
'required': is_input and field.required,
|
||||
}
|
||||
|
||||
# if it is a tuple or a list it means that we are returning
|
||||
# the graphql type and the child type
|
||||
if isinstance(graphql_type, (list, tuple)):
|
||||
kwargs['of_type'] = graphql_type[1]
|
||||
graphql_type = graphql_type[0]
|
||||
|
||||
if isinstance(field, serializers.ModelSerializer):
|
||||
if is_input:
|
||||
graphql_type = convert_serializer_to_input_type(field.__class__)
|
||||
else:
|
||||
global_registry = get_global_registry()
|
||||
field_model = field.Meta.model
|
||||
args = [global_registry.get_type_for_model(field_model)]
|
||||
|
||||
return graphql_type(*args, **kwargs)
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.Field)
|
||||
def convert_serializer_field_to_string(field):
|
||||
return graphene.String
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.ModelSerializer)
|
||||
def convert_serializer_to_field(field):
|
||||
return graphene.Field
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.IntegerField)
|
||||
def convert_serializer_field_to_int(field):
|
||||
return graphene.Int
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.BooleanField)
|
||||
def convert_serializer_field_to_bool(field):
|
||||
return graphene.Boolean
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.FloatField)
|
||||
@get_graphene_type_from_serializer_field.register(serializers.DecimalField)
|
||||
def convert_serializer_field_to_float(field):
|
||||
return graphene.Float
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.DateTimeField)
|
||||
@get_graphene_type_from_serializer_field.register(serializers.DateField)
|
||||
def convert_serializer_field_to_date_time(field):
|
||||
return graphene.types.datetime.DateTime
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.TimeField)
|
||||
def convert_serializer_field_to_time(field):
|
||||
return graphene.types.datetime.Time
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.ListField)
|
||||
def convert_serializer_field_to_list(field, is_input=True):
|
||||
child_type = get_graphene_type_from_serializer_field(field.child)
|
||||
|
||||
return (graphene.List, child_type)
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.DictField)
|
||||
def convert_serializer_field_to_dict(field):
|
||||
return DictType
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.JSONField)
|
||||
def convert_serializer_field_to_jsonstring(field):
|
||||
return graphene.types.json.JSONString
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.MultipleChoiceField)
|
||||
def convert_serializer_field_to_list_of_string(field):
|
||||
return (graphene.List, graphene.String)
|
0
graphene_django/rest_framework/tests/__init__.py
Normal file
0
graphene_django/rest_framework/tests/__init__.py
Normal file
162
graphene_django/rest_framework/tests/test_field_converter.py
Normal file
162
graphene_django/rest_framework/tests/test_field_converter.py
Normal file
|
@ -0,0 +1,162 @@
|
|||
import copy
|
||||
from rest_framework import serializers
|
||||
from py.test import raises
|
||||
|
||||
import graphene
|
||||
|
||||
from ..serializer_converter import convert_serializer_field
|
||||
from ..types import DictType
|
||||
|
||||
|
||||
def _get_type(rest_framework_field, is_input=True, **kwargs):
|
||||
# prevents the following error:
|
||||
# AssertionError: The `source` argument is not meaningful when applied to a `child=` field.
|
||||
# Remove `source=` from the field declaration.
|
||||
# since we are reusing the same child in when testing the required attribute
|
||||
|
||||
if 'child' in kwargs:
|
||||
kwargs['child'] = copy.deepcopy(kwargs['child'])
|
||||
|
||||
field = rest_framework_field(**kwargs)
|
||||
|
||||
return convert_serializer_field(field, is_input=is_input)
|
||||
|
||||
|
||||
def assert_conversion(rest_framework_field, graphene_field, **kwargs):
|
||||
graphene_type = _get_type(rest_framework_field, help_text='Custom Help Text', **kwargs)
|
||||
assert isinstance(graphene_type, graphene_field)
|
||||
|
||||
graphene_type_required = _get_type(
|
||||
rest_framework_field, help_text='Custom Help Text', required=True, **kwargs
|
||||
)
|
||||
assert isinstance(graphene_type_required, graphene_field)
|
||||
|
||||
return graphene_type
|
||||
|
||||
|
||||
def test_should_unknown_rest_framework_field_raise_exception():
|
||||
with raises(Exception) as excinfo:
|
||||
convert_serializer_field(None)
|
||||
assert 'Don\'t know how to convert the serializer field' in str(excinfo.value)
|
||||
|
||||
|
||||
def test_should_char_convert_string():
|
||||
assert_conversion(serializers.CharField, graphene.String)
|
||||
|
||||
|
||||
def test_should_email_convert_string():
|
||||
assert_conversion(serializers.EmailField, graphene.String)
|
||||
|
||||
|
||||
def test_should_slug_convert_string():
|
||||
assert_conversion(serializers.SlugField, graphene.String)
|
||||
|
||||
|
||||
def test_should_url_convert_string():
|
||||
assert_conversion(serializers.URLField, graphene.String)
|
||||
|
||||
|
||||
def test_should_choice_convert_string():
|
||||
assert_conversion(serializers.ChoiceField, graphene.String, choices=[])
|
||||
|
||||
|
||||
def test_should_base_field_convert_string():
|
||||
assert_conversion(serializers.Field, graphene.String)
|
||||
|
||||
|
||||
def test_should_regex_convert_string():
|
||||
assert_conversion(serializers.RegexField, graphene.String, regex='[0-9]+')
|
||||
|
||||
|
||||
def test_should_uuid_convert_string():
|
||||
if hasattr(serializers, 'UUIDField'):
|
||||
assert_conversion(serializers.UUIDField, graphene.String)
|
||||
|
||||
|
||||
def test_should_model_convert_field():
|
||||
|
||||
class MyModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = None
|
||||
fields = '__all__'
|
||||
|
||||
assert_conversion(MyModelSerializer, graphene.Field, is_input=False)
|
||||
|
||||
|
||||
def test_should_date_time_convert_datetime():
|
||||
assert_conversion(serializers.DateTimeField, graphene.types.datetime.DateTime)
|
||||
|
||||
|
||||
def test_should_date_convert_datetime():
|
||||
assert_conversion(serializers.DateField, graphene.types.datetime.DateTime)
|
||||
|
||||
|
||||
def test_should_time_convert_time():
|
||||
assert_conversion(serializers.TimeField, graphene.types.datetime.Time)
|
||||
|
||||
|
||||
def test_should_integer_convert_int():
|
||||
assert_conversion(serializers.IntegerField, graphene.Int)
|
||||
|
||||
|
||||
def test_should_boolean_convert_boolean():
|
||||
assert_conversion(serializers.BooleanField, graphene.Boolean)
|
||||
|
||||
|
||||
def test_should_float_convert_float():
|
||||
assert_conversion(serializers.FloatField, graphene.Float)
|
||||
|
||||
|
||||
def test_should_decimal_convert_float():
|
||||
assert_conversion(serializers.DecimalField, graphene.Float, max_digits=4, decimal_places=2)
|
||||
|
||||
|
||||
def test_should_list_convert_to_list():
|
||||
class StringListField(serializers.ListField):
|
||||
child = serializers.CharField()
|
||||
|
||||
field_a = assert_conversion(
|
||||
serializers.ListField,
|
||||
graphene.List,
|
||||
child=serializers.IntegerField(min_value=0, max_value=100)
|
||||
)
|
||||
|
||||
assert field_a.of_type == graphene.Int
|
||||
|
||||
field_b = assert_conversion(StringListField, graphene.List)
|
||||
|
||||
assert field_b.of_type == graphene.String
|
||||
|
||||
|
||||
def test_should_dict_convert_dict():
|
||||
assert_conversion(serializers.DictField, DictType)
|
||||
|
||||
|
||||
def test_should_duration_convert_string():
|
||||
assert_conversion(serializers.DurationField, graphene.String)
|
||||
|
||||
|
||||
def test_should_file_convert_string():
|
||||
assert_conversion(serializers.FileField, graphene.String)
|
||||
|
||||
|
||||
def test_should_filepath_convert_string():
|
||||
assert_conversion(serializers.FilePathField, graphene.String, path='/')
|
||||
|
||||
|
||||
def test_should_ip_convert_string():
|
||||
assert_conversion(serializers.IPAddressField, graphene.String)
|
||||
|
||||
|
||||
def test_should_image_convert_string():
|
||||
assert_conversion(serializers.ImageField, graphene.String)
|
||||
|
||||
|
||||
def test_should_json_convert_jsonstring():
|
||||
assert_conversion(serializers.JSONField, graphene.types.json.JSONString)
|
||||
|
||||
|
||||
def test_should_multiplechoicefield_convert_to_list_of_string():
|
||||
field = assert_conversion(serializers.MultipleChoiceField, graphene.List, choices=[1,2,3])
|
||||
|
||||
assert field.of_type == graphene.String
|
70
graphene_django/rest_framework/tests/test_mutation.py
Normal file
70
graphene_django/rest_framework/tests/test_mutation.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
from django.db import models
|
||||
from graphene import Field
|
||||
from graphene.types.inputobjecttype import InputObjectType
|
||||
from py.test import raises
|
||||
from rest_framework import serializers
|
||||
|
||||
from ...types import DjangoObjectType
|
||||
from ..mutation import SerializerMutation
|
||||
|
||||
|
||||
class MyFakeModel(models.Model):
|
||||
cool_name = models.CharField(max_length=50)
|
||||
|
||||
|
||||
class MyModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = MyFakeModel
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class MySerializer(serializers.Serializer):
|
||||
text = serializers.CharField()
|
||||
model = MyModelSerializer()
|
||||
|
||||
|
||||
def test_needs_serializer_class():
|
||||
with raises(Exception) as exc:
|
||||
class MyMutation(SerializerMutation):
|
||||
pass
|
||||
|
||||
assert exc.value.args[0] == 'Missing serializer_class'
|
||||
|
||||
|
||||
def test_has_fields():
|
||||
class MyMutation(SerializerMutation):
|
||||
class Meta:
|
||||
serializer_class = MySerializer
|
||||
|
||||
assert 'text' in MyMutation._meta.fields
|
||||
assert 'model' in MyMutation._meta.fields
|
||||
assert 'errors' in MyMutation._meta.fields
|
||||
|
||||
|
||||
def test_has_input_fields():
|
||||
class MyMutation(SerializerMutation):
|
||||
class Meta:
|
||||
serializer_class = MySerializer
|
||||
|
||||
assert 'text' in MyMutation.Input._meta.fields
|
||||
assert 'model' in MyMutation.Input._meta.fields
|
||||
|
||||
|
||||
def test_nested_model():
|
||||
|
||||
class MyFakeModelGrapheneType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = MyFakeModel
|
||||
|
||||
class MyMutation(SerializerMutation):
|
||||
class Meta:
|
||||
serializer_class = MySerializer
|
||||
|
||||
model_field = MyMutation._meta.fields['model']
|
||||
assert isinstance(model_field, Field)
|
||||
assert model_field.type == MyFakeModelGrapheneType
|
||||
|
||||
model_input = MyMutation.Input._meta.fields['model']
|
||||
model_input_type = model_input._type.of_type
|
||||
assert issubclass(model_input_type, InputObjectType)
|
||||
assert 'cool_name' in model_input_type._meta.fields
|
12
graphene_django/rest_framework/types.py
Normal file
12
graphene_django/rest_framework/types.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
import graphene
|
||||
from graphene.types.unmountedtype import UnmountedType
|
||||
|
||||
|
||||
class ErrorType(graphene.ObjectType):
|
||||
field = graphene.String()
|
||||
messages = graphene.List(graphene.String)
|
||||
|
||||
|
||||
class DictType(UnmountedType):
|
||||
key = graphene.String()
|
||||
value = graphene.String()
|
|
@ -11,8 +11,11 @@ class LazyList(object):
|
|||
pass
|
||||
|
||||
|
||||
import django_filters # noqa
|
||||
DJANGO_FILTER_INSTALLED = True
|
||||
try:
|
||||
import django_filters # noqa
|
||||
DJANGO_FILTER_INSTALLED = True
|
||||
except ImportError:
|
||||
DJANGO_FILTER_INSTALLED = False
|
||||
|
||||
|
||||
def get_reverse_fields(model, local_field_names):
|
||||
|
|
9
setup.py
9
setup.py
|
@ -1,5 +1,10 @@
|
|||
from setuptools import find_packages, setup
|
||||
|
||||
rest_framework_require = [
|
||||
'djangorestframework==3.6.3',
|
||||
]
|
||||
|
||||
|
||||
tests_require = [
|
||||
'pytest>=2.7.2',
|
||||
'pytest-cov',
|
||||
|
@ -8,7 +13,7 @@ tests_require = [
|
|||
'pytz',
|
||||
'django-filter',
|
||||
'pytest-django==2.9.1',
|
||||
]
|
||||
] + rest_framework_require
|
||||
|
||||
setup(
|
||||
name='graphene-django',
|
||||
|
@ -53,8 +58,10 @@ setup(
|
|||
'pytest-runner',
|
||||
],
|
||||
tests_require=tests_require,
|
||||
rest_framework_require=rest_framework_require,
|
||||
extras_require={
|
||||
'test': tests_require,
|
||||
'rest_framework': rest_framework_require,
|
||||
},
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
|
|
Loading…
Reference in New Issue
Block a user