Skip to content

Commit

Permalink
Support transformation of argument names (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cito committed Jun 30, 2019
1 parent 6cfc9c7 commit b732c1b
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 25 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Expand Down
11 changes: 7 additions & 4 deletions graphql/execution/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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


Expand Down
42 changes: 33 additions & 9 deletions graphql/type/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


Expand Down Expand Up @@ -529,24 +531,29 @@ 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__(
self,
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):
Expand All @@ -555,13 +562,15 @@ 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]:
return dict(
type_=self.type,
default_value=self.default_value,
description=self.description,
out_name=self.out_name,
ast_node=self.ast_node,
)

Expand Down Expand Up @@ -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]]

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -1204,38 +1218,48 @@ 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]:
return dict(
type_=self.type,
description=self.description,
default_value=self.default_value,
out_name=self.out_name,
ast_node=self.ast_node,
)

Expand Down
9 changes: 7 additions & 2 deletions graphql/utilities/coerce_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions graphql/utilities/value_from_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
81 changes: 74 additions & 7 deletions tests/execution/test_resolve.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from json import dumps

from graphql import graphql_sync
from graphql.type import (
GraphQLArgument,
GraphQLField,
GraphQLInputField,
GraphQLInputObjectType,
GraphQLInt,
GraphQLObjectType,
GraphQLSchema,
Expand Down Expand Up @@ -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,
)
Loading

0 comments on commit b732c1b

Please sign in to comment.