diff --git a/README.md b/README.md index 5c5405ae..35c14681 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ a query language for APIs created by Facebook. [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) The current version 1.0.5 of GraphQL-core-next is up-to-date with GraphQL.js version -14.3.1. All parts of the API are covered by an extensive test suite of currently 1792 +14.3.1. All parts of the API are covered by an extensive test suite of currently 1814 unit tests. diff --git a/graphql/execution/values.py b/graphql/execution/values.py index bbf69e5d..316ed36d 100644 --- a/graphql/execution/values.py +++ b/graphql/execution/values.py @@ -138,7 +138,8 @@ def get_argument_values( if not has_value and arg_def.default_value is not INVALID: # If no argument was provided where the definition has a default value, # use the default value. - coerced_values[name] = arg_def.default_value + # If an out name exists, we use that as the name (extension of GraphQL.js). + coerced_values[arg_def.out_name or name] = arg_def.default_value elif (not has_value or is_null) and is_non_null_type(arg_type): # If no argument or a null value was provided to an argument with a non-null # type (required), produce a field error. @@ -166,13 +167,15 @@ def get_argument_values( if isinstance(argument_node.value, NullValueNode): # If the explicit value `None` was provided, an entry in the coerced # values must exist as the value `None`. - coerced_values[name] = None + coerced_values[arg_def.out_name or name] = None elif isinstance(argument_node.value, VariableNode): variable_name = argument_node.value.name.value # Note: This Does no further checking that this variable is correct. # This assumes that this query has been validated and the variable # usage here is of the correct type. - coerced_values[name] = variable_values[variable_name] + coerced_values[arg_def.out_name or name] = variable_values[ + variable_name + ] else: value_node = argument_node.value coerced_value = value_from_ast(value_node, arg_type, variable_values) @@ -185,7 +188,7 @@ def get_argument_values( f" has invalid value {print_ast(value_node)}.", argument_node.value, ) - coerced_values[name] = coerced_value + coerced_values[arg_def.out_name or name] = coerced_value return coerced_values diff --git a/graphql/type/definition.py b/graphql/type/definition.py index 9b22c7dc..11cced91 100644 --- a/graphql/type/definition.py +++ b/graphql/type/definition.py @@ -365,9 +365,11 @@ def __str__(self): def to_kwargs(self) -> Dict[str, Any]: return dict( **super().to_kwargs(), - serialize=self.serialize, - parse_value=self.parse_value, - parse_literal=self.parse_literal, + serialize=None if self.serialize is identity_func else self.serialize, + parse_value=None if self.parse_value is identity_func else self.parse_value, + parse_literal=None + if self.parse_literal is value_from_ast_untyped + else self.parse_literal, ) @@ -529,6 +531,7 @@ class GraphQLArgument: type: "GraphQLInputType" default_value: Any description: Optional[str] + out_name: Optional[str] # for transforming names (extension of GraphQL.js) ast_node: Optional[InputValueDefinitionNode] def __init__( @@ -536,17 +539,21 @@ def __init__( type_: "GraphQLInputType", default_value: Any = INVALID, description: str = None, + out_name: str = None, ast_node: InputValueDefinitionNode = None, ) -> None: if not is_input_type(type_): raise TypeError(f"Argument type must be a GraphQL input type.") if description is not None and not isinstance(description, str): - raise TypeError("The description must be a string.") + raise TypeError("Argument description must be a string.") + if out_name is not None and not isinstance(out_name, str): + raise TypeError("Argument out name must be a string.") if ast_node and not isinstance(ast_node, InputValueDefinitionNode): raise TypeError("Argument AST node must be an InputValueDefinitionNode.") self.type = type_ self.default_value = default_value self.description = description + self.out_name = out_name self.ast_node = ast_node def __eq__(self, other): @@ -555,6 +562,7 @@ def __eq__(self, other): and self.type == other.type and self.default_value == other.default_value and self.description == other.description + and self.out_name == other.out_name ) def to_kwargs(self) -> Dict[str, Any]: @@ -562,6 +570,7 @@ def to_kwargs(self) -> Dict[str, Any]: type_=self.type, default_value=self.default_value, description=self.description, + out_name=self.out_name, ast_node=self.ast_node, ) @@ -1119,8 +1128,7 @@ class GeoPoint(GraphQLInputObjectType): converted to other types by specifying an `out_type` function or class. """ - # Transforms values to different type (this is an extension of GraphQL.js). - out_type: GraphQLInputFieldOutType + out_type: GraphQLInputFieldOutType # transforms values (extension of GraphQL.js) ast_node: Optional[InputObjectTypeDefinitionNode] extension_ast_nodes: Optional[Tuple[InputObjectTypeExtensionNode]] @@ -1156,7 +1164,13 @@ def __init__( self.out_type = out_type or identity_func # type: ignore def to_kwargs(self) -> Dict[str, Any]: - return dict(**super().to_kwargs(), fields=self.fields.copy()) + return dict( + **super().to_kwargs(), + fields=self.fields.copy(), + out_type=None + if self.out_type is identity_func # type: ignore + else self.out_type, # type: ignore + ) @cached_property def fields(self) -> GraphQLInputFieldMap: @@ -1204,31 +1218,40 @@ class GraphQLInputField: """Definition of a GraphQL input field""" type: "GraphQLInputType" - description: Optional[str] default_value: Any + description: Optional[str] + out_name: Optional[str] # for transforming names (extension of GraphQL.js) ast_node: Optional[InputValueDefinitionNode] def __init__( self, type_: "GraphQLInputType", - description: str = None, default_value: Any = INVALID, + description: str = None, + out_name: str = None, ast_node: InputValueDefinitionNode = None, ) -> None: if not is_input_type(type_): raise TypeError(f"Input field type must be a GraphQL input type.") + if description is not None and not isinstance(description, str): + raise TypeError("Input field description must be a string.") + if out_name is not None and not isinstance(out_name, str): + raise TypeError("Input field out name must be a string.") if ast_node and not isinstance(ast_node, InputValueDefinitionNode): raise TypeError("Input field AST node must be an InputValueDefinitionNode.") self.type = type_ self.default_value = default_value self.description = description + self.out_name = out_name self.ast_node = ast_node def __eq__(self, other): return self is other or ( isinstance(other, GraphQLInputField) and self.type == other.type + and self.default_value == other.default_value and self.description == other.description + and self.out_name == other.out_name ) def to_kwargs(self) -> Dict[str, Any]: @@ -1236,6 +1259,7 @@ def to_kwargs(self) -> Dict[str, Any]: type_=self.type, description=self.description, default_value=self.default_value, + out_name=self.out_name, ast_node=self.ast_node, ) diff --git a/graphql/utilities/coerce_value.py b/graphql/utilities/coerce_value.py index 17c17ee9..75deaabc 100644 --- a/graphql/utilities/coerce_value.py +++ b/graphql/utilities/coerce_value.py @@ -139,7 +139,10 @@ def coerce_value( field_value = value.get(field_name, INVALID) if is_invalid(field_value): if not is_invalid(field.default_value): - coerced_value_dict[field_name] = field.default_value + # Use out name as name if it exists (extension of GraphQL.js). + coerced_value_dict[ + field.out_name or field_name + ] = field.default_value elif is_non_null_type(field.type): errors = add( errors, @@ -156,7 +159,9 @@ def coerce_value( if coerced_field.errors: errors = add(errors, *coerced_field.errors) else: - coerced_value_dict[field_name] = coerced_field.value + coerced_value_dict[ + field.out_name or field_name + ] = coerced_field.value # Ensure every provided field is defined. for field_name in value: diff --git a/graphql/utilities/value_from_ast.py b/graphql/utilities/value_from_ast.py index e7a58da1..d5baa72d 100644 --- a/graphql/utilities/value_from_ast.py +++ b/graphql/utilities/value_from_ast.py @@ -114,14 +114,15 @@ def value_from_ast( field_node = field_nodes.get(field_name) if not field_node or is_missing_variable(field_node.value, variables): if field.default_value is not INVALID: - coerced_obj[field_name] = field.default_value + # Use out name as name if it exists (extension of GraphQL.js). + coerced_obj[field.out_name or field_name] = field.default_value elif is_non_null_type(field.type): return INVALID continue field_value = value_from_ast(field_node.value, field.type, variables) if is_invalid(field_value): return INVALID - coerced_obj[field_name] = field_value + coerced_obj[field.out_name or field_name] = field_value return type_.out_type(coerced_obj) # type: ignore diff --git a/tests/execution/test_resolve.py b/tests/execution/test_resolve.py index 694ce379..189eb645 100644 --- a/tests/execution/test_resolve.py +++ b/tests/execution/test_resolve.py @@ -1,9 +1,9 @@ -from json import dumps - from graphql import graphql_sync from graphql.type import ( GraphQLArgument, GraphQLField, + GraphQLInputField, + GraphQLInputObjectType, GraphQLInt, GraphQLObjectType, GraphQLSchema, @@ -79,22 +79,89 @@ def uses_provided_resolve_function(): "aStr": GraphQLArgument(GraphQLString), "aInt": GraphQLArgument(GraphQLInt), }, - resolve=lambda source, info, **args: dumps([source, args]), + resolve=lambda source, info, **args: repr([source, args]), ) ) - assert graphql_sync(schema, "{ test }") == ({"test": "[null, {}]"}, None) + assert graphql_sync(schema, "{ test }") == ({"test": "[None, {}]"}, None) assert graphql_sync(schema, "{ test }", "Source!") == ( - {"test": '["Source!", {}]'}, + {"test": "['Source!', {}]"}, None, ) assert graphql_sync(schema, '{ test(aStr: "String!") }', "Source!") == ( - {"test": '["Source!", {"aStr": "String!"}]'}, + {"test": "['Source!', {'aStr': 'String!'}]"}, None, ) assert graphql_sync( schema, '{ test(aInt: -123, aStr: "String!") }', "Source!" - ) == ({"test": '["Source!", {"aStr": "String!", "aInt": -123}]'}, None) + ) == ({"test": "['Source!', {'aStr': 'String!', 'aInt': -123}]"}, None) + + def transforms_arguments_using_out_names(): + # This is an extension of GraphQL.js. + schema = _test_schema( + GraphQLField( + GraphQLString, + args={ + "aStr": GraphQLArgument(GraphQLString, out_name="a_str"), + "aInt": GraphQLArgument(GraphQLInt, out_name="a_int"), + }, + resolve=lambda source, info, **args: repr([source, args]), + ) + ) + + assert graphql_sync(schema, "{ test }") == ({"test": "[None, {}]"}, None) + + assert graphql_sync(schema, "{ test }", "Source!") == ( + {"test": "['Source!', {}]"}, + None, + ) + + assert graphql_sync(schema, '{ test(aStr: "String!") }', "Source!") == ( + {"test": "['Source!', {'a_str': 'String!'}]"}, + None, + ) + + assert graphql_sync( + schema, '{ test(aInt: -123, aStr: "String!") }', "Source!" + ) == ({"test": "['Source!', {'a_str': 'String!', 'a_int': -123}]"}, None) + + def transforms_arguments_with_inputs_using_out_names(): + # This is an extension of GraphQL.js. + TestInputObject = GraphQLInputObjectType( + "TestInputObjectType", + lambda: { + "inputOne": GraphQLInputField(GraphQLString, out_name="input_one"), + "inputRecursive": GraphQLInputField( + TestInputObject, out_name="input_recursive" + ), + }, + ) + + schema = _test_schema( + GraphQLField( + GraphQLString, + args={"aInput": GraphQLArgument(TestInputObject, out_name="a_input")}, + resolve=lambda source, info, **args: repr([source, args]), + ) + ) + + assert graphql_sync(schema, "{ test }") == ({"test": "[None, {}]"}, None) + + assert graphql_sync( + schema, '{ test(aInput: {inputOne: "String!"}) }', "Source!" + ) == ({"test": "['Source!', {'a_input': {'input_one': 'String!'}}]"}, None) + + assert graphql_sync( + schema, + '{ test(aInput: {inputRecursive: {inputOne: "SourceRecursive!"}}) }', + "Source!", + ) == ( + { + "test": "['Source!'," + " {'a_input': {'input_recursive': {'input_one': 'SourceRecursive!'}}}]" + }, + None, + ) diff --git a/tests/type/test_definition.py b/tests/type/test_definition.py index d01df2dd..44752a5f 100644 --- a/tests/type/test_definition.py +++ b/tests/type/test_definition.py @@ -3,6 +3,7 @@ from pytest import mark, raises from graphql.error import INVALID +from graphql.language import Node, InputValueDefinitionNode from graphql.pyutils import identity_func from graphql.type import ( GraphQLArgument, @@ -51,6 +52,11 @@ def provides_default_methods_if_omitted(): assert scalar.parse_value is identity_func assert scalar.parse_literal is value_from_ast_untyped + kwargs = scalar.to_kwargs() + assert kwargs["serialize"] is None + assert kwargs["parse_value"] is None + assert kwargs["parse_literal"] is None + def rejects_a_scalar_type_defining_serialize_with_incorrect_type(): with raises(TypeError) as exc_info: # noinspection PyTypeChecker @@ -477,11 +483,13 @@ def accepts_an_input_object_type_with_an_out_type_function(): "SomeInputObject", {}, out_type=dict ) assert input_obj_type.out_type is dict + assert input_obj_type.to_kwargs()["out_type"] is dict def provides_default_out_type_if_omitted(): # This is an extension of GraphQL.js. input_obj_type = GraphQLInputObjectType("SomeInputObject", {}) assert input_obj_type.out_type is identity_func + assert input_obj_type.to_kwargs()["out_type"] is None def rejects_an_input_object_type_with_incorrect_fields(): input_obj_type = GraphQLInputObjectType("SomeInputObject", []) @@ -548,6 +556,128 @@ def rejects_an_input_object_type_with_resolver_constant(): ) +def describe_type_system_arguments(): + def accepts_an_argument_with_a_description(): + description = "nice argument" + argument = GraphQLArgument(GraphQLString, description=description) + assert argument.description is description + assert argument.to_kwargs()["description"] is description + + def accepts_an_argument_with_an_out_name(): + # This is an extension of GraphQL.js. + out_name = "python_rocks" + argument = GraphQLArgument(GraphQLString, out_name=out_name) + assert argument.out_name is out_name + assert argument.to_kwargs()["out_name"] is out_name + + def provides_no_out_name_if_omitted(): + # This is an extension of GraphQL.js. + argument = GraphQLArgument(GraphQLString) + assert argument.out_name is None + assert argument.to_kwargs()["out_name"] is None + + def accepts_an_argument_with_an_ast_node(): + ast_node = InputValueDefinitionNode() + argument = GraphQLArgument(GraphQLString, ast_node=ast_node) + assert argument.ast_node is ast_node + assert argument.to_kwargs()["ast_node"] is ast_node + + def rejects_an_argument_without_type(): + with raises(TypeError, match="missing 1 required positional argument"): + # noinspection PyArgumentList + GraphQLArgument() # type: ignore + + def rejects_an_argument_with_an_incorrect_type(): + with raises(TypeError) as exc_info: + # noinspection PyTypeChecker + GraphQLArgument(GraphQLObjectType) # type: ignore + msg = str(exc_info.value) + assert msg == "Argument type must be a GraphQL input type." + + def rejects_an_argument_with_an_incorrect_description(): + with raises(TypeError) as exc_info: + # noinspection PyTypeChecker + GraphQLArgument(GraphQLString, description=[]) # type: ignore + msg = str(exc_info.value) + assert msg == "Argument description must be a string." + + def rejects_an_argument_with_an_incorrect_out_name(): + # This is an extension of GraphQL.js. + with raises(TypeError) as exc_info: + # noinspection PyTypeChecker + GraphQLArgument(GraphQLString, out_name=[]) # type: ignore + msg = str(exc_info.value) + assert msg == "Argument out name must be a string." + + def rejects_an_argument_with_an_incorrect_ast_node(): + with raises(TypeError) as exc_info: + # noinspection PyTypeChecker + GraphQLArgument(GraphQLString, ast_node=Node()) # type: ignore + msg = str(exc_info.value) + assert msg == "Argument AST node must be an InputValueDefinitionNode." + + +def describe_type_system_input_fields(): + def accepts_an_input_field_with_a_description(): + description = "good input" + input_field = GraphQLInputField(GraphQLString, description=description) + assert input_field.description is description + assert input_field.to_kwargs()["description"] is description + + def accepts_an_input_field_with_an_out_name(): + # This is an extension of GraphQL.js. + out_name = "python_rocks" + input_field = GraphQLInputField(GraphQLString, out_name=out_name) + assert input_field.out_name is out_name + assert input_field.to_kwargs()["out_name"] is out_name + + def provides_no_out_name_if_omitted(): + # This is an extension of GraphQL.js. + input_field = GraphQLInputField(GraphQLString) + assert input_field.out_name is None + assert input_field.to_kwargs()["out_name"] is None + + def accepts_an_input_field_with_an_ast_node(): + ast_node = InputValueDefinitionNode() + input_field = GraphQLArgument(GraphQLString, ast_node=ast_node) + assert input_field.ast_node is ast_node + assert input_field.to_kwargs()["ast_node"] is ast_node + + def rejects_an_input_field_without_type(): + with raises(TypeError, match="missing 1 required positional argument"): + # noinspection PyArgumentList + GraphQLInputField() # type: ignore + + def rejects_an_input_field_with_an_incorrect_type(): + with raises(TypeError) as exc_info: + # noinspection PyTypeChecker + GraphQLInputField(GraphQLObjectType) # type: ignore + msg = str(exc_info.value) + assert msg == "Input field type must be a GraphQL input type." + + def rejects_an_input_field_with_an_incorrect_description(): + with raises(TypeError) as exc_info: + # noinspection PyTypeChecker + GraphQLInputField(GraphQLString, description=[]) # type: ignore + msg = str(exc_info.value) + assert msg == "Input field description must be a string." + + def rejects_an_input_field_with_an_incorrect_out_name(): + # This is an extension of GraphQL.js. + with raises(TypeError) as exc_info: + # noinspection PyTypeChecker + GraphQLInputField(GraphQLString, out_name=[]) # type: ignore + msg = str(exc_info.value) + assert msg == "Input field out name must be a string." + + def rejects_an_input_field_with_an_incorrect_ast_node(): + with raises(TypeError) as exc_info: + # noinspection PyTypeChecker + GraphQLInputField(GraphQLString, ast_node=Node()) # type: ignore + msg = str(exc_info.value) + assert msg == "Input field AST node must be an InputValueDefinitionNode." + + def describe_type_system_list(): types = [ ScalarType, diff --git a/tests/utilities/test_coerce_value.py b/tests/utilities/test_coerce_value.py index 1a809ca4..9f2cecdf 100644 --- a/tests/utilities/test_coerce_value.py +++ b/tests/utilities/test_coerce_value.py @@ -241,6 +241,20 @@ def returns_error_for_a_misspelled_field(): " Did you mean bar?" ] + def transforms_names_using_out_name(): + # This is an extension of GraphQL.js. + ComplexInputObject = GraphQLInputObjectType( + "Complex", + { + "realPart": GraphQLInputField(GraphQLFloat, out_name="real_part"), + "imagPart": GraphQLInputField( + GraphQLFloat, default_value=0, out_name="imag_part" + ), + }, + ) + result = coerce_value({"realPart": 1}, ComplexInputObject) + assert expect_value(result) == {"real_part": 1, "imag_part": 0} + def transforms_values_with_out_type(): # This is an extension of GraphQL.js. ComplexInputObject = GraphQLInputObjectType( diff --git a/tests/utilities/test_value_from_ast.py b/tests/utilities/test_value_from_ast.py index fec7dc94..d299ea1b 100644 --- a/tests/utilities/test_value_from_ast.py +++ b/tests/utilities/test_value_from_ast.py @@ -175,6 +175,21 @@ def omits_input_object_fields_for_unprovided_variables(): {"int": 42, "requiredBool": True}, ) + def transforms_names_using_out_name(): + # This is an extension of GraphQL.js. + complex_input_obj = GraphQLInputObjectType( + "Complex", + { + "realPart": GraphQLInputField(GraphQLFloat, out_name="real_part"), + "imagPart": GraphQLInputField( + GraphQLFloat, default_value=0, out_name="imag_part" + ), + }, + ) + _test_case( + complex_input_obj, "{ realPart: 1 }", {"real_part": 1, "imag_part": 0} + ) + def transforms_values_with_out_type(): # This is an extension of GraphQL.js. complex_input_obj = GraphQLInputObjectType(