Improved classtypes core support

This commit is contained in:
Syrus Akbary 2015-12-02 21:11:24 -08:00
parent 398f7da24c
commit 8abcaff02b
9 changed files with 38 additions and 565 deletions

View File

@ -8,11 +8,15 @@ from graphene.core.schema import (
Schema Schema
) )
from graphene.core.types import ( from graphene.core.classtypes import (
ObjectType, ObjectType,
InputObjectType, InputObjectType,
Interface, Interface,
Mutation, Mutation,
Scalar
)
from graphene.core.types import (
BaseType, BaseType,
LazyType, LazyType,
Argument, Argument,
@ -59,6 +63,7 @@ __all__ = [
'InputObjectType', 'InputObjectType',
'Interface', 'Interface',
'Mutation', 'Mutation',
'Scalar',
'Field', 'Field',
'InputField', 'InputField',
'StringField', 'StringField',

View File

@ -0,0 +1,16 @@
from .inputobjecttype import InputObjectType
from .interface import Interface
from .mutation import Mutation
from .objecttype import ObjectType
from .options import Options
from .scalar import Scalar
from .uniontype import UnionType
__all__ = [
'InputObjectType',
'Interface',
'Mutation',
'ObjectType',
'Options',
'Scalar',
'UnionType']

View File

@ -1,72 +0,0 @@
from collections import OrderedDict
from ..utils import cached_property
DEFAULT_NAMES = ('description', 'name', 'is_interface', 'is_mutation',
'type_name', 'interfaces', 'abstract')
class Options(object):
def __init__(self, meta=None):
self.meta = meta
self.local_fields = []
self.is_interface = False
self.is_mutation = False
self.is_union = False
self.abstract = False
self.interfaces = []
self.parents = []
self.types = []
self.valid_attrs = DEFAULT_NAMES
def contribute_to_class(self, cls, name):
cls._meta = self
self.parent = cls
# First, construct the default values for these options.
self.object_name = cls.__name__
self.type_name = self.object_name
self.description = cls.__doc__
# Store the original user-defined values for each option,
# for use when serializing the model definition
self.original_attrs = {}
# Next, apply any overridden values from 'class Meta'.
if self.meta:
meta_attrs = self.meta.__dict__.copy()
for name in self.meta.__dict__:
# Ignore any private attributes that Django doesn't care about.
# NOTE: We can't modify a dictionary's contents while looping
# over it, so we loop over the *original* dictionary instead.
if name.startswith('_'):
del meta_attrs[name]
for attr_name in self.valid_attrs:
if attr_name in meta_attrs:
setattr(self, attr_name, meta_attrs.pop(attr_name))
self.original_attrs[attr_name] = getattr(self, attr_name)
elif hasattr(self.meta, attr_name):
setattr(self, attr_name, getattr(self.meta, attr_name))
self.original_attrs[attr_name] = getattr(self, attr_name)
del self.valid_attrs
# Any leftover attributes must be invalid.
if meta_attrs != {}:
raise TypeError(
"'class Meta' got invalid attribute(s): %s" %
','.join(
meta_attrs.keys()))
del self.meta
def add_field(self, field):
self.local_fields.append(field)
@cached_property
def fields(self):
return sorted(self.local_fields)
@cached_property
def fields_map(self):
return OrderedDict([(f.attname, f) for f in self.fields])

View File

@ -11,7 +11,6 @@ from graphql.core.utils.schema_printer import print_schema
from graphene import signals from graphene import signals
from .types.base import BaseType from .types.base import BaseType
from .types.objecttype import BaseObjectType
from .classtypes.base import ClassType from .classtypes.base import ClassType
@ -49,7 +48,7 @@ class Schema(object):
internal_type = object_type.internal_type(self) internal_type = object_type.internal_type(self)
self._types[object_type] = internal_type self._types[object_type] = internal_type
is_objecttype = inspect.isclass( is_objecttype = inspect.isclass(
object_type) and issubclass(object_type, BaseObjectType) object_type) and issubclass(object_type, ClassType)
if is_objecttype: if is_objecttype:
self.register(object_type) self.register(object_type)
return self._types[object_type] return self._types[object_type]
@ -91,7 +90,7 @@ class Schema(object):
if name: if name:
objecttype = self._types_names.get(name, None) objecttype = self._types_names.get(name, None)
if objecttype and inspect.isclass( if objecttype and inspect.isclass(
objecttype) and issubclass(objecttype, BaseObjectType): objecttype) and issubclass(objecttype, ClassType):
return objecttype return objecttype
def __str__(self): def __str__(self):

View File

@ -1,7 +1,9 @@
from .base import BaseType, LazyType, OrderedType from .base import BaseType, LazyType, OrderedType
from .argument import Argument, ArgumentsGroup, to_arguments from .argument import Argument, ArgumentsGroup, to_arguments
from .definitions import List, NonNull from .definitions import List, NonNull
from .objecttype import ObjectTypeMeta, BaseObjectType, Interface, ObjectType, Mutation, InputObjectType # Compatibility import
from .objecttype import Interface, ObjectType, Mutation, InputObjectType
from .scalars import String, ID, Boolean, Int, Float, Scalar from .scalars import String, ID, Boolean, Int, Float, Scalar
from .field import Field, InputField from .field import Field, InputField
@ -17,8 +19,6 @@ __all__ = [
'Field', 'Field',
'InputField', 'InputField',
'Interface', 'Interface',
'BaseObjectType',
'ObjectTypeMeta',
'ObjectType', 'ObjectType',
'Mutation', 'Mutation',
'InputObjectType', 'InputObjectType',

View File

@ -2,9 +2,6 @@ from functools import total_ordering
import six import six
from ..classtypes.base import FieldsClassType
from ..classtypes.inputobjecttype import InputObjectType as NewInputObjectType
class BaseType(object): class BaseType(object):
@ -107,11 +104,12 @@ class ArgumentType(MirroredType):
class FieldType(MirroredType): class FieldType(MirroredType):
def contribute_to_class(self, cls, name): def contribute_to_class(self, cls, name):
from ..types import BaseObjectType, InputObjectType from ..classtypes.base import FieldsClassType
if issubclass(cls, (InputObjectType, NewInputObjectType)): from ..classtypes.inputobjecttype import InputObjectType
if issubclass(cls, (InputObjectType)):
inputfield = self.as_inputfield() inputfield = self.as_inputfield()
return inputfield.contribute_to_class(cls, name) return inputfield.contribute_to_class(cls, name)
elif issubclass(cls, (BaseObjectType, FieldsClassType)): elif issubclass(cls, (FieldsClassType)):
field = self.as_field() field = self.as_field()
return field.contribute_to_class(cls, name) return field.contribute_to_class(cls, name)

View File

@ -6,8 +6,8 @@ from graphql.core.type import GraphQLField, GraphQLInputObjectField
from ...utils import to_camel_case from ...utils import to_camel_case
from ..classtypes.base import FieldsClassType from ..classtypes.base import FieldsClassType
from ..classtypes.inputobjecttype import InputObjectType as NewInputObjectType from ..classtypes.mutation import Mutation
from ..types import BaseObjectType, InputObjectType from ..classtypes.inputobjecttype import InputObjectType
from .argument import ArgumentsGroup, snake_case_args from .argument import ArgumentsGroup, snake_case_args
from .base import LazyType, MountType, OrderedType from .base import LazyType, MountType, OrderedType
from .definitions import NonNull from .definitions import NonNull
@ -34,7 +34,7 @@ class Field(OrderedType):
def contribute_to_class(self, cls, attname): def contribute_to_class(self, cls, attname):
assert issubclass( assert issubclass(
cls, (BaseObjectType, FieldsClassType)), 'Field {} cannot be mounted in {}'.format( cls, (FieldsClassType)), 'Field {} cannot be mounted in {}'.format(
self, cls) self, cls)
if not self.name: if not self.name:
self.name = to_camel_case(attname) self.name = to_camel_case(attname)
@ -71,7 +71,7 @@ class Field(OrderedType):
description = resolver.__doc__ description = resolver.__doc__
type = schema.T(self.get_type(schema)) type = schema.T(self.get_type(schema))
type_objecttype = schema.objecttype(type) type_objecttype = schema.objecttype(type)
if type_objecttype and type_objecttype._meta.is_mutation: if type_objecttype and issubclass(type_objecttype, Mutation):
assert len(arguments) == 0 assert len(arguments) == 0
arguments = type_objecttype.get_arguments() arguments = type_objecttype.get_arguments()
resolver = getattr(type_objecttype, 'mutate') resolver = getattr(type_objecttype, 'mutate')
@ -128,7 +128,7 @@ class InputField(OrderedType):
def contribute_to_class(self, cls, attname): def contribute_to_class(self, cls, attname):
assert issubclass( assert issubclass(
cls, (InputObjectType, NewInputObjectType)), 'InputField {} cannot be mounted in {}'.format( cls, (InputObjectType)), 'InputField {} cannot be mounted in {}'.format(
self, cls) self, cls)
if not self.name: if not self.name:
self.name = to_camel_case(attname) self.name = to_camel_case(attname)

View File

@ -1,282 +1,3 @@
import copy from ..classtypes import ObjectType, Interface, Mutation, InputObjectType
import inspect
from collections import OrderedDict
from functools import partial
import six __all__ = ['ObjectType', 'Interface', 'Mutation', 'InputObjectType']
from graphql.core.type import (GraphQLInputObjectType, GraphQLInterfaceType,
GraphQLObjectType, GraphQLUnionType)
from graphene import signals
from ..exceptions import SkipField
from ..options import Options
from .argument import ArgumentsGroup
from .base import BaseType
from .definitions import List, NonNull
def is_objecttype(cls):
if not issubclass(cls, BaseObjectType):
return False
_meta = getattr(cls, '_meta', None)
return not(_meta and (_meta.abstract or _meta.is_interface))
class ObjectTypeMeta(type):
options_cls = Options
def is_interface(cls, parents):
return Interface in parents
def is_mutation(cls, parents):
return issubclass(cls, Mutation)
def __new__(cls, name, bases, attrs):
super_new = super(ObjectTypeMeta, cls).__new__
parents = [b for b in bases if isinstance(b, cls)]
if not parents:
# If this isn't a subclass of Model, don't do anything special.
return super_new(cls, name, bases, attrs)
module = attrs.pop('__module__', None)
doc = attrs.pop('__doc__', None)
new_class = super_new(cls, name, bases, {
'__module__': module,
'__doc__': doc
})
attr_meta = attrs.pop('Meta', None)
abstract = getattr(attr_meta, 'abstract', False)
if not attr_meta:
meta = getattr(new_class, 'Meta', None)
else:
meta = attr_meta
base_meta = getattr(new_class, '_meta', None)
new_class.add_to_class('_meta', new_class.options_cls(meta))
new_class._meta.is_interface = new_class.is_interface(parents)
new_class._meta.is_mutation = new_class.is_mutation(parents) or (base_meta and base_meta.is_mutation)
union_types = list(filter(is_objecttype, parents))
new_class._meta.is_union = len(union_types) > 1
new_class._meta.types = union_types
assert not (
new_class._meta.is_interface and new_class._meta.is_mutation)
assert not (
new_class._meta.is_interface and new_class._meta.is_union)
# Add all attributes to the class.
for obj_name, obj in attrs.items():
new_class.add_to_class(obj_name, obj)
if abstract:
new_class._prepare()
return new_class
if new_class._meta.is_mutation:
assert hasattr(
new_class, 'mutate'), "All mutations must implement mutate method"
new_class.add_extra_fields()
new_fields = new_class._meta.local_fields
assert not(new_class._meta.is_union and new_fields), 'An union cannot have extra fields'
field_names = {f.name: f for f in new_fields}
for base in parents:
if not hasattr(base, '_meta'):
# Things without _meta aren't functional models, so they're
# uninteresting parents.
continue
# if base._meta.schema != new_class._meta.schema:
# raise Exception('The parent schema is not the same')
parent_fields = base._meta.local_fields
# Check for clashes between locally declared fields and those
# on the base classes (we cannot handle shadowed fields at the
# moment).
for field in parent_fields:
if field.name in field_names and field.type.__class__ != field_names[
field.name].type.__class__:
raise Exception(
'Local field %r in class %r (%r) clashes '
'with field with similar name from '
'Interface %s (%r)' % (
field.name,
new_class.__name__,
field.__class__,
base.__name__,
field_names[field.name].__class__)
)
new_field = copy.copy(field)
new_class.add_to_class(field.attname, new_field)
new_class._meta.parents.append(base)
if base._meta.is_interface:
new_class._meta.interfaces.append(base)
# new_class._meta.parents.extend(base._meta.parents)
setattr(new_class, 'NonNull', NonNull(new_class))
setattr(new_class, 'List', List(new_class))
new_class._prepare()
return new_class
def add_extra_fields(cls):
pass
def _prepare(cls):
if hasattr(cls, '_prepare_class'):
cls._prepare_class()
signals.class_prepared.send(cls)
def add_to_class(cls, name, value):
# We should call the contribute_to_class method only if it's bound
if not inspect.isclass(value) and hasattr(
value, 'contribute_to_class'):
value.contribute_to_class(cls, name)
else:
setattr(cls, name, value)
class BaseObjectType(BaseType):
def __new__(cls, *args, **kwargs):
if cls._meta.is_interface:
raise Exception("An interface cannot be initialized")
elif cls._meta.is_union:
raise Exception("An union cannot be initialized")
elif cls._meta.abstract:
raise Exception("An abstract ObjectType cannot be initialized")
return super(BaseObjectType, cls).__new__(cls)
def __init__(self, *args, **kwargs):
signals.pre_init.send(self.__class__, args=args, kwargs=kwargs)
self._root = kwargs.pop('_root', None)
args_len = len(args)
fields = self._meta.fields
if args_len > len(fields):
# Daft, but matches old exception sans the err msg.
raise IndexError("Number of args exceeds number of fields")
fields_iter = iter(fields)
if not kwargs:
for val, field in zip(args, fields_iter):
setattr(self, field.attname, val)
else:
for val, field in zip(args, fields_iter):
setattr(self, field.attname, val)
kwargs.pop(field.attname, None)
for field in fields_iter:
try:
val = kwargs.pop(field.attname)
setattr(self, field.attname, val)
except KeyError:
pass
if kwargs:
for prop in list(kwargs):
try:
if isinstance(getattr(self.__class__, prop), property):
setattr(self, prop, kwargs.pop(prop))
except AttributeError:
pass
if kwargs:
raise TypeError(
"'%s' is an invalid keyword argument for this function" %
list(kwargs)[0])
signals.post_init.send(self.__class__, instance=self)
@classmethod
def _resolve_type(cls, schema, instance, *args):
return schema.T(instance.__class__)
@classmethod
def internal_type(cls, schema):
if cls._meta.abstract:
raise Exception("Abstract ObjectTypes don't have a specific type.")
if cls._meta.is_interface:
return GraphQLInterfaceType(
cls._meta.type_name,
description=cls._meta.description,
resolve_type=partial(cls._resolve_type, schema),
fields=partial(cls.get_fields, schema)
)
elif cls._meta.is_union:
return GraphQLUnionType(
cls._meta.type_name,
types=cls._meta.types,
description=cls._meta.description,
)
return GraphQLObjectType(
cls._meta.type_name,
description=cls._meta.description,
interfaces=[schema.T(i) for i in cls._meta.interfaces],
fields=partial(cls.get_fields, schema),
is_type_of=getattr(cls, 'is_type_of', None)
)
@classmethod
def get_fields(cls, schema):
fields = []
for field in cls._meta.fields:
try:
fields.append((field.name, schema.T(field)))
except SkipField:
continue
return OrderedDict(fields)
@classmethod
def wrap(cls, instance, args, info):
return cls(_root=instance)
class Interface(six.with_metaclass(ObjectTypeMeta, BaseObjectType)):
pass
class ObjectType(six.with_metaclass(ObjectTypeMeta, BaseObjectType)):
pass
class Mutation(six.with_metaclass(ObjectTypeMeta, BaseObjectType)):
@classmethod
def _construct_arguments(cls, items):
return ArgumentsGroup(**items)
@classmethod
def _prepare_class(cls):
input_class = getattr(cls, 'Input', None)
if input_class:
items = dict(vars(input_class))
items.pop('__dict__', None)
items.pop('__doc__', None)
items.pop('__module__', None)
items.pop('__weakref__', None)
cls.add_to_class('arguments', cls._construct_arguments(items))
delattr(cls, 'Input')
@classmethod
def get_arguments(cls):
return cls.arguments
class InputObjectType(ObjectType):
@classmethod
def internal_type(cls, schema):
return GraphQLInputObjectType(
cls._meta.type_name,
description=cls._meta.description,
fields=partial(cls.get_fields, schema),
)

View File

@ -1,194 +0,0 @@
from graphql.core.execution.middlewares.utils import (resolver_has_tag,
tag_resolver)
from graphql.core.type import (GraphQLInterfaceType, GraphQLObjectType,
GraphQLUnionType)
from py.test import raises
from graphene.core.schema import Schema
from graphene.core.types import Int, Interface, ObjectType, String
class Character(Interface):
'''Character description'''
name = String()
class Meta:
type_name = 'core_Character'
class Human(Character):
'''Human description'''
friends = String()
class Meta:
type_name = 'core_Human'
@property
def readonly_prop(self):
return 'readonly'
@property
def write_prop(self):
return self._write_prop
@write_prop.setter
def write_prop(self, value):
self._write_prop = value
class Droid(Character):
'''Droid description'''
class CharacterType(Droid, Human):
'''Union Type'''
schema = Schema()
def test_interface():
object_type = schema.T(Character)
assert Character._meta.is_interface is True
assert isinstance(object_type, GraphQLInterfaceType)
assert Character._meta.type_name == 'core_Character'
assert object_type.description == 'Character description'
assert list(object_type.get_fields().keys()) == ['name']
def test_interface_cannot_initialize():
with raises(Exception) as excinfo:
Character()
assert 'An interface cannot be initialized' == str(excinfo.value)
def test_union():
object_type = schema.T(CharacterType)
assert CharacterType._meta.is_union is True
assert isinstance(object_type, GraphQLUnionType)
assert object_type.description == 'Union Type'
def test_union_cannot_initialize():
with raises(Exception) as excinfo:
CharacterType()
assert 'An union cannot be initialized' == str(excinfo.value)
def test_interface_resolve_type():
resolve_type = Character._resolve_type(schema, Human(object()))
assert isinstance(resolve_type, GraphQLObjectType)
def test_object_type():
object_type = schema.T(Human)
assert Human._meta.is_interface is False
assert Human._meta.type_name == 'core_Human'
assert isinstance(object_type, GraphQLObjectType)
assert object_type.description == 'Human description'
assert list(object_type.get_fields().keys()) == ['name', 'friends']
assert object_type.get_interfaces() == [schema.T(Character)]
assert Human._meta.fields_map['name'].object_type == Human
def test_object_type_container():
h = Human(name='My name')
assert h.name == 'My name'
def test_object_type_set_properties():
h = Human(readonly_prop='custom', write_prop='custom')
assert h.readonly_prop == 'readonly'
assert h.write_prop == 'custom'
def test_object_type_container_invalid_kwarg():
with raises(TypeError):
Human(invalid='My name')
def test_object_type_container_too_many_args():
with raises(IndexError):
Human('Peter', 'No friends :(', None)
def test_field_clashes():
with raises(Exception) as excinfo:
class Droid(Character):
name = Int()
assert 'clashes' in str(excinfo.value)
def test_fields_inherited_should_be_different():
assert Character._meta.fields_map['name'] != Human._meta.fields_map['name']
def test_field_mantain_resolver_tags():
class Droid(Character):
name = String()
def resolve_name(self, *args):
return 'My Droid'
tag_resolver(resolve_name, 'test')
field = schema.T(Droid._meta.fields_map['name'])
assert resolver_has_tag(field.resolver, 'test')
def test_type_has_nonnull():
class Droid(Character):
name = String()
assert Droid.NonNull.of_type == Droid
def test_type_has_list():
class Droid(Character):
name = String()
assert Droid.List.of_type == Droid
def test_abstracttype():
class MyObject1(ObjectType):
class Meta:
abstract = True
name1 = String()
class MyObject2(ObjectType):
class Meta:
abstract = True
name2 = String()
class MyObject(MyObject1, MyObject2):
pass
object_type = schema.T(MyObject)
assert list(MyObject._meta.fields_map.keys()) == ['name1', 'name2']
assert MyObject._meta.fields_map['name1'].object_type == MyObject
assert MyObject._meta.fields_map['name2'].object_type == MyObject
assert isinstance(object_type, GraphQLObjectType)
def test_abstracttype_initialize():
class MyAbstractObjectType(ObjectType):
class Meta:
abstract = True
with raises(Exception) as excinfo:
MyAbstractObjectType()
assert 'An abstract ObjectType cannot be initialized' == str(excinfo.value)
def test_abstracttype_type():
class MyAbstractObjectType(ObjectType):
class Meta:
abstract = True
with raises(Exception) as excinfo:
schema.T(MyAbstractObjectType)
assert 'Abstract ObjectTypes don\'t have a specific type.' == str(excinfo.value)