First working version class types

This commit is contained in:
Syrus Akbary 2015-12-02 20:06:15 -08:00
parent e78936c424
commit 3363f588fe
20 changed files with 755 additions and 5 deletions

View File

View File

@ -0,0 +1,109 @@
from collections import OrderedDict
import inspect
import six
from ..exceptions import SkipField
from .options import Options
class ClassTypeMeta(type):
options_class = Options
def __new__(mcs, name, bases, attrs):
super_new = super(ClassTypeMeta, mcs).__new__
module = attrs.pop('__module__', None)
doc = attrs.pop('__doc__', None)
new_class = super_new(mcs, name, bases, {
'__module__': module,
'__doc__': doc
})
attr_meta = attrs.pop('Meta', None)
if not attr_meta:
meta = getattr(new_class, 'Meta', None)
else:
meta = attr_meta
new_class.add_to_class('_meta', new_class.get_options(meta))
return mcs.construct(new_class, bases, attrs)
def get_options(cls, meta):
return cls.options_class(meta)
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)
def construct(cls, bases, attrs):
# Add all attributes to the class.
for obj_name, obj in attrs.items():
cls.add_to_class(obj_name, obj)
return cls
class ClassType(six.with_metaclass(ClassTypeMeta)):
@classmethod
def internal_type(cls, schema):
raise NotImplementedError("Function internal_type not implemented in type {}".format(cls))
class FieldsOptions(Options):
def __init__(self, *args, **kwargs):
super(FieldsOptions, self).__init__(*args, **kwargs)
self.local_fields = []
def add_field(self, field):
self.local_fields.append(field)
@property
def fields(self):
return sorted(self.local_fields)
@property
def fields_map(self):
return OrderedDict([(f.attname, f) for f in self.fields])
class FieldsClassTypeMeta(ClassTypeMeta):
options_class = FieldsOptions
class FieldsClassType(six.with_metaclass(FieldsClassTypeMeta, ClassType)):
@classmethod
def fields_internal_types(cls, schema):
fields = []
for field in cls._meta.fields:
try:
fields.append((field.name, schema.T(field)))
except SkipField:
continue
return OrderedDict(fields)
# class NamedClassType(ClassType):
# pass
# class UnionType(NamedClassType):
# class Meta:
# abstract = True
# class ObjectType(NamedClassType):
# class Meta:
# abstract = True
# class InputObjectType(NamedClassType):
# class Meta:
# abstract = True
# class Mutation(ObjectType):
# class Meta:
# abstract = True

View File

@ -0,0 +1,24 @@
from functools import partial
from graphql.core.type import GraphQLInputObjectType
from .base import FieldsClassType
class InputObjectType(FieldsClassType):
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
raise Exception("An InputObjectType cannot be initialized")
@classmethod
def internal_type(cls, schema):
if cls._meta.abstract:
raise Exception("Abstract InputObjectTypes don't have a specific type.")
return GraphQLInputObjectType(
cls._meta.type_name,
description=cls._meta.description,
fields=partial(cls.fields_internal_types, schema),
)

View File

@ -0,0 +1,54 @@
import six
from functools import partial
from graphql.core.type import GraphQLInterfaceType
from .base import FieldsClassTypeMeta
from .objecttype import ObjectType, ObjectTypeMeta
class InterfaceMeta(ObjectTypeMeta):
def construct(cls, bases, attrs):
if cls._meta.abstract or Interface in bases:
# Return Interface type
cls = FieldsClassTypeMeta.construct(cls, bases, attrs)
setattr(cls._meta, 'interface', True)
return cls
else:
# Return ObjectType class with all the inherited interfaces
cls = super(InterfaceMeta, cls).construct(bases, attrs)
for interface in bases:
is_interface = issubclass(interface, Interface) and getattr(interface._meta, 'interface', False)
if not is_interface:
continue
cls._meta.interfaces.append(interface)
return cls
class Interface(six.with_metaclass(InterfaceMeta, ObjectType)):
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
if self._meta.interface:
raise Exception("An interface cannot be initialized")
return super(Interface, self).__init__(*args, **kwargs)
@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 Interfaces don't have a specific type.")
if not cls._meta.interface:
return super(Interface, cls).internal_type(schema)
return GraphQLInterfaceType(
cls._meta.type_name,
description=cls._meta.description,
resolve_type=partial(cls._resolve_type, schema),
fields=partial(cls.fields_internal_types, schema)
)

View File

