mirror of
https://github.com/graphql-python/graphene.git
synced 2025-02-02 20:54:16 +03:00
First working version with Django.
This commit is contained in:
parent
c79097879d
commit
2e8707aee6
0
graphene/contrib/__init__.py
Normal file
0
graphene/contrib/__init__.py
Normal file
4
graphene/contrib/django/__init__.py
Normal file
4
graphene/contrib/django/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from graphene.contrib.django.types import (
|
||||||
|
DjangoObjectType,
|
||||||
|
DjangoNode
|
||||||
|
)
|
40
graphene/contrib/django/converter.py
Normal file
40
graphene/contrib/django/converter.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from singledispatch import singledispatch
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from graphene.core.fields import (
|
||||||
|
StringField,
|
||||||
|
IDField,
|
||||||
|
IntField,
|
||||||
|
BooleanField,
|
||||||
|
FloatField,
|
||||||
|
)
|
||||||
|
|
||||||
|
@singledispatch
|
||||||
|
def convert_django_field(field):
|
||||||
|
raise Exception("Don't know how to convert the Django field %s"%field)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_django_field.register(models.CharField)
|
||||||
|
def _(field):
|
||||||
|
return StringField(description=field.help_text)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_django_field.register(models.AutoField)
|
||||||
|
def _(field):
|
||||||
|
return IDField(description=field.help_text)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_django_field.register(models.BigIntegerField)
|
||||||
|
@convert_django_field.register(models.IntegerField)
|
||||||
|
def _(field):
|
||||||
|
return IntField(description=field.help_text)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_django_field.register(models.BooleanField)
|
||||||
|
def _(field):
|
||||||
|
return BooleanField(description=field.help_text)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_django_field.register(models.FloatField)
|
||||||
|
def _(field):
|
||||||
|
return FloatField(description=field.help_text)
|
22
graphene/contrib/django/options.py
Normal file
22
graphene/contrib/django/options.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import inspect
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from graphene.core.options import Options
|
||||||
|
|
||||||
|
VALID_ATTRS = ('model', 'only_fields')
|
||||||
|
|
||||||
|
class DjangoOptions(Options):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.model = None
|
||||||
|
super(DjangoOptions, self).__init__(*args, **kwargs)
|
||||||
|
self.valid_attrs += VALID_ATTRS
|
||||||
|
self.only_fields = None
|
||||||
|
|
||||||
|
def contribute_to_class(self, cls, name):
|
||||||
|
super(DjangoOptions, self).contribute_to_class(cls, name)
|
||||||
|
if self.proxy:
|
||||||
|
return
|
||||||
|
if not self.model:
|
||||||
|
raise Exception('Django ObjectType %s must have a model in the Meta attr' % cls)
|
||||||
|
elif not inspect.isclass(self.model) or not issubclass(self.model, models.Model):
|
||||||
|
raise Exception('Provided model in %s is not a Django model' % cls)
|
31
graphene/contrib/django/types.py
Normal file
31
graphene/contrib/django/types.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import six
|
||||||
|
|
||||||
|
from graphene.core.types import ObjectTypeMeta, ObjectType
|
||||||
|
from graphene.contrib.django.options import DjangoOptions
|
||||||
|
from graphene.contrib.django.converter import convert_django_field
|
||||||
|
|
||||||
|
from graphene.relay import Node
|
||||||
|
|
||||||
|
class DjangoObjectTypeMeta(ObjectTypeMeta):
|
||||||
|
options_cls = DjangoOptions
|
||||||
|
def add_extra_fields(cls):
|
||||||
|
if not cls._meta.model:
|
||||||
|
return
|
||||||
|
|
||||||
|
only_fields = cls._meta.only_fields
|
||||||
|
# print cls._meta.model._meta._get_fields(forward=False, reverse=True, include_hidden=True)
|
||||||
|
for field in cls._meta.model._meta.fields:
|
||||||
|
if only_fields and field.name not in only_fields:
|
||||||
|
continue
|
||||||
|
converted_field = convert_django_field(field)
|
||||||
|
cls.add_to_class(field.name, converted_field)
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoObjectType(six.with_metaclass(DjangoObjectTypeMeta, ObjectType)):
|
||||||
|
class Meta:
|
||||||
|
proxy = True
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoNode(six.with_metaclass(DjangoObjectTypeMeta, Node)):
|
||||||
|
class Meta:
|
||||||
|
proxy = True
|
|
@ -8,8 +8,10 @@ from graphql.core.type import (
|
||||||
GraphQLBoolean,
|
GraphQLBoolean,
|
||||||
GraphQLID,
|
GraphQLID,
|
||||||
GraphQLArgument,
|
GraphQLArgument,
|
||||||
|
GraphQLFloat,
|
||||||
)
|
)
|
||||||
from graphene.utils import cached_property
|
from graphene.utils import cached_property
|
||||||
|
from graphene.core.types import ObjectType
|
||||||
|
|
||||||
class Field(object):
|
class Field(object):
|
||||||
def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args):
|
def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args):
|
||||||
|
@ -44,7 +46,6 @@ class Field(object):
|
||||||
return resolve_fn(instance, args, info)
|
return resolve_fn(instance, args, info)
|
||||||
|
|
||||||
def get_object_type(self):
|
def get_object_type(self):
|
||||||
from graphene.core.types import ObjectType
|
|
||||||
field_type = self.field_type
|
field_type = self.field_type
|
||||||
_is_class = inspect.isclass(field_type)
|
_is_class = inspect.isclass(field_type)
|
||||||
if _is_class and issubclass(field_type, ObjectType):
|
if _is_class and issubclass(field_type, ObjectType):
|
||||||
|
@ -143,6 +144,10 @@ class IDField(TypeField):
|
||||||
field_type = GraphQLID
|
field_type = GraphQLID
|
||||||
|
|
||||||
|
|
||||||
|
class FloatField(TypeField):
|
||||||
|
field_type = GraphQLFloat
|
||||||
|
|
||||||
|
|
||||||
class ListField(Field):
|
class ListField(Field):
|
||||||
def type_wrapper(self, field_type):
|
def type_wrapper(self, field_type):
|
||||||
return GraphQLList(field_type)
|
return GraphQLList(field_type)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from graphene.env import get_global_schema
|
||||||
from graphene.utils import cached_property
|
from graphene.utils import cached_property
|
||||||
|
|
||||||
DEFAULT_NAMES = ('description', 'name', 'interface', 'schema',
|
DEFAULT_NAMES = ('description', 'name', 'interface', 'schema',
|
||||||
'type_name', 'interfaces', 'proxy')
|
'type_name', 'interfaces', 'proxy')
|
||||||
|
|
||||||
|
|
||||||
class Options(object):
|
class Options(object):
|
||||||
|
@ -14,6 +14,7 @@ class Options(object):
|
||||||
self.schema = schema
|
self.schema = schema
|
||||||
self.interfaces = []
|
self.interfaces = []
|
||||||
self.parents = []
|
self.parents = []
|
||||||
|
self.valid_attrs = DEFAULT_NAMES
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
def contribute_to_class(self, cls, name):
|
||||||
cls._meta = self
|
cls._meta = self
|
||||||
|
@ -36,7 +37,7 @@ class Options(object):
|
||||||
# over it, so we loop over the *original* dictionary instead.
|
# over it, so we loop over the *original* dictionary instead.
|
||||||
if name.startswith('_'):
|
if name.startswith('_'):
|
||||||
del meta_attrs[name]
|
del meta_attrs[name]
|
||||||
for attr_name in DEFAULT_NAMES:
|
for attr_name in self.valid_attrs:
|
||||||
if attr_name in meta_attrs:
|
if attr_name in meta_attrs:
|
||||||
setattr(self, attr_name, meta_attrs.pop(attr_name))
|
setattr(self, attr_name, meta_attrs.pop(attr_name))
|
||||||
self.original_attrs[attr_name] = getattr(self, attr_name)
|
self.original_attrs[attr_name] = getattr(self, attr_name)
|
||||||
|
@ -44,9 +45,13 @@ class Options(object):
|
||||||
setattr(self, attr_name, getattr(self.meta, attr_name))
|
setattr(self, attr_name, getattr(self.meta, attr_name))
|
||||||
self.original_attrs[attr_name] = getattr(self, attr_name)
|
self.original_attrs[attr_name] = getattr(self, attr_name)
|
||||||
|
|
||||||
|
del self.valid_attrs
|
||||||
|
|
||||||
# Any leftover attributes must be invalid.
|
# Any leftover attributes must be invalid.
|
||||||
if meta_attrs != {}:
|
if meta_attrs != {}:
|
||||||
raise TypeError("'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys()))
|
raise TypeError("'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys()))
|
||||||
|
else:
|
||||||
|
self.proxy = False
|
||||||
|
|
||||||
if self.interfaces != [] and self.interface:
|
if self.interfaces != [] and self.interface:
|
||||||
raise Exception("A interface cannot inherit from interfaces")
|
raise Exception("A interface cannot inherit from interfaces")
|
||||||
|
|
|
@ -11,9 +11,11 @@ from graphene.core.options import Options
|
||||||
|
|
||||||
|
|
||||||
class ObjectTypeMeta(type):
|
class ObjectTypeMeta(type):
|
||||||
|
options_cls = Options
|
||||||
|
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(cls, name, bases, attrs):
|
||||||
super_new = super(ObjectTypeMeta, cls).__new__
|
super_new = super(ObjectTypeMeta, cls).__new__
|
||||||
parents = [b for b in bases if isinstance(b, ObjectTypeMeta)]
|
parents = [b for b in bases if isinstance(b, cls)]
|
||||||
if not parents:
|
if not parents:
|
||||||
# If this isn't a subclass of Model, don't do anything special.
|
# If this isn't a subclass of Model, don't do anything special.
|
||||||
return super_new(cls, name, bases, attrs)
|
return super_new(cls, name, bases, attrs)
|
||||||
|
@ -25,20 +27,26 @@ class ObjectTypeMeta(type):
|
||||||
'__doc__': doc
|
'__doc__': doc
|
||||||
})
|
})
|
||||||
attr_meta = attrs.pop('Meta', None)
|
attr_meta = attrs.pop('Meta', None)
|
||||||
|
proxy = None
|
||||||
if not attr_meta:
|
if not attr_meta:
|
||||||
meta = getattr(new_class, 'Meta', None)
|
meta = None
|
||||||
|
# meta = getattr(new_class, 'Meta', None)
|
||||||
else:
|
else:
|
||||||
meta = attr_meta
|
meta = attr_meta
|
||||||
|
|
||||||
base_meta = getattr(new_class, '_meta', None)
|
base_meta = getattr(new_class, '_meta', None)
|
||||||
|
|
||||||
schema = (base_meta and base_meta.schema)
|
schema = (base_meta and base_meta.schema)
|
||||||
|
|
||||||
new_class.add_to_class('_meta', Options(meta, schema))
|
new_class.add_to_class('_meta', new_class.options_cls(meta, schema))
|
||||||
|
|
||||||
if base_meta and base_meta.proxy:
|
if base_meta and base_meta.proxy:
|
||||||
new_class._meta.interface = base_meta.interface
|
new_class._meta.interface = base_meta.interface
|
||||||
|
|
||||||
# Add all attributes to the class.
|
# Add all attributes to the class.
|
||||||
for obj_name, obj in attrs.items():
|
for obj_name, obj in attrs.items():
|
||||||
new_class.add_to_class(obj_name, obj)
|
new_class.add_to_class(obj_name, obj)
|
||||||
|
new_class.add_extra_fields()
|
||||||
|
|
||||||
new_fields = new_class._meta.local_fields
|
new_fields = new_class._meta.local_fields
|
||||||
field_names = {f.field_name for f in new_fields}
|
field_names = {f.field_name for f in new_fields}
|
||||||
|
@ -71,6 +79,9 @@ class ObjectTypeMeta(type):
|
||||||
new_class._prepare()
|
new_class._prepare()
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
|
def add_extra_fields(cls):
|
||||||
|
pass
|
||||||
|
|
||||||
def _prepare(cls):
|
def _prepare(cls):
|
||||||
signals.class_prepared.send(cls)
|
signals.class_prepared.send(cls)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import collections
|
|
||||||
|
|
||||||
from graphql_relay.node.node import (
|
from graphql_relay.node.node import (
|
||||||
globalIdField
|
globalIdField
|
||||||
)
|
)
|
||||||
|
@ -8,7 +6,6 @@ from graphql_relay.connection.connection import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from graphene import signals
|
from graphene import signals
|
||||||
|
|
||||||
from graphene.core.fields import NativeField
|
from graphene.core.fields import NativeField
|
||||||
from graphene.relay.utils import get_relay
|
from graphene.relay.utils import get_relay
|
||||||
from graphene.relay.relay import Relay
|
from graphene.relay.relay import Relay
|
||||||
|
@ -18,10 +15,9 @@ from graphene.relay.relay import Relay
|
||||||
def object_type_created(object_type):
|
def object_type_created(object_type):
|
||||||
relay = get_relay(object_type._meta.schema)
|
relay = get_relay(object_type._meta.schema)
|
||||||
if relay and issubclass(object_type, relay.Node):
|
if relay and issubclass(object_type, relay.Node):
|
||||||
|
if object_type._meta.proxy:
|
||||||
|
return
|
||||||
type_name = object_type._meta.type_name
|
type_name = object_type._meta.type_name
|
||||||
# def getId(*args, **kwargs):
|
|
||||||
# print '**GET ID', args, kwargs
|
|
||||||
# return 2
|
|
||||||
field = NativeField(globalIdField(type_name))
|
field = NativeField(globalIdField(type_name))
|
||||||
object_type.add_to_class('id', field)
|
object_type.add_to_class('id', field)
|
||||||
assert hasattr(object_type, 'get_node'), 'get_node classmethod not found in %s Node' % type_name
|
assert hasattr(object_type, 'get_node'), 'get_node classmethod not found in %s Node' % type_name
|
||||||
|
|
|
@ -25,7 +25,7 @@ def create_node_definitions(getNode=None, getNodeType=getNodeType, schema=None):
|
||||||
getNode = getNode or getSchemaNode(schema)
|
getNode = getNode or getSchemaNode(schema)
|
||||||
_nodeDefinitions = nodeDefinitions(getNode, getNodeType)
|
_nodeDefinitions = nodeDefinitions(getNode, getNodeType)
|
||||||
|
|
||||||
_Interface = getattr(schema,'Interface', Interface)
|
_Interface = getattr(schema, 'Interface', Interface)
|
||||||
|
|
||||||
class Node(_Interface):
|
class Node(_Interface):
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
0
tests/contrib_django/__init__.py
Normal file
0
tests/contrib_django/__init__.py
Normal file
17
tests/contrib_django/data.py
Normal file
17
tests/contrib_django/data.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from .models import Reporter, Article
|
||||||
|
|
||||||
|
r = Reporter(first_name='John', last_name='Smith', email='john@example.com')
|
||||||
|
r.save()
|
||||||
|
|
||||||
|
r2 = Reporter(first_name='Paul', last_name='Jones', email='paul@example.com')
|
||||||
|
r2.save()
|
||||||
|
|
||||||
|
a = Article(id=None, headline="This is a test", pub_date=date(2005, 7, 27), reporter=r)
|
||||||
|
a.save()
|
||||||
|
|
||||||
|
new_article = r.articles.create(headline="John's second story", pub_date=date(2005, 7, 29))
|
||||||
|
|
||||||
|
new_article2 = Article(headline="Paul's story", pub_date=date(2006, 1, 17))
|
||||||
|
r.articles.add(new_article2)
|
43
tests/contrib_django/models.py
Normal file
43
tests/contrib_django/models.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
import django
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
settings.configure(
|
||||||
|
DATABASES={
|
||||||
|
'INSTALLED_APPS': [
|
||||||
|
'graphql.contrib.django',
|
||||||
|
],
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': 'db_test.sqlite',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class Reporter(models.Model):
|
||||||
|
first_name = models.CharField(max_length=30)
|
||||||
|
last_name = models.CharField(max_length=30)
|
||||||
|
email = models.EmailField()
|
||||||
|
|
||||||
|
def __str__(self): # __unicode__ on Python 2
|
||||||
|
return "%s %s" % (self.first_name, self.last_name)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = 'graphql'
|
||||||
|
|
||||||
|
class Article(models.Model):
|
||||||
|
headline = models.CharField(max_length=100)
|
||||||
|
pub_date = models.DateField()
|
||||||
|
reporter = models.ForeignKey(Reporter, related_name='articles')
|
||||||
|
|
||||||
|
def __str__(self): # __unicode__ on Python 2
|
||||||
|
return self.headline
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('headline',)
|
||||||
|
app_label = 'graphql'
|
||||||
|
|
||||||
|
|
||||||
|
django.setup()
|
115
tests/contrib_django/test_schema.py
Normal file
115
tests/contrib_django/test_schema.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
from py.test import raises
|
||||||
|
from collections import namedtuple
|
||||||
|
from pytest import raises
|
||||||
|
import graphene
|
||||||
|
from graphene import relay
|
||||||
|
from graphene.contrib.django import (
|
||||||
|
DjangoObjectType,
|
||||||
|
DjangoNode
|
||||||
|
)
|
||||||
|
from .models import Reporter, Article
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_raise_if_no_model():
|
||||||
|
with raises(Exception) as excinfo:
|
||||||
|
class Character1(DjangoObjectType):
|
||||||
|
pass
|
||||||
|
assert 'model in the Meta' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_raise_if_model_is_invalid():
|
||||||
|
with raises(Exception) as excinfo:
|
||||||
|
class Character2(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = 1
|
||||||
|
assert 'not a Django model' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_map_fields():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
reporter = graphene.Field(ReporterType)
|
||||||
|
|
||||||
|
def resolve_reporter(self, *args, **kwargs):
|
||||||
|
return ReporterType(Reporter(first_name='ABA', last_name='X'))
|
||||||
|
|
||||||
|
query = '''
|
||||||
|
query ReporterQuery {
|
||||||
|
reporter {
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
expected = {
|
||||||
|
'reporter': {
|
||||||
|
'first_name': 'ABA',
|
||||||
|
'last_name': 'X',
|
||||||
|
'email': ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Schema = graphene.Schema(query=Query)
|
||||||
|
result = Schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_map_only_few_fields():
|
||||||
|
class Reporter2(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
only_fields = ('id', 'email')
|
||||||
|
assert Reporter2._meta.fields_map.keys() == ['id', 'email']
|
||||||
|
|
||||||
|
def test_should_node():
|
||||||
|
class ReporterNodeType(DjangoNode):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_node(cls, id):
|
||||||
|
return ReporterNodeType(Reporter(id=2, first_name='Cookie Monster'))
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
node = relay.NodeField()
|
||||||
|
reporter = graphene.Field(ReporterNodeType)
|
||||||
|
|
||||||
|
def resolve_reporter(self, *args, **kwargs):
|
||||||
|
return ReporterNodeType(Reporter(id=1, first_name='ABA', last_name='X'))
|
||||||
|
|
||||||
|
query = '''
|
||||||
|
query ReporterQuery {
|
||||||
|
reporter {
|
||||||
|
id,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
aCustomNode: node(id:"UmVwb3J0ZXJOb2RlVHlwZToy") {
|
||||||
|
id
|
||||||
|
... on ReporterNodeType {
|
||||||
|
first_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
expected = {
|
||||||
|
'reporter': {
|
||||||
|
'id': 'UmVwb3J0ZXJOb2RlVHlwZTox',
|
||||||
|
'first_name': 'ABA',
|
||||||
|
'last_name': 'X',
|
||||||
|
'email': ''
|
||||||
|
},
|
||||||
|
'aCustomNode': {
|
||||||
|
'id': 'UmVwb3J0ZXJOb2RlVHlwZToy',
|
||||||
|
'first_name': 'Cookie Monster'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Schema = graphene.Schema(query=Query)
|
||||||
|
result = Schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
Loading…
Reference in New Issue
Block a user