diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index 31428ee6..1c54d697 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -437,10 +437,15 @@ def complete_leaf_value(return_type, result): """ Complete a Scalar or Enum by serializing to a valid value, returning null if serialization is not possible. """ - # serialize = getattr(return_type, 'serialize', None) - # assert serialize, 'Missing serialize method on type' + assert hasattr(return_type, 'serialize'), 'Missing serialize method on type' + serialized_result = return_type.serialize(result) - return return_type.serialize(result) + if serialized_result is None: + raise GraphQLError( + ('Expected a value of type "{}" but ' + + 'received: {}').format(return_type, result) + ) + return serialized_result def complete_abstract_value(exe_context, return_type, field_asts, info, result): diff --git a/graphql/type/definition.py b/graphql/type/definition.py index eb1a8764..e4abf3ce 100644 --- a/graphql/type/definition.py +++ b/graphql/type/definition.py @@ -413,6 +413,12 @@ def __init__(self, name, values, description=None): self.values = define_enum_values(self, values) + def get_values(self): + return self.values + + def get_value(self, name): + return self._name_lookup.get(name) + def serialize(self, value): if isinstance(value, collections.Hashable): enum_value = self._value_lookup.get(value) @@ -463,7 +469,7 @@ def define_enum_values(type, value_map): ) value = copy.copy(value) value.name = value_name - if value.value is None: + if value.value == Undefined: value.value = value_name values.append(value) @@ -471,15 +477,24 @@ def define_enum_values(type, value_map): return values +class Undefined(object): + """A representation of an Undefined value distinct from a None value""" + pass + + class GraphQLEnumValue(object): __slots__ = 'name', 'value', 'deprecation_reason', 'description' - def __init__(self, value=None, deprecation_reason=None, description=None, name=None): + def __init__(self, value=Undefined, deprecation_reason=None, description=None, name=None): self.name = name self.value = value self.deprecation_reason = deprecation_reason self.description = description + @property + def is_deprecated(self): + return bool(self.deprecation_reason) + def __eq__(self, other): return ( self is other or ( diff --git a/graphql/type/tests/test_definition.py b/graphql/type/tests/test_definition.py index d9a6ec87..659ecfd4 100644 --- a/graphql/type/tests/test_definition.py +++ b/graphql/type/tests/test_definition.py @@ -118,6 +118,31 @@ def test_defines_a_subscription_schema(): # assert subscription.name == 'articleSubscribe' +def test_defines_an_enum_type_with_deprecated_value(): + EnumTypeWithDeprecatedValue = GraphQLEnumType('EnumWithDeprecatedValue', { + 'foo': GraphQLEnumValue(deprecation_reason='Just because'), + }) + value = EnumTypeWithDeprecatedValue.get_values()[0] + assert value.name == 'foo' + assert value.description is None + assert value.is_deprecated is True + assert value.deprecation_reason == 'Just because' + assert value.value == 'foo' + + +def test_defines_an_enum_type_with_a_value_of_none(): + EnumTypeWithNoneValue = GraphQLEnumType('EnumWithNullishValue', { + 'NULL': GraphQLEnumValue(None), + }) + + value = EnumTypeWithNoneValue.get_values()[0] + assert value.name == 'NULL' + assert value.description is None + assert value.is_deprecated is False + assert value.deprecation_reason is None + assert value.value is None + + def test_defines_an_object_type_with_deprecated_field(): TypeWithDeprecatedField = GraphQLObjectType('foo', fields={ 'bar': GraphQLField( @@ -178,6 +203,23 @@ def test_includes_nested_input_objects_in_the_map(): assert schema.get_type_map()['NestedInputObject'] is NestedInputObject +def test_includes_interface_possible_types_in_the_type_map(): + SomeInterface = GraphQLInterfaceType('SomeInterface', fields={'f': GraphQLField(GraphQLInt)}) + SomeSubtype = GraphQLObjectType( + name='SomeSubtype', + fields={'f': GraphQLField(GraphQLInt)}, + interfaces=[SomeInterface], + is_type_of=lambda: None + ) + schema = GraphQLSchema( + query=GraphQLObjectType( + name='Query', + fields={ + 'iface': GraphQLField(SomeInterface)}), + types=[SomeSubtype]) + assert schema.get_type_map()['SomeSubtype'] == SomeSubtype + + def test_includes_interfaces_thunk_subtypes_in_the_type_map(): SomeInterface = GraphQLInterfaceType( name='SomeInterface', @@ -205,23 +247,6 @@ def test_includes_interfaces_thunk_subtypes_in_the_type_map(): assert schema.get_type_map()['SomeSubtype'] is SomeSubtype -def test_includes_interfaces_subtypes_in_the_type_map(): - SomeInterface = GraphQLInterfaceType('SomeInterface', fields={'f': GraphQLField(GraphQLInt)}) - SomeSubtype = GraphQLObjectType( - name='SomeSubtype', - fields={'f': GraphQLField(GraphQLInt)}, - interfaces=[SomeInterface], - is_type_of=lambda: None - ) - schema = GraphQLSchema( - query=GraphQLObjectType( - name='Query', - fields={ - 'iface': GraphQLField(SomeInterface)}), - types=[SomeSubtype]) - assert schema.get_type_map()['SomeSubtype'] == SomeSubtype - - def test_stringifies_simple_types(): assert str(GraphQLInt) == 'Int' assert str(BlogArticle) == 'Article' diff --git a/graphql/type/tests/test_enum_type.py b/graphql/type/tests/test_enum_type.py index c68ee131..65c9ae18 100644 --- a/graphql/type/tests/test_enum_type.py +++ b/graphql/type/tests/test_enum_type.py @@ -1,7 +1,6 @@ from collections import OrderedDict from rx import Observable -from pytest import raises from graphql import graphql from graphql.type import (GraphQLArgument, GraphQLEnumType, GraphQLEnumValue, @@ -114,13 +113,28 @@ def test_does_not_accept_string_literals(): 'Expected type "Color", found "GREEN".' +def test_does_not_accept_values_not_in_the_enum(): + result = graphql(Schema, '{ colorEnum(fromEnum: GREENISH) }') + assert not result.data + assert result.errors[0].message == 'Argument "fromEnum" has invalid value GREENISH.\n' \ + 'Expected type "Color", found GREENISH.' + + +def test_does_not_accept_values_with_incorrect_casing(): + result = graphql(Schema, '{ colorEnum(fromEnum: green) }') + assert not result.data + assert result.errors[0].message == 'Argument "fromEnum" has invalid value green.\n' \ + 'Expected type "Color", found green.' + + def test_does_not_accept_incorrect_internal_value(): result = graphql(Schema, '{ colorEnum(fromString: "GREEN") }') assert result.data == {'colorEnum': None} - assert not result.errors + assert result.errors[0].message == 'Expected a value of type "Color" ' \ + 'but received: GREEN' -def test_does_not_accept_internal_value_in_placeof_enum_literal(): +def test_does_not_accept_internal_value_in_place_of_enum_literal(): result = graphql(Schema, '{ colorEnum(fromEnum: 1) }') assert not result.data assert result.errors[0].message == 'Argument "fromEnum" has invalid value 1.\n' \ @@ -135,8 +149,11 @@ def test_does_not_accept_enum_literal_in_place_of_int(): def test_accepts_json_string_as_enum_variable(): - result = graphql(Schema, 'query test($color: Color!) { colorEnum(fromEnum: $color) }', variable_values={ - 'color': 'BLUE'}) + result = graphql( + Schema, + 'query test($color: Color!) { colorEnum(fromEnum: $color) }', + variable_values={'color': 'BLUE'} + ) assert not result.errors assert result.data == {'colorEnum': 'BLUE'} @@ -195,6 +212,26 @@ def test_enum_inputs_may_be_nullable(): assert result.data == {'colorEnum': None, 'colorInt': None} +def test_presents_a_get_values_api(): + values = ColorType.get_values() + assert len(values) == 3 + assert values[0].name == 'RED' + assert values[0].value == 0 + assert values[1].name == 'GREEN' + assert values[1].value == 1 + assert values[2].name == 'BLUE' + assert values[2].value == 2 + + +def test_presents_a_get_value_api(): + oneValue = ColorType.get_value('RED') + assert oneValue.name == 'RED' + assert oneValue.value == 0 + + badUsage = ColorType.get_value(0) + assert badUsage is None + + def test_sorts_values_if_not_using_ordered_dict(): enum = GraphQLEnumType(name='Test', values={ 'c': GraphQLEnumValue(),