diff --git a/graphql/core/execution/base.py b/graphql/core/execution/base.py index c91a22f1..ae75647a 100644 --- a/graphql/core/execution/base.py +++ b/graphql/core/execution/base.py @@ -15,7 +15,7 @@ TypeMetaFieldDef, TypeNameMetaFieldDef, ) -from ..utils import type_from_ast +from ..utils.type_from_ast import type_from_ast from .values import get_argument_values, get_variable_values Undefined = object() diff --git a/graphql/core/execution/executor.py b/graphql/core/execution/executor.py index f9140a60..ad5ddada 100644 --- a/graphql/core/execution/executor.py +++ b/graphql/core/execution/executor.py @@ -8,7 +8,7 @@ from ..language.source import Source from ..type import GraphQLEnumType, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, \ GraphQLScalarType, GraphQLUnionType -from ..utils import is_nullish +from ..utils.is_nullish import is_nullish from ..validation import validate from .base import ExecutionContext, ExecutionResult, ResolveInfo, Undefined, collect_fields, default_resolve_fn, \ get_argument_values, get_field_def, get_operation_root_type diff --git a/graphql/core/execution/values.py b/graphql/core/execution/values.py index 70d7d871..d8798c6e 100644 --- a/graphql/core/execution/values.py +++ b/graphql/core/execution/values.py @@ -11,7 +11,8 @@ GraphQLScalarType, is_input_type ) -from ..utils import is_nullish, type_from_ast +from ..utils.is_nullish import is_nullish +from ..utils.type_from_ast import type_from_ast __all__ = ['get_variable_values', 'get_argument_values'] diff --git a/graphql/core/type/introspection.py b/graphql/core/type/introspection.py index ab312c20..e97fe02d 100644 --- a/graphql/core/type/introspection.py +++ b/graphql/core/type/introspection.py @@ -1,4 +1,5 @@ -import json +from ..language.printer import print_ast +from ..utils.ast_from_value import ast_from_value from .definition import ( GraphQLArgument, GraphQLEnumType, @@ -186,7 +187,7 @@ def input_fields(type, *_): type=GraphQLString, resolver=lambda input_val, *_: None if input_val.default_value is None - else json.dumps(input_val.default_value) + else print_ast(ast_from_value(input_val.default_value, input_val)) ) }) diff --git a/graphql/core/utils/__init__.py b/graphql/core/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphql/core/utils/ast_from_value.py b/graphql/core/utils/ast_from_value.py new file mode 100644 index 00000000..8a2d009f --- /dev/null +++ b/graphql/core/utils/ast_from_value.py @@ -0,0 +1,70 @@ +import json +import re +import sys +from ..compat import str_type +from ..language import ast +from ..type.definition import ( + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLList, + GraphQLNonNull, +) +from ..type.scalars import GraphQLFloat +from .is_nullish import is_nullish + + +def ast_from_value(value, type=None): + if isinstance(type, GraphQLNonNull): + return ast_from_value(value, type.of_type) + + if is_nullish(value): + return None + + if isinstance(value, list): + item_type = type.of_type if isinstance(type, GraphQLList) else None + return ast.ListValue([ast_from_value(item, item_type) for item in value]) + + elif isinstance(type, GraphQLList): + return ast_from_value(value, type.of_type) + + if isinstance(value, bool): + return ast.BooleanValue(value) + + if isinstance(value, (int, float)): + string_num = str(value) + int_value = int(value) + is_int_value = string_num.isdigit() + + if is_int_value or (int_value == value and value < sys.maxsize): + if type == GraphQLFloat: + return ast.FloatValue(str(float(value))) + + return ast.IntValue(str(int(value))) + + return ast.FloatValue(string_num) + + if isinstance(value, str_type): + if isinstance(type, GraphQLEnumType) and re.match(r'^[_a-zA-Z][_a-zA-Z0-9]*$', value): + return ast.EnumValue(value) + + return ast.StringValue(json.dumps(value)[1:-1]) + + assert isinstance(value, dict) + + fields = [] + is_graph_ql_input_object_type = isinstance(type, GraphQLInputObjectType) + + for field_name, field_value in value.items(): + field_type = None + if is_graph_ql_input_object_type: + field_def = type.get_fields().get(field_name) + field_type = field_def and field_def.type + + field_value = ast_from_value(field_value, field_type) + if field_value: + fields.append(ast.ObjectField( + ast.Name(field_name), + field_value + )) + + return ast.ObjectValue(fields) diff --git a/graphql/core/utils/get_field_def.py b/graphql/core/utils/get_field_def.py new file mode 100644 index 00000000..278f5fd5 --- /dev/null +++ b/graphql/core/utils/get_field_def.py @@ -0,0 +1,26 @@ +from ..type.definition import ( + GraphQLInterfaceType, + GraphQLObjectType, + GraphQLUnionType, +) +from ..type.introspection import SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef + + +def get_field_def(schema, parent_type, field_ast): + """Not exactly the same as the executor's definition of get_field_def, in this + statically evaluated environment we do not always have an Object type, + and need to handle Interface and Union types.""" + name = field_ast.name.value + if name == SchemaMetaFieldDef.name and schema.get_query_type() == parent_type: + return SchemaMetaFieldDef + elif name == TypeMetaFieldDef.name and schema.get_query_type() == parent_type: + return TypeMetaFieldDef + elif name == TypeNameMetaFieldDef.name and \ + isinstance(parent_type, ( + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLUnionType, + )): + return TypeNameMetaFieldDef + elif isinstance(parent_type, (GraphQLObjectType, GraphQLInterfaceType)): + return parent_type.get_fields().get(name) diff --git a/graphql/core/utils/is_nullish.py b/graphql/core/utils/is_nullish.py new file mode 100644 index 00000000..3e3a03f9 --- /dev/null +++ b/graphql/core/utils/is_nullish.py @@ -0,0 +1,2 @@ +def is_nullish(value): + return value is None or value != value diff --git a/graphql/core/utils/is_valid_literal_value.py b/graphql/core/utils/is_valid_literal_value.py new file mode 100644 index 00000000..57f47b7e --- /dev/null +++ b/graphql/core/utils/is_valid_literal_value.py @@ -0,0 +1,52 @@ +from ..language import ast +from ..type.definition import ( + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLList, + GraphQLNonNull, + GraphQLScalarType, +) +from .is_nullish import is_nullish + + +def is_valid_literal_value(type, value_ast): + if isinstance(type, GraphQLNonNull): + if not value_ast: + return False + + of_type = type.of_type + return is_valid_literal_value(of_type, value_ast) + + if not value_ast: + return True + + if isinstance(value_ast, ast.Variable): + return True + + if isinstance(type, GraphQLList): + item_type = type.of_type + if isinstance(value_ast, ast.ListValue): + return all(is_valid_literal_value(item_type, item_ast) for item_ast in value_ast.values) + + return is_valid_literal_value(item_type, value_ast) + + if isinstance(type, GraphQLInputObjectType): + if not isinstance(value_ast, ast.ObjectValue): + return False + + fields = type.get_fields() + field_asts = value_ast.fields + + if any(not fields.get(field_ast.name.value, None) for field_ast in field_asts): + return False + + field_ast_map = {field_ast.name.value: field_ast for field_ast in field_asts} + get_field_ast_value = lambda field_name: field_ast_map[ + field_name].value if field_name in field_ast_map else None + + return all(is_valid_literal_value(field.type, get_field_ast_value(field_name)) + for field_name, field in fields.items()) + + assert isinstance(type, (GraphQLScalarType, GraphQLEnumType)), 'Must be input type' + + return not is_nullish(type.parse_literal(value_ast)) diff --git a/graphql/core/utils/type_from_ast.py b/graphql/core/utils/type_from_ast.py new file mode 100644 index 00000000..e52c907e --- /dev/null +++ b/graphql/core/utils/type_from_ast.py @@ -0,0 +1,24 @@ +from ..language import ast +from ..type.definition import ( + GraphQLList, + GraphQLNonNull, +) + + +def type_from_ast(schema, input_type_ast): + if isinstance(input_type_ast, ast.ListType): + inner_type = type_from_ast(schema, input_type_ast.type) + if inner_type: + return GraphQLList(inner_type) + else: + return None + + if isinstance(input_type_ast, ast.NonNullType): + inner_type = type_from_ast(schema, input_type_ast.type) + if inner_type: + return GraphQLNonNull(inner_type) + else: + return None + + assert isinstance(input_type_ast, ast.NamedType), 'Must be a type name.' + return schema.get_type(input_type_ast.name.value) diff --git a/graphql/core/utils.py b/graphql/core/utils/type_info.py similarity index 57% rename from graphql/core/utils.py rename to graphql/core/utils/type_info.py index c6df6d33..3b9404f1 100644 --- a/graphql/core/utils.py +++ b/graphql/core/utils/type_info.py @@ -1,39 +1,14 @@ -from .language import ast -from .type.definition import ( - GraphQLEnumType, +from ..language import ast +from ..type.definition import ( GraphQLInputObjectType, - GraphQLInterfaceType, GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLScalarType, - GraphQLUnionType, get_named_type, get_nullable_type, is_composite_type, ) -from .type.introspection import SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef - -def type_from_ast(schema, input_type_ast): - if isinstance(input_type_ast, ast.ListType): - inner_type = type_from_ast(schema, input_type_ast.type) - if inner_type: - return GraphQLList(inner_type) - else: - return None - if isinstance(input_type_ast, ast.NonNullType): - inner_type = type_from_ast(schema, input_type_ast.type) - if inner_type: - return GraphQLNonNull(inner_type) - else: - return None - assert isinstance(input_type_ast, ast.NamedType), 'Must be a type name.' - return schema.get_type(input_type_ast.name.value) - - -def is_nullish(value): - return value is None or value != value +from .get_field_def import get_field_def +from .type_from_ast import type_from_ast def pop(lst): @@ -149,66 +124,3 @@ def leave(self, node): pop(self._input_type_stack) elif isinstance(node, (ast.ListType, ast.ObjectField)): pop(self._input_type_stack) - - -def get_field_def(schema, parent_type, field_ast): - """Not exactly the same as the executor's definition of get_field_def, in this - statically evaluated environment we do not always have an Object type, - and need to handle Interface and Union types.""" - name = field_ast.name.value - if name == SchemaMetaFieldDef.name and schema.get_query_type() == parent_type: - return SchemaMetaFieldDef - elif name == TypeMetaFieldDef.name and schema.get_query_type() == parent_type: - return TypeMetaFieldDef - elif name == TypeNameMetaFieldDef.name and \ - isinstance(parent_type, ( - GraphQLObjectType, - GraphQLInterfaceType, - GraphQLUnionType, - )): - return TypeNameMetaFieldDef - elif isinstance(parent_type, (GraphQLObjectType, GraphQLInterfaceType)): - return parent_type.get_fields().get(name) - - -def is_valid_literal_value(type, value_ast): - if isinstance(type, GraphQLNonNull): - if not value_ast: - return False - - of_type = type.of_type - return is_valid_literal_value(of_type, value_ast) - - if not value_ast: - return True - - if isinstance(value_ast, ast.Variable): - return True - - if isinstance(type, GraphQLList): - item_type = type.of_type - if isinstance(value_ast, ast.ListValue): - return all(is_valid_literal_value(item_type, item_ast) for item_ast in value_ast.values) - - return is_valid_literal_value(item_type, value_ast) - - if isinstance(type, GraphQLInputObjectType): - if not isinstance(value_ast, ast.ObjectValue): - return False - - fields = type.get_fields() - field_asts = value_ast.fields - - if any(not fields.get(field_ast.name.value, None) for field_ast in field_asts): - return False - - field_ast_map = {field_ast.name.value: field_ast for field_ast in field_asts} - get_field_ast_value = lambda field_name: field_ast_map[ - field_name].value if field_name in field_ast_map else None - - return all(is_valid_literal_value(field.type, get_field_ast_value(field_name)) - for field_name, field in fields.items()) - - assert isinstance(type, (GraphQLScalarType, GraphQLEnumType)), 'Must be input type' - - return not is_nullish(type.parse_literal(value_ast)) diff --git a/graphql/core/validation/__init__.py b/graphql/core/validation/__init__.py index 3876f3a8..00ded816 100644 --- a/graphql/core/validation/__init__.py +++ b/graphql/core/validation/__init__.py @@ -3,7 +3,7 @@ from ..language.ast import FragmentDefinition, FragmentSpread from ..language.visitor import Visitor, visit from ..type import GraphQLSchema -from ..utils import TypeInfo +from ..utils.type_info import TypeInfo specified_rules = [ Rules.UniqueOperationNames, diff --git a/graphql/core/validation/rules.py b/graphql/core/validation/rules.py index 6a6b3d60..2c7df44e 100644 --- a/graphql/core/validation/rules.py +++ b/graphql/core/validation/rules.py @@ -14,7 +14,8 @@ is_input_type, is_leaf_type, ) -from ..utils import is_valid_literal_value, type_from_ast +from ..utils.is_valid_literal_value import is_valid_literal_value +from ..utils.type_from_ast import type_from_ast from .utils import DefaultOrderedDict, PairSet diff --git a/tests/core_utils/test_ast_from_value.py b/tests/core_utils/test_ast_from_value.py new file mode 100644 index 00000000..6bd728a3 --- /dev/null +++ b/tests/core_utils/test_ast_from_value.py @@ -0,0 +1,100 @@ +from collections import OrderedDict +from graphql.core.type.definition import GraphQLEnumType, GraphQLList, GraphQLInputObjectType, GraphQLInputObjectField +from graphql.core.type.scalars import GraphQLFloat +from graphql.core.utils.ast_from_value import ast_from_value +from graphql.core.language import ast + + +def test_converts_boolean_values_to_asts(): + assert ast_from_value(True) == ast.BooleanValue(True) + assert ast_from_value(False) == ast.BooleanValue(False) + + +def test_converts_numeric_values_to_asts(): + assert ast_from_value(123) == ast.IntValue('123') + assert ast_from_value(123.0) == ast.IntValue('123') + assert ast_from_value(123.5) == ast.FloatValue('123.5') + assert ast_from_value(1e4) == ast.IntValue('10000') + assert ast_from_value(1e40) == ast.FloatValue('1e+40') + + +def test_it_converts_numeric_values_to_float_asts(): + assert ast_from_value(123, GraphQLFloat) == ast.FloatValue('123.0') + assert ast_from_value(123.0, GraphQLFloat) == ast.FloatValue('123.0') + assert ast_from_value(123.5, GraphQLFloat) == ast.FloatValue('123.5') + assert ast_from_value(1e4, GraphQLFloat) == ast.FloatValue('10000.0') + assert ast_from_value(1e40, GraphQLFloat) == ast.FloatValue('1e+40') + + +def test_it_converts_string_values_to_asts(): + assert ast_from_value('hello') == ast.StringValue('hello') + assert ast_from_value('VALUE') == ast.StringValue('VALUE') + assert ast_from_value(u'VAL\nUE') == ast.StringValue('VAL\\nUE') + assert ast_from_value('VAL\nUE') == ast.StringValue('VAL\\nUE') + assert ast_from_value('123') == ast.StringValue('123') + + +my_enum = GraphQLEnumType( + 'MyEnum', { + 'HELLO': 1, + 'GOODBYE': 2 + } +) + + +def test_converts_string_values_to_enum_asts_if_possible(): + assert ast_from_value('hello', my_enum) == ast.EnumValue('hello') + assert ast_from_value('HELLO', my_enum) == ast.EnumValue('HELLO') + assert ast_from_value('VAL\nUE', my_enum) == ast.StringValue('VAL\\nUE') + assert ast_from_value('123', my_enum) == ast.StringValue('123') + + +def test_converts_array_values_to_list_asts(): + assert ast_from_value(['FOO', 'BAR']) == ast.ListValue( + values=[ + ast.StringValue('FOO'), + ast.StringValue('BAR') + ] + ) + + +def test_converts_list_singletons(): + assert ast_from_value('FOO', GraphQLList(my_enum)) == ast.EnumValue('FOO') + + +def test_converts_input_objects(): + value = OrderedDict([ + ('foo', 3), + ('bar', 'HELLO') + ]) + + assert ast_from_value(value) == ast.ObjectValue( + fields=[ + ast.ObjectField( + name=ast.Name('foo'), + value=ast.IntValue('3') + ), + ast.ObjectField( + name=ast.Name('bar'), + value=ast.StringValue('HELLO') + ) + ] + ) + + input_obj = GraphQLInputObjectType('MyInputObj', { + 'foo': GraphQLInputObjectField(GraphQLFloat), + 'bar': GraphQLInputObjectField(my_enum) + }) + + assert ast_from_value(value, input_obj) == ast.ObjectValue( + fields=[ + ast.ObjectField( + name=ast.Name('foo'), + value=ast.FloatValue('3.0') + ), + ast.ObjectField( + name=ast.Name('bar'), + value=ast.EnumValue('HELLO') + ) + ] + )