From afe861475398992857806653c664352e4fd4ea8b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 6 Nov 2015 00:07:44 -0800 Subject: [PATCH] First types implementation --- graphene/core/ntypes/__init__.py | 0 graphene/core/ntypes/argument.py | 42 +++++++++++ graphene/core/ntypes/base.py | 71 +++++++++++++++++++ graphene/core/ntypes/definitions.py | 20 ++++++ graphene/core/ntypes/field.py | 61 ++++++++++++++++ graphene/core/ntypes/scalars.py | 40 +++++++++++ graphene/core/ntypes/tests/__init__.py | 0 graphene/core/ntypes/tests/test_argument.py | 45 ++++++++++++ graphene/core/ntypes/tests/test_base.py | 66 +++++++++++++++++ .../core/ntypes/tests/test_definitions.py | 26 +++++++ graphene/core/ntypes/tests/test_field.py | 62 ++++++++++++++++ graphene/core/ntypes/tests/test_scalars.py | 52 ++++++++++++++ graphene/core/schema.py | 4 +- setup.cfg | 3 + setup.py | 1 + 15 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 graphene/core/ntypes/__init__.py create mode 100644 graphene/core/ntypes/argument.py create mode 100644 graphene/core/ntypes/base.py create mode 100644 graphene/core/ntypes/definitions.py create mode 100644 graphene/core/ntypes/field.py create mode 100644 graphene/core/ntypes/scalars.py create mode 100644 graphene/core/ntypes/tests/__init__.py create mode 100644 graphene/core/ntypes/tests/test_argument.py create mode 100644 graphene/core/ntypes/tests/test_base.py create mode 100644 graphene/core/ntypes/tests/test_definitions.py create mode 100644 graphene/core/ntypes/tests/test_field.py create mode 100644 graphene/core/ntypes/tests/test_scalars.py diff --git a/graphene/core/ntypes/__init__.py b/graphene/core/ntypes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/core/ntypes/argument.py b/graphene/core/ntypes/argument.py new file mode 100644 index 00000000..6e8788ce --- /dev/null +++ b/graphene/core/ntypes/argument.py @@ -0,0 +1,42 @@ +from itertools import chain + +from graphql.core.type import GraphQLArgument + +from .base import OrderedType, ArgumentType +from ...utils import to_camel_case + + +class Argument(OrderedType): + def __init__(self, type, description=None, default=None, name=None, _creation_counter=None): + super(Argument, self).__init__(_creation_counter=_creation_counter) + self.name = name + self.type = type + self.description = description + self.default = default + + def internal_type(self, schema): + return GraphQLArgument(schema.T(self.type), self.default, self.description) + + def __repr__(self): + return self.name + + +def to_arguments(*args, **kwargs): + arguments = {} + iter_arguments = chain(kwargs.items(), [(None, a) for a in args]) + + for name, arg in iter_arguments: + if isinstance(arg, Argument): + argument = arg + elif isinstance(arg, ArgumentType): + argument = arg.as_argument() + else: + raise ValueError('Unknown argument value type %r' % arg) + + if name: + argument.name = to_camel_case(name) + assert argument.name, 'Argument in field must have a name' + assert argument.name not in arguments, 'Found more than one Argument with same name {}'.format(argument.name) + arguments[argument.name] = argument + + return sorted(arguments.values()) diff --git a/graphene/core/ntypes/base.py b/graphene/core/ntypes/base.py new file mode 100644 index 00000000..20b1d271 --- /dev/null +++ b/graphene/core/ntypes/base.py @@ -0,0 +1,71 @@ +from functools import total_ordering +from ..types import BaseObjectType, InputObjectType + + +@total_ordering +class OrderedType(object): + creation_counter = 0 + + def __init__(self, _creation_counter=None): + self.creation_counter = _creation_counter or self.gen_counter() + + @staticmethod + def gen_counter(): + counter = OrderedType.creation_counter + OrderedType.creation_counter += 1 + return counter + + def __eq__(self, other): + # Needed for @total_ordering + if type(self) == type(other): + return self.creation_counter == other.creation_counter + return NotImplemented + + def __lt__(self, other): + # This is needed because bisect does not take a comparison function. + if type(self) == type(other): + return self.creation_counter < other.creation_counter + return NotImplemented + + def __hash__(self): + return hash((self.creation_counter)) + + +class MirroredType(OrderedType): + def __init__(self, *args, **kwargs): + _creation_counter = kwargs.pop('_creation_counter', None) + super(MirroredType, self).__init__(_creation_counter=_creation_counter) + self.args = args + self.kwargs = kwargs + + @classmethod + def internal_type(cls, schema): + return getattr(cls, 'T', None) + + +class ArgumentType(MirroredType): + def as_argument(self): + from .argument import Argument + return Argument(self.__class__, _creation_counter=self.creation_counter, *self.args, **self.kwargs) + + +class FieldType(MirroredType): + def contribute_to_class(self, cls, name): + if issubclass(cls, InputObjectType): + inputfield = self.as_inputfield() + return inputfield.contribute_to_class(cls, name) + elif issubclass(cls, BaseObjectType): + field = self.as_field() + return field.contribute_to_class(cls, name) + + def as_field(self): + from .field import Field + return Field(self.__class__, _creation_counter=self.creation_counter, *self.args, **self.kwargs) + + def as_inputfield(self): + from .field import InputField + return InputField(self.__class__, _creation_counter=self.creation_counter, *self.args, **self.kwargs) + + +class MountedType(FieldType, ArgumentType): + pass diff --git a/graphene/core/ntypes/definitions.py b/graphene/core/ntypes/definitions.py new file mode 100644 index 00000000..7eaf9a88 --- /dev/null +++ b/graphene/core/ntypes/definitions.py @@ -0,0 +1,20 @@ +from graphql.core.type import (GraphQLList, GraphQLNonNull) + +from .base import MountedType + + +class OfType(MountedType): + def __init__(self, of_type, *args, **kwargs): + self.of_type = of_type + super(OfType, self).__init__(*args, **kwargs) + + def internal_type(self, schema): + return self.T(schema.T(self.of_type)) + + +class List(OfType): + T = GraphQLList + + +class NonNull(OfType): + T = GraphQLNonNull diff --git a/graphene/core/ntypes/field.py b/graphene/core/ntypes/field.py new file mode 100644 index 00000000..a9eae3d8 --- /dev/null +++ b/graphene/core/ntypes/field.py @@ -0,0 +1,61 @@ +from collections import OrderedDict + +from graphql.core.type import GraphQLField, GraphQLInputObjectField + +from .base import OrderedType +from .argument import to_arguments +from ...utils import to_camel_case +from ..types import BaseObjectType, InputObjectType + + +class Field(OrderedType): + def __init__(self, type, description=None, args=None, name=None, resolver=None, *args_list, **kwargs): + _creation_counter = kwargs.pop('_creation_counter', None) + super(Field, self).__init__(_creation_counter=_creation_counter) + self.name = name + self.type = type + self.description = description + args = OrderedDict(args or {}, **kwargs) + self.arguments = to_arguments(*args_list, **args) + self.resolver = resolver + + def contribute_to_class(self, cls, attname): + assert issubclass(cls, BaseObjectType), 'Field {} cannot be mounted in {}'.format(self, cls) + if not self.name: + self.name = to_camel_case(attname) + self.attname = attname + self.object_type = cls + if self.type == 'self': + self.type = cls + cls._meta.add_field(self) + + def internal_type(self, schema): + return GraphQLField(schema.T(self.type), args=self.get_arguments(schema), resolver=self.resolver, + description=self.description,) + + def get_arguments(self, schema): + if not self.arguments: + return None + + return OrderedDict([(arg.name, schema.T(arg)) for arg in self.arguments]) + + +class InputField(OrderedType): + def __init__(self, type, description=None, default=None, name=None, _creation_counter=None): + super(InputField, self).__init__(_creation_counter=_creation_counter) + self.name = name + self.type = type + self.description = description + self.default = default + + def contribute_to_class(self, cls, attname): + assert issubclass(cls, InputObjectType), 'InputField {} cannot be mounted in {}'.format(self, cls) + if not self.name: + self.name = to_camel_case(attname) + self.attname = attname + self.object_type = cls + cls._meta.add_field(self) + + def internal_type(self, schema): + return GraphQLInputObjectField(schema.T(self.type), default_value=self.default, + description=self.description) diff --git a/graphene/core/ntypes/scalars.py b/graphene/core/ntypes/scalars.py new file mode 100644 index 00000000..2dfc32b7 --- /dev/null +++ b/graphene/core/ntypes/scalars.py @@ -0,0 +1,40 @@ +from graphql.core.type import (GraphQLBoolean, GraphQLFloat, GraphQLID, + GraphQLInt, GraphQLScalarType, GraphQLString) + +from .base import MountedType + + +class String(MountedType): + T = GraphQLString + + +class Int(MountedType): + T = GraphQLInt + + +class Boolean(MountedType): + T = GraphQLBoolean + + +class ID(MountedType): + T = GraphQLID + + +class Float(MountedType): + T = GraphQLFloat + + +class Scalar(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.__name__, + description=cls.__doc__, + serialize=serialize, + parse_value=parse_value, + parse_literal=parse_literal + ) diff --git a/graphene/core/ntypes/tests/__init__.py b/graphene/core/ntypes/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/core/ntypes/tests/test_argument.py b/graphene/core/ntypes/tests/test_argument.py new file mode 100644 index 00000000..ad7650b1 --- /dev/null +++ b/graphene/core/ntypes/tests/test_argument.py @@ -0,0 +1,45 @@ +from pytest import raises +from graphql.core.type import GraphQLArgument + +from ..argument import Argument, to_arguments +from ..scalars import String +from graphene.core.types import ObjectType +from graphene.core.schema import Schema + + +def test_argument_internal_type(): + class MyObjectType(ObjectType): + pass + schema = Schema(query=MyObjectType) + a = Argument(MyObjectType, description='My argument', default='3') + type = schema.T(a) + assert isinstance(type, GraphQLArgument) + assert type.description == 'My argument' + assert type.default_value == '3' + + +def test_to_arguments(): + arguments = to_arguments( + Argument(String, name='myArg'), + String(name='otherArg'), + my_kwarg=String(), + other_kwarg=String(), + ) + + assert [a.name for a in arguments] == ['myArg', 'otherArg', 'myKwarg', 'otherKwarg'] + + +def test_to_arguments_no_name(): + with raises(AssertionError) as excinfo: + to_arguments( + String(), + ) + assert 'must have a name' in str(excinfo.value) + + +def test_to_arguments_wrong_type(): + with raises(ValueError) as excinfo: + to_arguments( + p=3 + ) + assert 'Unknown argument value type 3' == str(excinfo.value) diff --git a/graphene/core/ntypes/tests/test_base.py b/graphene/core/ntypes/tests/test_base.py new file mode 100644 index 00000000..9537f0ea --- /dev/null +++ b/graphene/core/ntypes/tests/test_base.py @@ -0,0 +1,66 @@ +from mock import patch + +from ..base import OrderedType, MountedType +from ..field import Field, InputField +from ..argument import Argument +from graphene.core.types import ObjectType, InputObjectType + + +def test_orderedtype_equal(): + a = OrderedType() + assert a == a + assert hash(a) == hash(a) + + +def test_orderedtype_different(): + a = OrderedType() + b = OrderedType() + assert a != b + assert hash(a) != hash(b) + assert a < b + assert b > a + + +@patch('graphene.core.ntypes.field.Field') +def test_type_as_field_called(Field): + resolver = lambda x: x + a = MountedType(2, description='A', resolver=resolver) + a.as_field() + Field.assert_called_with(MountedType, 2, _creation_counter=a.creation_counter, description='A', resolver=resolver) + + +@patch('graphene.core.ntypes.argument.Argument') +def test_type_as_argument_called(Argument): + a = MountedType(2, description='A') + a.as_argument() + Argument.assert_called_with(MountedType, 2, _creation_counter=a.creation_counter, description='A') + + +def test_type_as_field(): + resolver = lambda x: x + + class MyObjectType(ObjectType): + t = MountedType(description='A', resolver=resolver) + + fields_map = MyObjectType._meta.fields_map + field = fields_map.get('t') + assert isinstance(field, Field) + assert field.description == 'A' + assert field.object_type == MyObjectType + + +def test_type_as_inputfield(): + class MyObjectType(InputObjectType): + t = MountedType(description='A') + + fields_map = MyObjectType._meta.fields_map + field = fields_map.get('t') + assert isinstance(field, InputField) + assert field.description == 'A' + assert field.object_type == MyObjectType + + +def test_type_as_argument(): + a = MountedType(description='A') + argument = a.as_argument() + assert isinstance(argument, Argument) diff --git a/graphene/core/ntypes/tests/test_definitions.py b/graphene/core/ntypes/tests/test_definitions.py new file mode 100644 index 00000000..652a5cd8 --- /dev/null +++ b/graphene/core/ntypes/tests/test_definitions.py @@ -0,0 +1,26 @@ +from graphql.core.type import (GraphQLList, GraphQLString, GraphQLNonNull) + +from ..definitions import List, NonNull +from ..scalars import String +from graphene.core.schema import Schema + +schema = Schema() + + +def test_list_scalar(): + type = schema.T(List(String())) + assert isinstance(type, GraphQLList) + assert type.of_type == GraphQLString + + +def test_nonnull_scalar(): + type = schema.T(NonNull(String())) + assert isinstance(type, GraphQLNonNull) + assert type.of_type == GraphQLString + + +def test_mixed_scalar(): + type = schema.T(NonNull(List(String()))) + assert isinstance(type, GraphQLNonNull) + assert isinstance(type.of_type, GraphQLList) + assert type.of_type.of_type == GraphQLString diff --git a/graphene/core/ntypes/tests/test_field.py b/graphene/core/ntypes/tests/test_field.py new file mode 100644 index 00000000..e34a1184 --- /dev/null +++ b/graphene/core/ntypes/tests/test_field.py @@ -0,0 +1,62 @@ +from graphql.core.type import GraphQLField, GraphQLInputObjectField, GraphQLString + +from ..field import Field, InputField +from ..scalars import String +from graphene.core.types import ObjectType, InputObjectType +from graphene.core.schema import Schema + + +def test_field_internal_type(): + resolver = lambda *args: args + + field = Field(String, description='My argument', resolver=resolver) + + class Query(ObjectType): + my_field = field + schema = Schema(query=Query) + + type = schema.T(field) + assert field.name == 'myField' + assert isinstance(type, GraphQLField) + assert type.description == 'My argument' + assert type.resolver == resolver + assert type.type == GraphQLString + + +def test_field_custom_name(): + field = Field(None, name='my_customName') + + class MyObjectType(ObjectType): + my_field = field + + assert field.name == 'my_customName' + + +def test_field_custom_arguments(): + field = Field(None, name='my_customName', p=String()) + + class MyObjectType(ObjectType): + my_field = field + + schema = Schema(query=MyObjectType) + + args = field.get_arguments(schema) + assert 'p' in args + + +def test_inputfield_internal_type(): + field = InputField(String, description='My input field', default='3') + + class MyObjectType(InputObjectType): + my_field = field + + class Query(ObjectType): + input_ot = Field(MyObjectType) + + schema = Schema(query=MyObjectType) + + type = schema.T(field) + assert field.name == 'myField' + assert isinstance(type, GraphQLInputObjectField) + assert type.description == 'My input field' + assert type.default_value == '3' diff --git a/graphene/core/ntypes/tests/test_scalars.py b/graphene/core/ntypes/tests/test_scalars.py new file mode 100644 index 00000000..7312752a --- /dev/null +++ b/graphene/core/ntypes/tests/test_scalars.py @@ -0,0 +1,52 @@ +from graphql.core.type import (GraphQLBoolean, GraphQLFloat, GraphQLID, + GraphQLInt, GraphQLScalarType, GraphQLString) + +from ..scalars import String, Int, Boolean, ID, Float, Scalar +from graphene.core.schema import Schema + +schema = Schema() + + +def test_string_scalar(): + assert schema.T(String()) == GraphQLString + + +def test_int_scalar(): + assert schema.T(Int()) == GraphQLInt + + +def test_boolean_scalar(): + assert schema.T(Boolean()) == GraphQLBoolean + + +def test_id_scalar(): + assert schema.T(ID()) == GraphQLID + + +def test_float_scalar(): + assert schema.T(Float()) == GraphQLFloat + + +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") + + 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/schema.py b/graphene/core/schema.py index 54361100..1ad1141d 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -37,7 +37,9 @@ class Schema(object): if object_type not in self._types: internal_type = object_type.internal_type(self) self._types[object_type] = internal_type - self._types_names[internal_type.name] = object_type + name = getattr(internal_type, 'name', None) + if name: + self._types_names[name] = object_type return self._types[object_type] @property diff --git a/setup.cfg b/setup.cfg index 15bcaf1b..52475711 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,6 @@ [flake8] exclude = tests/*,setup.py max-line-length = 160 + +[coverage:run] +omit = core/ntypes/tests/* diff --git a/setup.py b/setup.py index c3ab220b..216fa2d3 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,7 @@ setup( tests_require=[ 'pytest>=2.7.2', 'pytest-django', + 'mock', ], extras_require={ 'django': [