diff --git a/bin/autolinter b/bin/autolinter index 164618ae..7f749242 100755 --- a/bin/autolinter +++ b/bin/autolinter @@ -1,5 +1,5 @@ #!/bin/bash -autoflake ./ -r --remove-unused-variables --remove-all-unused-imports --in-place -autopep8 ./ -r --in-place --experimental --aggressive --max-line-length 120 -isort -rc . +autoflake ./examples/ ./graphene/ -r --remove-unused-variables --remove-all-unused-imports --in-place +autopep8 ./examples/ ./graphene/ -r --in-place --experimental --aggressive --max-line-length 120 +isort -rc ./examples/ ./graphene/ diff --git a/graphene/__init__.py b/graphene/__init__.py index 520b2f75..88d3a365 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -8,11 +8,15 @@ from graphene.core.schema import ( Schema ) -from graphene.core.types import ( +from graphene.core.classtypes import ( ObjectType, InputObjectType, Interface, Mutation, + Scalar +) + +from graphene.core.types import ( BaseType, LazyType, Argument, @@ -59,6 +63,7 @@ __all__ = [ 'InputObjectType', 'Interface', 'Mutation', + 'Scalar', 'Field', 'InputField', 'StringField', diff --git a/graphene/contrib/django/__init__.py b/graphene/contrib/django/__init__.py index ec39b7f9..11720f9f 100644 --- a/graphene/contrib/django/__init__.py +++ b/graphene/contrib/django/__init__.py @@ -1,7 +1,6 @@ from graphene.contrib.django.types import ( DjangoConnection, DjangoObjectType, - DjangoInterface, DjangoNode ) from graphene.contrib.django.fields import ( @@ -9,5 +8,5 @@ from graphene.contrib.django.fields import ( DjangoModelField ) -__all__ = ['DjangoObjectType', 'DjangoInterface', 'DjangoNode', - 'DjangoConnection', 'DjangoConnectionField', 'DjangoModelField'] +__all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection', + 'DjangoConnectionField', 'DjangoModelField'] diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index 812e2b03..61dd37a3 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -1,24 +1,15 @@ -import inspect - -from django.db import models - -from ...core.options import Options +from ...core.classtypes.objecttype import ObjectTypeOptions from ...relay.types import Node from ...relay.utils import is_node VALID_ATTRS = ('model', 'only_fields', 'exclude_fields') -def is_base(cls): - from graphene.contrib.django.types import DjangoObjectType - return DjangoObjectType in cls.__bases__ - - -class DjangoOptions(Options): +class DjangoOptions(ObjectTypeOptions): def __init__(self, *args, **kwargs): - self.model = None super(DjangoOptions, self).__init__(*args, **kwargs) + self.model = None self.valid_attrs += VALID_ATTRS self.only_fields = None self.exclude_fields = [] @@ -28,11 +19,3 @@ class DjangoOptions(Options): if is_node(cls): self.exclude_fields = list(self.exclude_fields) + ['id'] self.interfaces.append(Node) - if not is_node(cls) and not is_base(cls): - return - if not self.model: - raise Exception( - 'Django ObjectType %s must have a model in the Meta class 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) diff --git a/graphene/contrib/django/tests/test_types.py b/graphene/contrib/django/tests/test_types.py index 4d709580..42028a5e 100644 --- a/graphene/contrib/django/tests/test_types.py +++ b/graphene/contrib/django/tests/test_types.py @@ -1,9 +1,8 @@ -from graphql.core.type import GraphQLInterfaceType, GraphQLObjectType +from graphql.core.type import GraphQLObjectType from mock import patch -from pytest import raises from graphene import Schema -from graphene.contrib.django.types import DjangoInterface, DjangoNode +from graphene.contrib.django.types import DjangoNode, DjangoObjectType from graphene.core.fields import Field from graphene.core.types.scalars import Int from graphene.relay.fields import GlobalIDField @@ -14,7 +13,8 @@ from .models import Article, Reporter schema = Schema() -class Character(DjangoInterface): +@schema.register +class Character(DjangoObjectType): '''Character description''' class Meta: model = Reporter @@ -31,7 +31,7 @@ class Human(DjangoNode): def test_django_interface(): - assert DjangoNode._meta.is_interface is True + assert DjangoNode._meta.interface is True @patch('graphene.contrib.django.tests.models.Article.objects.get', return_value=Article(id=1)) @@ -41,17 +41,6 @@ def test_django_get_node(get): assert human.id == 1 -def test_pseudo_interface_registered(): - object_type = schema.T(Character) - assert Character._meta.is_interface is True - assert isinstance(object_type, GraphQLInterfaceType) - assert Character._meta.model == Reporter - assert_equal_lists( - object_type.get_fields().keys(), - ['articles', 'firstName', 'lastName', 'email', 'pets', 'id'] - ) - - def test_djangonode_idfield(): idfield = DjangoNode._meta.fields_map['id'] assert isinstance(idfield, GlobalIDField) @@ -68,32 +57,21 @@ def test_node_replacedfield(): assert schema.T(idfield).type == schema.T(Int()) -def test_interface_resolve_type(): - resolve_type = Character._resolve_type(schema, Human()) - assert isinstance(resolve_type, GraphQLObjectType) - - -def test_interface_objecttype_init_none(): +def test_objecttype_init_none(): h = Human() assert h._root is None -def test_interface_objecttype_init_good(): +def test_objecttype_init_good(): instance = Article() h = Human(instance) assert h._root == instance -def test_interface_objecttype_init_unexpected(): - with raises(AssertionError) as excinfo: - Human(object()) - assert str(excinfo.value) == "Human received a non-compatible instance (object) when expecting Article" - - def test_object_type(): object_type = schema.T(Human) Human._meta.fields_map - assert Human._meta.is_interface is False + assert Human._meta.interface is False assert isinstance(object_type, GraphQLObjectType) assert_equal_lists( object_type.get_fields().keys(), @@ -103,5 +81,5 @@ def test_object_type(): def test_node_notinterface(): - assert Human._meta.is_interface is False + assert Human._meta.interface is False assert DjangoNode in Human._meta.interfaces diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index b5097c72..5b68ebbb 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -1,22 +1,19 @@ -import six +import inspect -from ...core.types import BaseObjectType, ObjectTypeMeta -from ...relay.fields import GlobalIDField -from ...relay.types import BaseNode, Connection +import six +from django.db import models + +from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta +from ...relay.types import Connection, Node, NodeMeta from .converter import convert_django_field from .options import DjangoOptions from .utils import get_reverse_fields, maybe_queryset class DjangoObjectTypeMeta(ObjectTypeMeta): - options_cls = DjangoOptions + options_class = DjangoOptions - def is_interface(cls, parents): - return DjangoInterface in parents - - def add_extra_fields(cls): - if not cls._meta.model: - return + def construct_fields(cls): only_fields = cls._meta.only_fields reverse_fields = get_reverse_fields(cls._meta.model) all_fields = sorted(list(cls._meta.model._meta.fields) + @@ -35,8 +32,24 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): converted_field = convert_django_field(field) cls.add_to_class(field.name, converted_field) + def construct(cls, *args, **kwargs): + cls = super(DjangoObjectTypeMeta, cls).construct(*args, **kwargs) + if not cls._meta.abstract: + if not cls._meta.model: + raise Exception( + 'Django ObjectType %s must have a model in the Meta class attr' % + cls) + elif not inspect.isclass(cls._meta.model) or not issubclass(cls._meta.model, models.Model): + raise Exception('Provided model in %s is not a Django model' % cls) -class InstanceObjectType(BaseObjectType): + cls.construct_fields() + return cls + + +class InstanceObjectType(ObjectType): + + class Meta: + abstract = True def __init__(self, _root=None): if _root: @@ -63,12 +76,9 @@ class InstanceObjectType(BaseObjectType): class DjangoObjectType(six.with_metaclass( DjangoObjectTypeMeta, InstanceObjectType)): - pass - -class DjangoInterface(six.with_metaclass( - DjangoObjectTypeMeta, InstanceObjectType)): - pass + class Meta: + abstract = True class DjangoConnection(Connection): @@ -79,8 +89,21 @@ class DjangoConnection(Connection): return super(DjangoConnection, cls).from_list(iterable, *args, **kwargs) -class DjangoNode(BaseNode, DjangoInterface): - id = GlobalIDField() +class DjangoNodeMeta(DjangoObjectTypeMeta, NodeMeta): + pass + + +class NodeInstance(Node, InstanceObjectType): + + class Meta: + abstract = True + + +class DjangoNode(six.with_metaclass( + DjangoNodeMeta, NodeInstance)): + + class Meta: + abstract = True @classmethod def get_node(cls, id, info=None): diff --git a/graphene/core/classtypes/__init__.py b/graphene/core/classtypes/__init__.py new file mode 100644 index 00000000..488ccbb2 --- /dev/null +++ b/graphene/core/classtypes/__init__.py @@ -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'] diff --git a/graphene/core/classtypes/base.py b/graphene/core/classtypes/base.py new file mode 100644 index 00000000..31a2c8d2 --- /dev/null +++ b/graphene/core/classtypes/base.py @@ -0,0 +1,133 @@ +import copy +import inspect +from collections import OrderedDict + +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) + + if not cls._meta.abstract: + from ..types import List, NonNull + setattr(cls, 'NonNull', NonNull(cls)) + setattr(cls, 'List', List(cls)) + + return cls + + +class ClassType(six.with_metaclass(ClassTypeMeta)): + + class Meta: + abstract = True + + @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 + + def extend_fields(cls, bases): + new_fields = cls._meta.local_fields + field_names = {f.name: f for f in new_fields} + + for base in bases: + if not isinstance(base, FieldsClassTypeMeta): + continue + + parent_fields = base._meta.local_fields + 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, + cls.__name__, + field.__class__, + base.__name__, + field_names[field.name].__class__) + ) + new_field = copy.copy(field) + cls.add_to_class(field.attname, new_field) + + def construct(cls, bases, attrs): + cls = super(FieldsClassTypeMeta, cls).construct(bases, attrs) + cls.extend_fields(bases) + return cls + + +class FieldsClassType(six.with_metaclass(FieldsClassTypeMeta, ClassType)): + + class Meta: + abstract = True + + @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) diff --git a/graphene/core/classtypes/inputobjecttype.py b/graphene/core/classtypes/inputobjecttype.py new file mode 100644 index 00000000..43f4c897 --- /dev/null +++ b/graphene/core/classtypes/inputobjecttype.py @@ -0,0 +1,25 @@ +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), + ) diff --git a/graphene/core/classtypes/interface.py b/graphene/core/classtypes/interface.py new file mode 100644 index 00000000..c3aa3110 --- /dev/null +++ b/graphene/core/classtypes/interface.py @@ -0,0 +1,53 @@ +from functools import partial + +import six +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 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) + ) diff --git a/graphene/core/classtypes/mutation.py b/graphene/core/classtypes/mutation.py new file mode 100644 index 00000000..ab443607 --- /dev/null +++ b/graphene/core/classtypes/mutation.py @@ -0,0 +1,32 @@ +import six + +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): + from ..types.argument import ArgumentsGroup + return ArgumentsGroup(**items) + + +class Mutation(six.with_metaclass(MutationMeta, ObjectType)): + + class Meta: + abstract = True + + @classmethod + def get_arguments(cls): + return cls.arguments diff --git a/graphene/core/classtypes/objecttype.py b/graphene/core/classtypes/objecttype.py new file mode 100644 index 00000000..32294d4e --- /dev/null +++ b/graphene/core/classtypes/objecttype.py @@ -0,0 +1,103 @@ +from functools import partial + +import six +from graphql.core.type import GraphQLObjectType + +from graphene import signals + +from .base import FieldsClassType, FieldsClassTypeMeta, FieldsOptions +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=list(map(schema.T, 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) diff --git a/graphene/core/options.py b/graphene/core/classtypes/options.py similarity index 70% rename from graphene/core/options.py rename to graphene/core/classtypes/options.py index 4f1bf4e4..52e4536e 100644 --- a/graphene/core/options.py +++ b/graphene/core/classtypes/options.py @@ -1,24 +1,11 @@ -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): + def __init__(self, meta=None, **defaults): 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 + 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 @@ -59,14 +46,3 @@ class Options(object): 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]) diff --git a/graphene/core/classtypes/scalar.py b/graphene/core/classtypes/scalar.py new file mode 100644 index 00000000..8d34eba0 --- /dev/null +++ b/graphene/core/classtypes/scalar.py @@ -0,0 +1,21 @@ +from graphql.core.type import GraphQLScalarType + +from ..types.base import MountedType +from .base import ClassType + + +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 + ) diff --git a/graphene/core/tests/test_options.py b/graphene/core/classtypes/test_options.py similarity index 73% rename from graphene/core/tests/test_options.py rename to graphene/core/classtypes/test_options.py index 3b656bd4..512c3e6a 100644 --- a/graphene/core/tests/test_options.py +++ b/graphene/core/classtypes/test_options.py @@ -1,11 +1,9 @@ from py.test import raises -from graphene.core.fields import Field -from graphene.core.options import Options +from graphene.core.classtypes import Options class Meta: - is_interface = True type_name = 'Character' @@ -13,19 +11,6 @@ class InvalidMeta: other_value = True -def test_field_added_in_meta(): - opt = Options(Meta) - - class ObjectType(object): - pass - - opt.contribute_to_class(ObjectType, '_meta') - f = Field(None) - f.attname = 'string_field' - opt.add_field(f) - assert f in opt.fields - - def test_options_contribute(): opt = Options(Meta) diff --git a/graphene/core/classtypes/tests/__init__.py b/graphene/core/classtypes/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/core/classtypes/tests/test_base.py b/graphene/core/classtypes/tests/test_base.py new file mode 100644 index 00000000..5017f94d --- /dev/null +++ b/graphene/core/classtypes/tests/test_base.py @@ -0,0 +1,67 @@ +from ...schema import Schema +from ...types import Field, List, NonNull, String +from ..base import ClassType, FieldsClassType + + +def test_classtype_basic(): + class Character(ClassType): + '''Character description''' + 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_classtype_definition_list(): + class Character(ClassType): + '''Character description''' + assert isinstance(Character.List, List) + assert Character.List.of_type == Character + + +def test_classtype_definition_nonnull(): + class Character(ClassType): + '''Character description''' + assert isinstance(Character.NonNull, NonNull) + assert Character.NonNull.of_type == Character + + +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 + + +def test_fieldsclasstype_inheritfields(): + name_field = Field(String()) + last_name_field = Field(String()) + + class Fields1(FieldsClassType): + name = name_field + + class Fields2(Fields1): + last_name = last_name_field + + assert list(Fields2._meta.fields_map.keys()) == ['name', 'last_name'] diff --git a/graphene/core/classtypes/tests/test_inputobjecttype.py b/graphene/core/classtypes/tests/test_inputobjecttype.py new file mode 100644 index 00000000..2268d46f --- /dev/null +++ b/graphene/core/classtypes/tests/test_inputobjecttype.py @@ -0,0 +1,21 @@ + +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'] diff --git a/graphene/core/classtypes/tests/test_interface.py b/graphene/core/classtypes/tests/test_interface.py new file mode 100644 index 00000000..7994659a --- /dev/null +++ b/graphene/core/classtypes/tests/test_interface.py @@ -0,0 +1,86 @@ +from graphql.core.type import GraphQLInterfaceType, GraphQLObjectType +from py.test import raises + +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 CommonClass(object): + common_attr = True + + class Character(CommonClass, Interface): + pass + + class ShouldBeObjectType(Character): + pass + + assert Character._meta.interface + assert Character.common_attr + assert ShouldBeObjectType.common_attr diff --git a/graphene/core/classtypes/tests/test_mutation.py b/graphene/core/classtypes/tests/test_mutation.py new file mode 100644 index 00000000..ac32585e --- /dev/null +++ b/graphene/core/classtypes/tests/test_mutation.py @@ -0,0 +1,27 @@ + +from graphql.core.type import GraphQLObjectType + +from graphene.core.schema import Schema +from graphene.core.types import String + +from ...types.argument import ArgumentsGroup +from ..mutation import Mutation + + +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 diff --git a/graphene/core/classtypes/tests/test_objecttype.py b/graphene/core/classtypes/tests/test_objecttype.py new file mode 100644 index 00000000..93cabc76 --- /dev/null +++ b/graphene/core/classtypes/tests/test_objecttype.py @@ -0,0 +1,89 @@ +from graphql.core.type import GraphQLObjectType +from py.test import raises + +from graphene.core.schema import Schema +from graphene.core.types import 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 diff --git a/graphene/core/classtypes/tests/test_scalar.py b/graphene/core/classtypes/tests/test_scalar.py new file mode 100644 index 00000000..a6e37881 --- /dev/null +++ b/graphene/core/classtypes/tests/test_scalar.py @@ -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' diff --git a/graphene/core/classtypes/tests/test_uniontype.py b/graphene/core/classtypes/tests/test_uniontype.py new file mode 100644 index 00000000..308b2ce0 --- /dev/null +++ b/graphene/core/classtypes/tests/test_uniontype.py @@ -0,0 +1,28 @@ +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)] diff --git a/graphene/core/classtypes/uniontype.py b/graphene/core/classtypes/uniontype.py new file mode 100644 index 00000000..62003005 --- /dev/null +++ b/graphene/core/classtypes/uniontype.py @@ -0,0 +1,40 @@ +import six +from graphql.core.type import GraphQLUnionType + +from .base import FieldsClassType, FieldsClassTypeMeta, FieldsOptions + + +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=list(map(schema.T, cls._meta.types)), + resolve_type=cls._resolve_type, + description=cls._meta.description, + ) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index eef79678..1b0ce8f9 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -10,8 +10,8 @@ from graphql.core.utils.schema_printer import print_schema from graphene import signals +from .classtypes.base import ClassType from .types.base import BaseType -from .types.objecttype import BaseObjectType class GraphQLSchema(_GraphQLSchema): @@ -42,13 +42,13 @@ class Schema(object): if not object_type: return if inspect.isclass(object_type) and issubclass( - object_type, BaseType) or isinstance( + object_type, (BaseType, ClassType)) or isinstance( object_type, BaseType): if object_type not in self._types: internal_type = object_type.internal_type(self) self._types[object_type] = internal_type is_objecttype = inspect.isclass( - object_type) and issubclass(object_type, BaseObjectType) + object_type) and issubclass(object_type, ClassType) if is_objecttype: self.register(object_type) return self._types[object_type] @@ -90,7 +90,7 @@ class Schema(object): if name: objecttype = self._types_names.get(name, None) if objecttype and inspect.isclass( - objecttype) and issubclass(objecttype, BaseObjectType): + objecttype) and issubclass(objecttype, ClassType): return objecttype def __str__(self): diff --git a/graphene/core/types/__init__.py b/graphene/core/types/__init__.py index 5bd49f2f..9260476c 100644 --- a/graphene/core/types/__init__.py +++ b/graphene/core/types/__init__.py @@ -1,7 +1,9 @@ from .base import BaseType, LazyType, OrderedType from .argument import Argument, ArgumentsGroup, to_arguments 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 .field import Field, InputField @@ -17,8 +19,6 @@ __all__ = [ 'Field', 'InputField', 'Interface', - 'BaseObjectType', - 'ObjectTypeMeta', 'ObjectType', 'Mutation', 'InputObjectType', diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index fe261790..2b4078e4 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -104,11 +104,12 @@ class ArgumentType(MirroredType): class FieldType(MirroredType): def contribute_to_class(self, cls, name): - from ..types import BaseObjectType, InputObjectType - if issubclass(cls, InputObjectType): + from ..classtypes.base import FieldsClassType + from ..classtypes.inputobjecttype import InputObjectType + if issubclass(cls, (InputObjectType)): inputfield = self.as_inputfield() return inputfield.contribute_to_class(cls, name) - elif issubclass(cls, BaseObjectType): + elif issubclass(cls, (FieldsClassType)): field = self.as_field() return field.contribute_to_class(cls, name) diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index db52086a..d7643b5e 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -5,7 +5,9 @@ import six from graphql.core.type import GraphQLField, GraphQLInputObjectField from ...utils import to_camel_case -from ..types import BaseObjectType, InputObjectType +from ..classtypes.base import FieldsClassType +from ..classtypes.inputobjecttype import InputObjectType +from ..classtypes.mutation import Mutation from .argument import ArgumentsGroup, snake_case_args from .base import LazyType, MountType, OrderedType from .definitions import NonNull @@ -32,7 +34,7 @@ class Field(OrderedType): def contribute_to_class(self, cls, attname): assert issubclass( - cls, BaseObjectType), 'Field {} cannot be mounted in {}'.format( + cls, (FieldsClassType)), 'Field {} cannot be mounted in {}'.format( self, cls) if not self.name: self.name = to_camel_case(attname) @@ -69,7 +71,7 @@ class Field(OrderedType): description = resolver.__doc__ type = schema.T(self.get_type(schema)) 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 arguments = type_objecttype.get_arguments() resolver = getattr(type_objecttype, 'mutate') @@ -126,7 +128,7 @@ class InputField(OrderedType): def contribute_to_class(self, cls, attname): assert issubclass( - cls, InputObjectType), 'InputField {} cannot be mounted in {}'.format( + cls, (InputObjectType)), 'InputField {} cannot be mounted in {}'.format( self, cls) if not self.name: self.name = to_camel_case(attname) diff --git a/graphene/core/types/objecttype.py b/graphene/core/types/objecttype.py index a9ac43f1..d7cf42ab 100644 --- a/graphene/core/types/objecttype.py +++ b/graphene/core/types/objecttype.py @@ -1,282 +1,3 @@ -import copy -import inspect -from collections import OrderedDict -from functools import partial +from ..classtypes import InputObjectType, Interface, Mutation, ObjectType -import six -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), - ) +__all__ = ['ObjectType', 'Interface', 'Mutation', 'InputObjectType'] diff --git a/graphene/core/types/tests/test_objecttype.py b/graphene/core/types/tests/test_objecttype.py deleted file mode 100644 index 51b84f61..00000000 --- a/graphene/core/types/tests/test_objecttype.py +++ /dev/null @@ -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) diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index b3829ad8..dc8c4973 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -90,11 +90,5 @@ class GlobalIDField(Field): def __init__(self, *args, **kwargs): super(GlobalIDField, self).__init__(NonNull(ID()), *args, **kwargs) - def contribute_to_class(self, cls, name): - from graphene.relay.utils import is_node, is_node_type - in_node = is_node(cls) or is_node_type(cls) - assert in_node, 'GlobalIDField could only be inside a Node, but got %r' % cls - super(GlobalIDField, self).contribute_to_class(cls, name) - def resolver(self, instance, args, info): return instance.to_global_id() diff --git a/graphene/relay/types.py b/graphene/relay/types.py index 9fa673ef..672042e7 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -3,11 +3,14 @@ import warnings from collections import Iterable from functools import wraps +import six from graphql_relay.connection.arrayconnection import connection_from_list from graphql_relay.node.node import to_global_id -from ..core.types import (Boolean, Field, InputObjectType, Interface, List, - Mutation, ObjectType, String) +from ..core.classtypes import InputObjectType, Interface, Mutation, ObjectType +from ..core.classtypes.interface import InterfaceMeta +from ..core.classtypes.mutation import MutationMeta +from ..core.types import Boolean, Field, List, String from ..core.types.argument import ArgumentsGroup from ..core.types.definitions import NonNull from ..utils import memoize @@ -83,33 +86,44 @@ class Connection(ObjectType): return self._connection_data -class BaseNode(object): +class NodeMeta(InterfaceMeta): - @classmethod - def _prepare_class(cls): - from graphene.relay.utils import is_node - if is_node(cls): - get_node = getattr(cls, 'get_node') - assert get_node, 'get_node classmethod not found in %s Node' % cls - assert callable(get_node), 'get_node have to be callable' - args = 3 - if isinstance(get_node, staticmethod): - args -= 1 + def construct_get_node(cls): + get_node = getattr(cls, 'get_node', None) + assert get_node, 'get_node classmethod not found in %s Node' % cls + assert callable(get_node), 'get_node have to be callable' + args = 3 + if isinstance(get_node, staticmethod): + args -= 1 - get_node_num_args = len(inspect.getargspec(get_node).args) - if get_node_num_args < args: - warnings.warn("get_node will receive also the info arg" - " in future versions of graphene".format(cls.__name__), - FutureWarning) + get_node_num_args = len(inspect.getargspec(get_node).args) + if get_node_num_args < args: + warnings.warn("get_node will receive also the info arg" + " in future versions of graphene".format(cls.__name__), + FutureWarning) - @staticmethod - @wraps(get_node) - def wrapped_node(*node_args): - if len(node_args) < args: - node_args += (None, ) - return get_node(*node_args[:-1]) + @staticmethod + @wraps(get_node) + def wrapped_node(*node_args): + if len(node_args) < args: + node_args += (None, ) + return get_node(*node_args[:-1]) - setattr(cls, 'get_node', wrapped_node) + setattr(cls, 'get_node', wrapped_node) + + def construct(cls, *args, **kwargs): + cls = super(NodeMeta, cls).construct(*args, **kwargs) + if not cls._meta.abstract: + cls.construct_get_node() + return cls + + +class Node(six.with_metaclass(NodeMeta, Interface)): + '''An object with an ID''' + id = GlobalIDField() + + class Meta: + abstract = True def to_global_id(self): type_name = self._meta.type_name @@ -127,27 +141,32 @@ class BaseNode(object): return cls.edge_type -class Node(BaseNode, Interface): - '''An object with an ID''' - id = GlobalIDField() - - class MutationInputType(InputObjectType): client_mutation_id = String(required=True) -class ClientIDMutation(Mutation): - client_mutation_id = String(required=True) +class RelayMutationMeta(MutationMeta): - @classmethod - def _construct_arguments(cls, items): - assert hasattr( - cls, 'mutate_and_get_payload'), 'You have to implement mutate_and_get_payload' + def construct(cls, *args, **kwargs): + cls = super(RelayMutationMeta, cls).construct(*args, **kwargs) + if not cls._meta.abstract: + assert hasattr( + cls, 'mutate_and_get_payload'), 'You have to implement mutate_and_get_payload' + return cls + + def construct_arguments(cls, items): new_input_type = type('{}Input'.format( cls._meta.type_name), (MutationInputType, ), items) cls.add_to_class('input_type', new_input_type) return ArgumentsGroup(input=NonNull(new_input_type)) + +class ClientIDMutation(six.with_metaclass(RelayMutationMeta, Mutation)): + client_mutation_id = String(required=True) + + class Meta: + abstract = True + @classmethod def mutate(cls, instance, args, info): input = args.get('input') diff --git a/graphene/relay/utils.py b/graphene/relay/utils.py index dc281830..98cb81dd 100644 --- a/graphene/relay/utils.py +++ b/graphene/relay/utils.py @@ -1,10 +1,11 @@ -from .types import BaseNode +from .types import Node def is_node(object_type): return object_type and issubclass( - object_type, BaseNode) and not is_node_type(object_type) + object_type, Node) and not object_type._meta.abstract def is_node_type(object_type): - return BaseNode in object_type.__bases__ + return object_type and issubclass( + object_type, Node) and object_type._meta.abstract diff --git a/graphene/signals.py b/graphene/signals.py index cdfa3d06..3183eeec 100644 --- a/graphene/signals.py +++ b/graphene/signals.py @@ -2,6 +2,7 @@ try: from blinker import Signal except ImportError: class Signal(object): + def send(self, *args, **kwargs): pass