@ -0,0 +1,30 @@
import six
from ..types.argument import ArgumentsGroup
from .objecttype import ObjectType, ObjectTypeMeta
class MutationMeta(ObjectTypeMeta):
def construct(cls, bases, attrs):
input_class = attrs.pop('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))
cls = super(MutationMeta, cls).construct(bases, attrs)
return cls
def construct_arguments(cls, items):
return ArgumentsGroup(**items)
class Mutation(six.with_metaclass(MutationMeta, ObjectType)):
class Meta:
abstract = True
@classmethod
def get_arguments(cls):
return cls.arguments

View File

@ -0,0 +1,99 @@
import six
from functools import partial
from graphql.core.type import GraphQLObjectType
from graphene import signals
from .base import FieldsOptions, FieldsClassType, FieldsClassTypeMeta
from .uniontype import UnionType
def is_objecttype(cls):
if not issubclass(cls, ObjectType):
return False
return not cls._meta.interface
class ObjectTypeOptions(FieldsOptions):
def __init__(self, *args, **kwargs):
super(ObjectTypeOptions, self).__init__(*args, **kwargs)
self.interface = False
self.interfaces = []
class ObjectTypeMeta(FieldsClassTypeMeta):
def construct(cls, bases, attrs):
cls = super(ObjectTypeMeta, cls).construct(bases, attrs)
if not cls._meta.abstract:
union_types = list(filter(is_objecttype, bases))
if len(union_types) > 1:
meta_attrs = dict(cls._meta.original_attrs, types=union_types)
Meta = type('Meta', (object, ), meta_attrs)
attrs['Meta'] = Meta
attrs['__module__'] = cls.__module__
attrs['__doc__'] = cls.__doc__
return type(cls.__name__, (UnionType, ), attrs)
return cls
options_class = ObjectTypeOptions
class ObjectType(six.with_metaclass(ObjectTypeMeta, FieldsClassType)):
class Meta:
abstract = True
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 internal_type(cls, schema):
if cls._meta.abstract:
raise Exception("Abstract ObjectTypes don't have a specific type.")
return GraphQLObjectType(
cls._meta.type_name,
description=cls._meta.description,
interfaces=[schema.T(i) for i in cls._meta.interfaces],
fields=partial(cls.fields_internal_types, schema),
is_type_of=getattr(cls, 'is_type_of', None)
)
@classmethod
def wrap(cls, instance, args, info):
return cls(_root=instance)

View File

@ -0,0 +1,47 @@
class Options(object):
def __init__(self, meta=None, **defaults):
self.meta = meta
self.abstract = False
for name, value in defaults.items():
setattr(self, name, value)
self.valid_attrs = list(defaults.keys()) + ['type_name', 'description', 'abstract']
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

View File

@ -0,0 +1,21 @@
from graphql.core.type import GraphQLScalarType
from .base import ClassType
from ..types.base import MountedType
class Scalar(ClassType, MountedType):
@classmethod
def internal_type(cls, schema):
serialize = getattr(cls, 'serialize')
parse_literal = getattr(cls, 'parse_literal')
parse_value = getattr(cls, 'parse_value')
return GraphQLScalarType(
name=cls._meta.type_name,
description=cls._meta.description,
serialize=serialize,
parse_value=parse_value,
parse_literal=parse_literal
)

View File

@ -0,0 +1,40 @@
from ..base import ClassType, FieldsClassType
from ...types import Field, String
from ...schema import Schema
def test_classtype_basic():
class Character(ClassType):
'''Character description'''
pass
assert Character._meta.type_name == 'Character'
assert Character._meta.description == 'Character description'
def test_classtype_advanced():
class Character(ClassType):
class Meta:
type_name = 'OtherCharacter'
description = 'OtherCharacter description'
assert Character._meta.type_name == 'OtherCharacter'
assert Character._meta.description == 'OtherCharacter description'
def test_fieldsclasstype():
f = Field(String())
class Character(FieldsClassType):
field_name = f
assert Character._meta.fields == [f]
def test_fieldsclasstype_fieldtype():
f = Field(String())
class Character(FieldsClassType):
field_name = f
schema = Schema(query=Character)
assert Character.fields_internal_types(schema)['fieldName'] == schema.T(f)
assert Character._meta.fields_map['field_name'] == f

View File

@ -0,0 +1,21 @@
from py.test import raises
from graphql.core.type import GraphQLInputObjectType
from graphene.core.schema import Schema
from graphene.core.types import String
from ..inputobjecttype import InputObjectType
def test_inputobjecttype():
class InputCharacter(InputObjectType):
'''InputCharacter description'''
name = String()
schema = Schema()
object_type = schema.T(InputCharacter)
assert isinstance(object_type, GraphQLInputObjectType)
assert InputCharacter._meta.type_name == 'InputCharacter'
assert object_type.description == 'InputCharacter description'
assert list(object_type.get_fields().keys()) == ['name']

View File

@ -0,0 +1,85 @@
from py.test import raises
from graphql.core.type import GraphQLInterfaceType, GraphQLObjectType
from graphene.core.schema import Schema
from graphene.core.types import String
from ..interface import Interface
from ..objecttype import ObjectType
def test_interface():
class Character(Interface):
'''Character description'''
name = String()
schema = Schema()
object_type = schema.T(Character)
assert issubclass(Character, Interface)
assert isinstance(object_type, GraphQLInterfaceType)
assert Character._meta.interface
assert Character._meta.type_name == 'Character'
assert object_type.description == 'Character description'
assert list(object_type.get_fields().keys()) == ['name']
def test_interface_cannot_initialize():
class Character(Interface):
pass
with raises(Exception) as excinfo:
Character()
assert 'An interface cannot be initialized' == str(excinfo.value)
def test_interface_inheritance_abstract():
class Character(Interface):
pass
class ShouldBeInterface(Character):
class Meta:
abstract = True
class ShouldBeObjectType(ShouldBeInterface):
pass
assert ShouldBeInterface._meta.interface
assert not ShouldBeObjectType._meta.interface
assert issubclass(ShouldBeObjectType, ObjectType)
def test_interface_inheritance():
class Character(Interface):
pass
class GeneralInterface(Interface):
pass
class ShouldBeObjectType(GeneralInterface, Character):
pass
schema = Schema()
assert Character._meta.interface
assert not ShouldBeObjectType._meta.interface
assert issubclass(ShouldBeObjectType, ObjectType)
assert Character in ShouldBeObjectType._meta.interfaces
assert GeneralInterface in ShouldBeObjectType._meta.interfaces
assert isinstance(schema.T(Character), GraphQLInterfaceType)
assert isinstance(schema.T(ShouldBeObjectType), GraphQLObjectType)
def test_interface_inheritance_non_objects():
class ComonClass(object):
common_attr = True
class Character(ComonClass, Interface):
pass
class ShouldBeObjectType(Character):
pass
assert Character._meta.interface
assert Character.common_attr
assert ShouldBeObjectType.common_attr

View File

@ -0,0 +1,27 @@
from py.test import raises
from graphql.core.type import GraphQLObjectType
from graphene.core.schema import Schema
from graphene.core.types import String
from ..mutation import Mutation
from ...types.argument import ArgumentsGroup
def test_mutation():
class MyMutation(Mutation):
'''MyMutation description'''
class Input:
arg_name = String()
name = String()
schema = Schema()
object_type = schema.T(MyMutation)
assert MyMutation._meta.type_name == 'MyMutation'
assert isinstance(object_type, GraphQLObjectType)
assert object_type.description == 'MyMutation description'
assert list(object_type.get_fields().keys()) == ['name']
assert MyMutation._meta.fields_map['name'].object_type == MyMutation
assert isinstance(MyMutation.arguments, ArgumentsGroup)
assert 'argName' in MyMutation.arguments

View File

@ -0,0 +1,89 @@
from py.test import raises
from graphql.core.type import GraphQLObjectType
from graphene.core.schema import Schema
from graphene.core.types import Int, String
from ..objecttype import ObjectType
from ..uniontype import UnionType
def test_object_type():
class Human(ObjectType):
'''Human description'''
name = String()
friends = String()
schema = Schema()
object_type = schema.T(Human)
assert Human._meta.type_name == 'Human'
assert isinstance(object_type, GraphQLObjectType)
assert object_type.description == 'Human description'
assert list(object_type.get_fields().keys()) == ['name', 'friends']
assert Human._meta.fields_map['name'].object_type == Human
def test_object_type_container():
class Human(ObjectType):
name = String()
friends = String()
h = Human(name='My name')
assert h.name == 'My name'
def test_object_type_set_properties():
class Human(ObjectType):
name = String()
friends = String()
@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
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():
class Human(ObjectType):
name = String()
with raises(TypeError):
Human(invalid='My name')
def test_object_type_container_too_many_args():
class Human(ObjectType):
name = String()
with raises(IndexError):
Human('Peter', 'No friends :(', None)
def test_object_type_union():
class Human(ObjectType):
name = String()
class Pet(ObjectType):
name = String()
class Thing(Human, Pet):
'''Thing union description'''
my_attr = True
assert issubclass(Thing, UnionType)
assert Thing._meta.types == [Human, Pet]
assert Thing._meta.type_name == 'Thing'
assert Thing._meta.description == 'Thing union description'
assert Thing.my_attr

View File

@ -0,0 +1,32 @@
from graphql.core.type import GraphQLScalarType
from ...schema import Schema
from ..scalar import Scalar
def test_custom_scalar():
import datetime
from graphql.core.language import ast
class DateTimeScalar(Scalar):
'''DateTimeScalar Documentation'''
@staticmethod
def serialize(dt):
return dt.isoformat()
@staticmethod
def parse_literal(node):
if isinstance(node, ast.StringValue):
return datetime.datetime.strptime(
node.value, "%Y-%m-%dT%H:%M:%S.%f")
@staticmethod
def parse_value(value):
return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f")
schema = Schema()
scalar_type = schema.T(DateTimeScalar)
assert isinstance(scalar_type, GraphQLScalarType)
assert scalar_type.name == 'DateTimeScalar'
assert scalar_type.description == 'DateTimeScalar Documentation'

View File

@ -0,0 +1,27 @@
from graphql.core.type import GraphQLUnionType
from graphene.core.schema import Schema
from graphene.core.types import String
from ..objecttype import ObjectType
from ..uniontype import UnionType
def test_uniontype():
class Human(ObjectType):
name = String()
class Pet(ObjectType):
name = String()
class Thing(UnionType):
'''Thing union description'''
class Meta:
types = [Human, Pet]
schema = Schema()
object_type = schema.T(Thing)
assert isinstance(object_type, GraphQLUnionType)
assert Thing._meta.type_name == 'Thing'
assert object_type.description == 'Thing union description'
assert object_type.get_possible_types() == [schema.T(Human), schema.T(Pet)]

View File

@ -0,0 +1,39 @@
import six
from graphql.core.type import GraphQLUnionType
from .base import FieldsOptions, FieldsClassType, FieldsClassTypeMeta
class UnionTypeOptions(FieldsOptions):
def __init__(self, *args, **kwargs):
super(UnionTypeOptions, self).__init__(*args, **kwargs)
self.types = []
class UnionTypeMeta(FieldsClassTypeMeta):
options_class = UnionTypeOptions
def get_options(cls, meta):
return cls.options_class(meta, types=[])
class UnionType(six.with_metaclass(UnionTypeMeta, FieldsClassType)):
class Meta:
abstract = True
@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.")
return GraphQLUnionType(
cls._meta.type_name,
types=map(schema.T, cls._meta.types),
resolve_type=cls._resolve_type,
description=cls._meta.description,
)

View File

@ -12,6 +12,7 @@ from graphene import signals
from .types.base import BaseType from .types.base import BaseType
from .types.objecttype import BaseObjectType from .types.objecttype import BaseObjectType
from .classtypes.base import ClassType
class GraphQLSchema(_GraphQLSchema): class GraphQLSchema(_GraphQLSchema):
@ -42,7 +43,7 @@ class Schema(object):
if not object_type: if not object_type:
return return
if inspect.isclass(object_type) and issubclass( if inspect.isclass(object_type) and issubclass(
object_type, BaseType) or isinstance( object_type, (BaseType, ClassType)) or isinstance(
object_type, BaseType): object_type, BaseType):
if object_type not in self._types: if object_type not in self._types:
internal_type = object_type.internal_type(self) internal_type = object_type.internal_type(self)

View File

@ -2,6 +2,9 @@ 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):
@ -105,10 +108,10 @@ class FieldType(MirroredType):
def contribute_to_class(self, cls, name): def contribute_to_class(self, cls, name):
from ..types import BaseObjectType, InputObjectType from ..types import BaseObjectType, InputObjectType
if issubclass(cls, InputObjectType): if issubclass(cls, (InputObjectType, NewInputObjectType)):
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): elif issubclass(cls, (BaseObjectType, 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

@ -5,6 +5,8 @@ import six
from graphql.core.type import GraphQLField, GraphQLInputObjectField 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.inputobjecttype import InputObjectType as NewInputObjectType
from ..types import BaseObjectType, InputObjectType from ..types import BaseObjectType, 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
@ -32,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), 'Field {} cannot be mounted in {}'.format( cls, (BaseObjectType, 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)
@ -126,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), 'InputField {} cannot be mounted in {}'.format( cls, (InputObjectType, NewInputObjectType)), '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)