diff --git a/graphene/new_types/scalars.py b/graphene/new_types/scalars.py new file mode 100644 index 00000000..f071b45e --- /dev/null +++ b/graphene/new_types/scalars.py @@ -0,0 +1,148 @@ +import six + +from graphql.language.ast import BooleanValue, FloatValue, IntValue, StringValue + +from ..utils.is_base_type import is_base_type +from .options import Options +from .unmountedtype import UnmountedType + + +class ScalarTypeMeta(type): + + def __new__(cls, name, bases, attrs): + super_new = super(ScalarTypeMeta, cls).__new__ + + # Also ensure initialization is only performed for subclasses of Model + # (excluding Model class itself). + if not is_base_type(bases, ScalarTypeMeta): + return super_new(cls, name, bases, attrs) + + options = Options( + attrs.pop('Meta', None), + name=None, + description=None, + ) + + return super_new(cls, name, bases, dict(attrs, _meta=options)) + + +class Scalar(six.with_metaclass(ScalarTypeMeta, UnmountedType)): + serialize = None + parse_value = None + parse_literal = None + +# As per the GraphQL Spec, Integers are only treated as valid when a valid +# 32-bit signed integer, providing the broadest support across platforms. +# +# n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because +# they are internally represented as IEEE 754 doubles. +MAX_INT = 2147483647 +MIN_INT = -2147483648 + + +class Int(Scalar): + ''' + The `Int` scalar type represents non-fractional signed whole numeric + values. Int can represent values between -(2^53 - 1) and 2^53 - 1 since + represented in JSON as double-precision floating point numbers specified + by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). + ''' + + @staticmethod + def coerce_int(value): + try: + num = int(value) + except ValueError: + try: + num = int(float(value)) + except ValueError: + return None + if MIN_INT <= num <= MAX_INT: + return num + + serialize = coerce_int + parse_value = coerce_int + + @staticmethod + def parse_literal(ast): + if isinstance(ast, IntValue): + num = int(ast.value) + if MIN_INT <= num <= MAX_INT: + return num + + +class Float(Scalar): + ''' + The `Float` scalar type represents signed double-precision fractional + values as specified by + [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). + ''' + + @staticmethod + def coerce_float(value): + try: + return float(value) + except ValueError: + return None + + serialize = coerce_float + parse_value = coerce_float + + @staticmethod + def parse_literal(ast): + if isinstance(ast, (FloatValue, IntValue)): + return float(ast.value) + + +class String(Scalar): + ''' + The `String` scalar type represents textual data, represented as UTF-8 + character sequences. The String type is most often used by GraphQL to + represent free-form human-readable text. + ''' + + @staticmethod + def coerce_string(value): + if isinstance(value, bool): + return u'true' if value else u'false' + return six.text_type(value) + + serialize = coerce_string + parse_value = coerce_string + + @staticmethod + def parse_literal(ast): + if isinstance(ast, StringValue): + return ast.value + + +class Boolean(Scalar): + ''' + The `Boolean` scalar type represents `true` or `false`. + ''' + + serialize = bool + parse_value = bool + + @staticmethod + def parse_literal(ast): + if isinstance(ast, BooleanValue): + return ast.value + + +class ID(Scalar): + ''' + The `ID` scalar type represents a unique identifier, often used to + refetch an object or as key for a cache. The ID type appears in a JSON + response as a String; however, it is not intended to be human-readable. + When expected as an input type, any string (such as `"4"`) or integer + (such as `4`) input value will be accepted as an ID. + ''' + + serialize = str + parse_value = str + + @staticmethod + def parse_literal(ast): + if isinstance(ast, (StringValue, IntValue)): + return ast.value diff --git a/graphene/new_types/tests/test_objecttype.py b/graphene/new_types/tests/test_objecttype.py index aefa9292..7ec66107 100644 --- a/graphene/new_types/tests/test_objecttype.py +++ b/graphene/new_types/tests/test_objecttype.py @@ -70,6 +70,7 @@ def test_generate_objecttype_inherit_abstracttype(): assert MyObjectType._meta.fields.keys() == ['field1', 'field2'] assert [type(x) for x in MyObjectType._meta.fields.values()] == [Field, Field] + def test_generate_objecttype_inherit_abstracttype_reversed(): class MyAbstractType(AbstractType): field1 = MyScalar(MyType) diff --git a/graphene/new_types/tests/test_scalars_serialization.py b/graphene/new_types/tests/test_scalars_serialization.py new file mode 100644 index 00000000..4eb261ca --- /dev/null +++ b/graphene/new_types/tests/test_scalars_serialization.py @@ -0,0 +1,50 @@ +from ..scalars import (Boolean, Float, Int, String) + + +def test_serializes_output_int(): + assert Int.serialize(1) == 1 + assert Int.serialize(0) == 0 + assert Int.serialize(-1) == -1 + assert Int.serialize(0.1) == 0 + assert Int.serialize(1.1) == 1 + assert Int.serialize(-1.1) == -1 + assert Int.serialize(1e5) == 100000 + assert Int.serialize(9876504321) is None + assert Int.serialize(-9876504321) is None + assert Int.serialize(1e100) is None + assert Int.serialize(-1e100) is None + assert Int.serialize('-1.1') == -1 + assert Int.serialize('one') is None + assert Int.serialize(False) == 0 + assert Int.serialize(True) == 1 + + +def test_serializes_output_float(): + assert Float.serialize(1) == 1.0 + assert Float.serialize(0) == 0.0 + assert Float.serialize(-1) == -1.0 + assert Float.serialize(0.1) == 0.1 + assert Float.serialize(1.1) == 1.1 + assert Float.serialize(-1.1) == -1.1 + assert Float.serialize('-1.1') == -1.1 + assert Float.serialize('one') is None + assert Float.serialize(False) == 0 + assert Float.serialize(True) == 1 + + +def test_serializes_output_string(): + assert String.serialize('string') == 'string' + assert String.serialize(1) == '1' + assert String.serialize(-1.1) == '-1.1' + assert String.serialize(True) == 'true' + assert String.serialize(False) == 'false' + assert String.serialize(u'\U0001F601') == u'\U0001F601' + + +def test_serializes_output_boolean(): + assert Boolean.serialize('string') is True + assert Boolean.serialize('') is False + assert Boolean.serialize(1) is True + assert Boolean.serialize(0) is False + assert Boolean.serialize(True) is True + assert Boolean.serialize(False) is False