diff --git a/graphene/new_types/__init__.py b/graphene/new_types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/new_types/abstracttype.py b/graphene/new_types/abstracttype.py new file mode 100644 index 00000000..93243b05 --- /dev/null +++ b/graphene/new_types/abstracttype.py @@ -0,0 +1,37 @@ +import six +from collections import OrderedDict + +from ..utils.is_base_type import is_base_type +from .options import Options + +from .utils import get_fields_in_type, attrs_without_fields + + +def merge_fields_in_attrs(bases, attrs): + for base in bases: + if not issubclass(base, AbstractType): + continue + for name, field in base._meta.fields.items(): + if name in attrs: + continue + attrs[name] = field + return attrs + + +class AbstractTypeMeta(type): + + def __new__(cls, name, bases, attrs): + options = attrs.get('_meta', Options()) + + attrs = merge_fields_in_attrs(bases, attrs) + fields = get_fields_in_type(cls, attrs) + options.fields = OrderedDict(sorted(fields, key=lambda f: f[1])) + + attrs = attrs_without_fields(attrs, fields) + cls = type.__new__(cls, name, bases, dict(attrs, _meta=options)) + + return cls + + +class AbstractType(six.with_metaclass(AbstractTypeMeta)): + pass diff --git a/graphene/new_types/field.py b/graphene/new_types/field.py new file mode 100644 index 00000000..3f2f975a --- /dev/null +++ b/graphene/new_types/field.py @@ -0,0 +1,70 @@ +# import inspect +from functools import partial +from collections import OrderedDict + +# from graphql.type import (GraphQLField, GraphQLInputObjectField) +# from graphql.utils.assert_valid_name import assert_valid_name + +from ..utils.orderedtype import OrderedType +from .structures import NonNull +# from ..utils.str_converters import to_camel_case +# from .argument import to_arguments + + +# class AbstractField(object): + +# @property +# def name(self): +# return self._name or self.attname and to_camel_case(self.attname) + +# @name.setter +# def name(self, name): +# if name is not None: +# assert_valid_name(name) +# self._name = name + +# @property +# def type(self): +# from ..utils.get_graphql_type import get_graphql_type +# from .structures import NonNull +# if inspect.isfunction(self._type): +# _type = self._type() +# else: +# _type = self._type + +# if self.required: +# return NonNull(_type) +# return get_graphql_type(_type) + +# @type.setter +# def type(self, type): +# self._type = type + +def source_resolver(source, root, args, context, info): + resolved = getattr(root, source, None) + if callable(resolved): + return resolved() + return resolved + + +class Field(OrderedType): + + def __init__(self, type, args=None, resolver=None, source=None, + deprecation_reason=None, name=None, description=None, + required=False, _creation_counter=None, **extra_args): + super(Field, self).__init__(_creation_counter=_creation_counter) + self.name = name + # self.attname = None + # self.parent = None + if required: + type = NonNull(type) + self.type = type + self.args = args or OrderedDict() + # self.args = to_arguments(args, extra_args) + assert not (source and resolver), ('You cannot provide a source and a ' + 'resolver in a Field at the same time.') + if source: + resolver = partial(source_resolver, source) + self.resolver = resolver + self.deprecation_reason = deprecation_reason + self.description = description diff --git a/graphene/new_types/interface.py b/graphene/new_types/interface.py new file mode 100644 index 00000000..418f76e6 --- /dev/null +++ b/graphene/new_types/interface.py @@ -0,0 +1,41 @@ +import six +from collections import OrderedDict + +from ..utils.is_base_type import is_base_type +from .options import Options + +from .utils import get_fields_in_type, attrs_without_fields + + +class InterfaceMeta(type): + + def __new__(cls, name, bases, attrs): + # Also ensure initialization is only performed for subclasses of + # ObjectType + if not is_base_type(bases, InterfaceMeta): + return type.__new__(cls, name, bases, attrs) + + options = Options( + attrs.pop('Meta', None), + name=name, + description=attrs.get('__doc__'), + ) + + fields = get_fields_in_type(Interface, attrs) + options.fields = OrderedDict(sorted(fields, key=lambda f: f[1])) + + attrs = attrs_without_fields(attrs, fields) + cls = super(InterfaceMeta, cls).__new__(cls, name, bases, dict(attrs, _meta=options)) + + return cls + + +class Interface(six.with_metaclass(InterfaceMeta)): + resolve_type = None + + def __init__(self, *args, **kwargs): + raise Exception("An interface cannot be intitialized") + + @classmethod + def implements(cls, objecttype): + pass diff --git a/graphene/new_types/objecttype.py b/graphene/new_types/objecttype.py new file mode 100644 index 00000000..cbff92fd --- /dev/null +++ b/graphene/new_types/objecttype.py @@ -0,0 +1,73 @@ +import six +from collections import OrderedDict + +from ..utils.is_base_type import is_base_type +from .options import Options + +from .utils import get_fields_in_type, attrs_without_fields + + +class ObjectTypeMeta(type): + + def __new__(cls, name, bases, attrs): + # Also ensure initialization is only performed for subclasses of + # ObjectType + if not is_base_type(bases, ObjectTypeMeta): + return type.__new__(cls, name, bases, attrs) + + options = Options( + attrs.pop('Meta', None), + name=name, + description=attrs.get('__doc__'), + interfaces=(), + ) + + fields = get_fields_in_type(ObjectType, attrs) + options.fields = OrderedDict(sorted(fields, key=lambda f: f[1])) + + attrs = attrs_without_fields(attrs, fields) + cls = super(ObjectTypeMeta, cls).__new__(cls, name, bases, dict(attrs, _meta=options)) + + return cls + + +class ObjectType(six.with_metaclass(ObjectTypeMeta)): + + def __init__(self, *args, **kwargs): + # GraphQL ObjectType acting as container + args_len = len(args) + fields = self._meta.fields.items() + 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, (name, field) in zip(args, fields_iter): + setattr(self, name, val) + else: + for val, (name, field) in zip(args, fields_iter): + setattr(self, name, val) + kwargs.pop(name, None) + + for name, field in fields_iter: + try: + val = kwargs.pop(name) + setattr(self, name, 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( + "'{}' is an invalid keyword argument for {}".format( + list(kwargs)[0], + self.__class__.__name__ + ) + ) diff --git a/graphene/new_types/options.py b/graphene/new_types/options.py new file mode 100644 index 00000000..38549df6 --- /dev/null +++ b/graphene/new_types/options.py @@ -0,0 +1,37 @@ +import inspect +from ..utils.props import props + + +class Options(object): + ''' + This is the class wrapper around Meta. + It helps to validate and cointain the attributes inside + ''' + + def __init__(self, meta=None, **defaults): + if meta: + assert inspect.isclass(meta), ( + 'Meta have to be a class, received "{}".'.format(repr(meta)) + ) + self.add_attrs_from_meta(meta, defaults) + + def add_attrs_from_meta(self, meta, defaults): + meta_attrs = props(meta) if meta else {} + for attr_name, value in defaults.items(): + if attr_name in meta_attrs: + value = meta_attrs.pop(attr_name) + elif hasattr(meta, attr_name): + value = getattr(meta, attr_name) + setattr(self, attr_name, value) + + # If meta_attrs is not empty, it implicit means + # it received invalid attributes + if meta_attrs: + raise TypeError( + "Invalid attributes: {}".format( + ','.join(meta_attrs.keys()) + ) + ) + + def __repr__(self): + return ''.format(props(self)) diff --git a/graphene/new_types/structures.py b/graphene/new_types/structures.py new file mode 100644 index 00000000..1eedadbe --- /dev/null +++ b/graphene/new_types/structures.py @@ -0,0 +1,23 @@ +from .unmountedtype import UnmountedType + + +class Structure(UnmountedType): + ''' + A structure is a GraphQL type instance that + wraps a main type with certain structure. + ''' + + def __init__(self, of_type, *args, **kwargs): + super(Structure, self).__init__(*args, **kwargs) + self.of_type = of_type + + def get_type(self): + return self + + +class List(Structure): + pass + + +class NonNull(Structure): + pass diff --git a/graphene/new_types/tests/__init__.py b/graphene/new_types/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/new_types/tests/test_abstracttype.py b/graphene/new_types/tests/test_abstracttype.py new file mode 100644 index 00000000..6be6eac3 --- /dev/null +++ b/graphene/new_types/tests/test_abstracttype.py @@ -0,0 +1,99 @@ +import pytest + +from ..field import Field +from ..abstracttype import AbstractType +from ..unmountedtype import UnmountedType + + +class MyType(object): + pass + + +class MyScalar(UnmountedType): + def get_type(self): + return MyType + + +def test_generate_abstracttype_with_fields(): + class MyAbstractType(AbstractType): + field = Field(MyType) + + assert 'field' in MyAbstractType._meta.fields + assert isinstance(MyAbstractType._meta.fields['field'], Field) + + +def test_generate_abstracttype_with_unmountedfields(): + class MyAbstractType(AbstractType): + field = UnmountedType(MyType) + + assert 'field' in MyAbstractType._meta.fields + assert isinstance(MyAbstractType._meta.fields['field'], UnmountedType) + + +def test_generate_abstracttype_inheritance(): + class MyAbstractType1(AbstractType): + field1 = UnmountedType(MyType) + + class MyAbstractType2(MyAbstractType1): + field2 = UnmountedType(MyType) + + assert MyAbstractType2._meta.fields.keys() == ['field1', 'field2'] + + +# def test_ordered_fields_in_objecttype(): +# class MyObjectType(ObjectType): +# b = Field(MyType) +# a = Field(MyType) +# field = MyScalar() +# asa = Field(MyType) + +# assert list(MyObjectType._meta.fields.keys()) == ['b', 'a', 'field', 'asa'] + + +# def test_generate_objecttype_unmountedtype(): +# class MyObjectType(ObjectType): +# field = MyScalar(MyType) + +# assert 'field' in MyObjectType._meta.fields +# assert isinstance(MyObjectType._meta.fields['field'], Field) + + +# def test_parent_container_get_fields(): +# assert list(Container._meta.fields.keys()) == ['field1', 'field2'] + + +# def test_objecttype_as_container_only_args(): +# container = Container("1", "2") +# assert container.field1 == "1" +# assert container.field2 == "2" + + +# def test_objecttype_as_container_args_kwargs(): +# container = Container("1", field2="2") +# assert container.field1 == "1" +# assert container.field2 == "2" + + +# def test_objecttype_as_container_few_kwargs(): +# container = Container(field2="2") +# assert container.field2 == "2" + + +# def test_objecttype_as_container_all_kwargs(): +# container = Container(field1="1", field2="2") +# assert container.field1 == "1" +# assert container.field2 == "2" + + +# def test_objecttype_as_container_extra_args(): +# with pytest.raises(IndexError) as excinfo: +# Container("1", "2", "3") + +# assert "Number of args exceeds number of fields" == str(excinfo.value) + + +# def test_objecttype_as_container_invalid_kwargs(): +# with pytest.raises(TypeError) as excinfo: +# Container(unexisting_field="3") + +# assert "'unexisting_field' is an invalid keyword argument for Container" == str(excinfo.value) diff --git a/graphene/new_types/tests/test_field.py b/graphene/new_types/tests/test_field.py new file mode 100644 index 00000000..0863f6e4 --- /dev/null +++ b/graphene/new_types/tests/test_field.py @@ -0,0 +1,55 @@ +import pytest + +from ..field import Field +from ..structures import NonNull + + +class MyInstance(object): + value = 'value' + value_func = staticmethod(lambda: 'value_func') + + +def test_field_basic(): + MyType = object() + args = {} + resolver = lambda: None + deprecation_reason = 'Deprecated now' + description = 'My Field' + field = Field( + MyType, + name='name', + args=args, + resolver=resolver, + description=description, + deprecation_reason=deprecation_reason + ) + assert field.name == 'name' + assert field.args == args + assert field.resolver == resolver + assert field.deprecation_reason == deprecation_reason + assert field.description == description + + +def test_field_required(): + MyType = object() + field = Field(MyType, required=True) + assert isinstance(field.type, NonNull) + assert field.type.of_type == MyType + + +def test_field_source(): + MyType = object() + field = Field(MyType, source='value') + assert field.resolver(MyInstance, {}, None, None) == MyInstance.value + + +def test_field_not_source_and_resolver(): + MyType = object() + with pytest.raises(Exception) as exc_info: + Field(MyType, source='value', resolver=lambda: None) + assert str(exc_info.value) == 'You cannot provide a source and a resolver in a Field at the same time.' + +def test_field_source_func(): + MyType = object() + field = Field(MyType, source='value_func') + assert field.resolver(MyInstance(), {}, None, None) == MyInstance.value_func() diff --git a/graphene/new_types/tests/test_interface.py b/graphene/new_types/tests/test_interface.py new file mode 100644 index 00000000..508baa99 --- /dev/null +++ b/graphene/new_types/tests/test_interface.py @@ -0,0 +1,59 @@ +import pytest + +from ..field import Field +from ..interface import Interface +from ..unmountedtype import UnmountedType + + +class MyType(object): + pass + + +class MyScalar(UnmountedType): + def get_type(self): + return MyType + + +def test_generate_interface(): + class MyInterface(Interface): + '''Documentation''' + + assert MyInterface._meta.name == "MyInterface" + assert MyInterface._meta.description == "Documentation" + assert MyInterface._meta.fields == {} + + +def test_generate_interface_with_meta(): + class MyInterface(Interface): + + class Meta: + name = 'MyOtherInterface' + description = 'Documentation' + + assert MyInterface._meta.name == "MyOtherInterface" + assert MyInterface._meta.description == "Documentation" + + +def test_generate_interface_with_fields(): + class MyInterface(Interface): + field = Field(MyType) + + assert 'field' in MyInterface._meta.fields + + +def test_ordered_fields_in_interface(): + class MyInterface(Interface): + b = Field(MyType) + a = Field(MyType) + field = MyScalar() + asa = Field(MyType) + + assert list(MyInterface._meta.fields.keys()) == ['b', 'a', 'field', 'asa'] + + +def test_generate_interface_unmountedtype(): + class MyInterface(Interface): + field = MyScalar(MyType) + + assert 'field' in MyInterface._meta.fields + assert isinstance(MyInterface._meta.fields['field'], Field) diff --git a/graphene/new_types/tests/test_objecttype.py b/graphene/new_types/tests/test_objecttype.py new file mode 100644 index 00000000..d652b165 --- /dev/null +++ b/graphene/new_types/tests/test_objecttype.py @@ -0,0 +1,108 @@ +import pytest + +from ..field import Field +from ..objecttype import ObjectType +from ..unmountedtype import UnmountedType + + +class MyType(object): + pass + + +class Container(ObjectType): + field1 = Field(MyType) + field2 = Field(MyType) + + +class MyScalar(UnmountedType): + def get_type(self): + return MyType + + +def test_generate_objecttype(): + class MyObjectType(ObjectType): + '''Documentation''' + + assert MyObjectType._meta.name == "MyObjectType" + assert MyObjectType._meta.description == "Documentation" + assert MyObjectType._meta.interfaces == tuple() + assert MyObjectType._meta.fields == {} + + +def test_generate_objecttype_with_meta(): + class MyObjectType(ObjectType): + + class Meta: + name = 'MyOtherObjectType' + description = 'Documentation' + interfaces = (MyType, ) + + assert MyObjectType._meta.name == "MyOtherObjectType" + assert MyObjectType._meta.description == "Documentation" + assert MyObjectType._meta.interfaces == (MyType, ) + + +def test_generate_objecttype_with_fields(): + class MyObjectType(ObjectType): + field = Field(MyType) + + assert 'field' in MyObjectType._meta.fields + + +def test_ordered_fields_in_objecttype(): + class MyObjectType(ObjectType): + b = Field(MyType) + a = Field(MyType) + field = MyScalar() + asa = Field(MyType) + + assert list(MyObjectType._meta.fields.keys()) == ['b', 'a', 'field', 'asa'] + + +def test_generate_objecttype_unmountedtype(): + class MyObjectType(ObjectType): + field = MyScalar(MyType) + + assert 'field' in MyObjectType._meta.fields + assert isinstance(MyObjectType._meta.fields['field'], Field) + + +def test_parent_container_get_fields(): + assert list(Container._meta.fields.keys()) == ['field1', 'field2'] + + +def test_objecttype_as_container_only_args(): + container = Container("1", "2") + assert container.field1 == "1" + assert container.field2 == "2" + + +def test_objecttype_as_container_args_kwargs(): + container = Container("1", field2="2") + assert container.field1 == "1" + assert container.field2 == "2" + + +def test_objecttype_as_container_few_kwargs(): + container = Container(field2="2") + assert container.field2 == "2" + + +def test_objecttype_as_container_all_kwargs(): + container = Container(field1="1", field2="2") + assert container.field1 == "1" + assert container.field2 == "2" + + +def test_objecttype_as_container_extra_args(): + with pytest.raises(IndexError) as excinfo: + Container("1", "2", "3") + + assert "Number of args exceeds number of fields" == str(excinfo.value) + + +def test_objecttype_as_container_invalid_kwargs(): + with pytest.raises(TypeError) as excinfo: + Container(unexisting_field="3") + + assert "'unexisting_field' is an invalid keyword argument for Container" == str(excinfo.value) diff --git a/graphene/new_types/unmountedtype.py b/graphene/new_types/unmountedtype.py new file mode 100644 index 00000000..080f867d --- /dev/null +++ b/graphene/new_types/unmountedtype.py @@ -0,0 +1,60 @@ +from ..utils.orderedtype import OrderedType +# from .argument import Argument + + + +class UnmountedType(OrderedType): + ''' + This class acts a proxy for a Graphene Type, so it can be mounted + as Field, InputField or Argument. + + Instead of writing + >>> class MyObjectType(ObjectType): + >>> my_field = Field(String(), description='Description here') + + It let you write + >>> class MyObjectType(ObjectType): + >>> my_field = String(description='Description here') + ''' + + def __init__(self, *args, **kwargs): + super(UnmountedType, self).__init__() + self.args = args + self.kwargs = kwargs + + def get_type(self): + raise NotImplementedError("get_type not implemented in {}".format(self)) + + def as_field(self): + ''' + Mount the UnmountedType as Field + ''' + from .field import Field + return Field( + self.get_type(), + *self.args, + _creation_counter=self.creation_counter, + **self.kwargs + ) + + # def as_inputfield(self): + # ''' + # Mount the UnmountedType as InputField + # ''' + # return InputField( + # self.get_type(), + # *self.args, + # _creation_counter=self.creation_counter, + # **self.kwargs + # ) + + # def as_argument(self): + # ''' + # Mount the UnmountedType as Argument + # ''' + # return Argument( + # self.get_type(), + # *self.args, + # _creation_counter=self.creation_counter, + # **self.kwargs + # ) diff --git a/graphene/new_types/utils.py b/graphene/new_types/utils.py new file mode 100644 index 00000000..1a1ed78f --- /dev/null +++ b/graphene/new_types/utils.py @@ -0,0 +1,42 @@ +from .unmountedtype import UnmountedType +from .field import Field + + +def unmounted_field_in_type(attname, unmounted_field, type): + ''' + Mount the UnmountedType dinamically as Field or InputField + depending on where mounted in. + + ObjectType -> Field + InputObjectType -> InputField + ''' + # from ..types.inputobjecttype import InputObjectType + from ..new_types.objecttype import ObjectTypeMeta + from ..new_types.interface import Interface + from ..new_types.abstracttype import AbstractTypeMeta + + if issubclass(type, (ObjectTypeMeta, Interface)): + return unmounted_field.as_field() + + elif issubclass(type, (AbstractTypeMeta)): + return unmounted_field + # elif issubclass(type, (InputObjectType)): + # return unmounted_field.as_inputfield() + + raise Exception( + 'Unmounted field "{}" cannot be mounted in {}.{}.'.format( + unmounted_field, type, attname + ) + ) + + +def get_fields_in_type(in_type, attrs): + for attname, value in list(attrs.items()): + if isinstance(value, (Field)): # , InputField + yield attname, value + elif isinstance(value, UnmountedType): + yield attname, unmounted_field_in_type(attname, value, in_type) + + +def attrs_without_fields(attrs, fields): + return {k: v for k, v in attrs.items() if k not in fields}