diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index db20a522..f47fffea 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -270,7 +270,7 @@ The following is an example for creating a DateTime scalar: return dt.isoformat() @staticmethod - def parse_literal(node): + def parse_literal(node, _variables=None): if isinstance(node, ast.StringValue): return datetime.datetime.strptime( node.value, "%Y-%m-%dT%H:%M:%S.%f") diff --git a/graphene/tests/issues/test_1419.py b/graphene/tests/issues/test_1419.py index 243645fa..a08374da 100644 --- a/graphene/tests/issues/test_1419.py +++ b/graphene/tests/issues/test_1419.py @@ -24,8 +24,8 @@ from ...types.uuid import UUID (ID, "1"), (DateTime, '"2022-02-02T11:11:11"'), (UUID, '"cbebbc62-758e-4f75-a890-bc73b5017d81"'), - (Decimal, "1.1"), - (JSONString, '{key:"foo",value:"bar"}'), + (Decimal, '"1.1"'), + (JSONString, '"{\\"key\\":\\"foo\\",\\"value\\":\\"bar\\"}"'), (Base64, '"Q2hlbG8gd29ycmxkCg=="'), ], ) diff --git a/graphene/types/decimal.py b/graphene/types/decimal.py index 94968f49..0c6ccc97 100644 --- a/graphene/types/decimal.py +++ b/graphene/types/decimal.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from decimal import Decimal as _Decimal +from graphql import Undefined from graphql.language.ast import StringValueNode, IntValueNode from .scalars import Scalar @@ -25,10 +26,11 @@ class Decimal(Scalar): def parse_literal(cls, node, _variables=None): if isinstance(node, (StringValueNode, IntValueNode)): return cls.parse_value(node.value) + return Undefined @staticmethod def parse_value(value): try: return _Decimal(value) - except ValueError: - return None + except Exception: + return Undefined diff --git a/graphene/types/json.py b/graphene/types/json.py index 7e60de7e..ca55836b 100644 --- a/graphene/types/json.py +++ b/graphene/types/json.py @@ -2,6 +2,7 @@ from __future__ import absolute_import import json +from graphql import Undefined from graphql.language.ast import StringValueNode from .scalars import Scalar @@ -22,7 +23,11 @@ class JSONString(Scalar): @staticmethod def parse_literal(node, _variables=None): if isinstance(node, StringValueNode): - return json.loads(node.value) + try: + return json.loads(node.value) + except Exception as error: + raise ValueError(f"Badly formed JSONString: {str(error)}") + return Undefined @staticmethod def parse_value(value): diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index 80cfcc8b..a468bb3e 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -1,5 +1,6 @@ from typing import Any +from graphql import Undefined from graphql.language.ast import ( BooleanValueNode, FloatValueNode, @@ -67,9 +68,10 @@ class Int(Scalar): try: num = int(float(value)) except ValueError: - return None + return Undefined if MIN_INT <= num <= MAX_INT: return num + return Undefined serialize = coerce_int parse_value = coerce_int @@ -80,6 +82,7 @@ class Int(Scalar): num = int(ast.value) if MIN_INT <= num <= MAX_INT: return num + return Undefined class BigInt(Scalar): @@ -97,7 +100,7 @@ class BigInt(Scalar): try: num = int(float(value)) except ValueError: - return None + return Undefined return num serialize = coerce_int @@ -107,6 +110,7 @@ class BigInt(Scalar): def parse_literal(ast, _variables=None): if isinstance(ast, IntValueNode): return int(ast.value) + return Undefined class Float(Scalar): @@ -122,7 +126,7 @@ class Float(Scalar): try: return float(value) except ValueError: - return None + return Undefined serialize = coerce_float parse_value = coerce_float @@ -131,6 +135,7 @@ class Float(Scalar): def parse_literal(ast, _variables=None): if isinstance(ast, (FloatValueNode, IntValueNode)): return float(ast.value) + return Undefined class String(Scalar): @@ -153,6 +158,7 @@ class String(Scalar): def parse_literal(ast, _variables=None): if isinstance(ast, StringValueNode): return ast.value + return Undefined class Boolean(Scalar): @@ -167,6 +173,7 @@ class Boolean(Scalar): def parse_literal(ast, _variables=None): if isinstance(ast, BooleanValueNode): return ast.value + return Undefined class ID(Scalar): @@ -185,3 +192,4 @@ class ID(Scalar): def parse_literal(ast, _variables=None): if isinstance(ast, (StringValueNode, IntValueNode)): return ast.value + return Undefined diff --git a/graphene/types/tests/test_decimal.py b/graphene/types/tests/test_decimal.py index 9757e82c..1ba48bd1 100644 --- a/graphene/types/tests/test_decimal.py +++ b/graphene/types/tests/test_decimal.py @@ -39,8 +39,25 @@ def test_bad_decimal_query(): not_a_decimal = "Nobody expects the Spanish Inquisition!" result = schema.execute("""{ decimal(input: "%s") }""" % not_a_decimal) + assert result.errors assert len(result.errors) == 1 assert result.data is None + assert ( + result.errors[0].message + == "Expected value of type 'Decimal', found \"Nobody expects the Spanish Inquisition!\"." + ) + + result = schema.execute("{ decimal(input: true) }") + assert result.errors + assert len(result.errors) == 1 + assert result.data is None + assert result.errors[0].message == "Expected value of type 'Decimal', found true." + + result = schema.execute("{ decimal(input: 1.2) }") + assert result.errors + assert len(result.errors) == 1 + assert result.data is None + assert result.errors[0].message == "Expected value of type 'Decimal', found 1.2." def test_decimal_string_query_integer(): diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py index b5537180..bb754b3a 100644 --- a/graphene/types/tests/test_json.py +++ b/graphene/types/tests/test_json.py @@ -21,6 +21,10 @@ def test_jsonstring_query(): assert not result.errors assert result.data == {"json": json_value} + result = schema.execute("""{ json(input: "{}") }""") + assert not result.errors + assert result.data == {"json": "{}"} + def test_jsonstring_query_variable(): json_value = '{"key": "value"}' @@ -31,3 +35,51 @@ def test_jsonstring_query_variable(): ) assert not result.errors assert result.data == {"json": json_value} + + +def test_jsonstring_optional_uuid_input(): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ json(input: null) }") + assert not result.errors + assert result.data == {"json": None} + + +def test_jsonstring_invalid_query(): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute("{ json(input: 1) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'JSONString', found 1." + + result = schema.execute("{ json(input: {}) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'JSONString', found {}." + + result = schema.execute('{ json(input: "a") }') + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == ( + "Expected value of type 'JSONString', found \"a\"; " + "Badly formed JSONString: Expecting value: line 1 column 1 (char 0)" + ) + + result = schema.execute("""{ json(input: "{\\'key\\': 0}") }""") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Syntax Error: Invalid character escape sequence: '\\''." + ) + + result = schema.execute("""{ json(input: "{\\"key\\": 0,}") }""") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == ( + 'Expected value of type \'JSONString\', found "{\\"key\\": 0,}"; ' + "Badly formed JSONString: Expecting property name enclosed in double quotes: line 1 column 11 (char 10)" + ) diff --git a/graphene/types/tests/test_scalar.py b/graphene/types/tests/test_scalar.py index 9dce6c38..cbdfd8c5 100644 --- a/graphene/types/tests/test_scalar.py +++ b/graphene/types/tests/test_scalar.py @@ -1,4 +1,7 @@ -from ..scalars import Scalar, Int, BigInt +from ..objecttype import ObjectType, Field +from ..scalars import Scalar, Int, BigInt, Float, String, Boolean +from ..schema import Schema +from graphql import Undefined from graphql.language.ast import IntValueNode @@ -11,19 +14,295 @@ def test_scalar(): def test_ints(): - assert Int.parse_value(2**31 - 1) is not None - assert Int.parse_value("2.0") is not None - assert Int.parse_value(2**31) is None + assert Int.parse_value(2**31 - 1) is not Undefined + assert Int.parse_value("2.0") == 2 + assert Int.parse_value(2**31) is Undefined assert Int.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1 - assert Int.parse_literal(IntValueNode(value=str(2**31))) is None + assert Int.parse_literal(IntValueNode(value=str(2**31))) is Undefined - assert Int.parse_value(-(2**31)) is not None - assert Int.parse_value(-(2**31) - 1) is None + assert Int.parse_value(-(2**31)) is not Undefined + assert Int.parse_value(-(2**31) - 1) is Undefined - assert BigInt.parse_value(2**31) is not None - assert BigInt.parse_value("2.0") is not None - assert BigInt.parse_value(-(2**31) - 1) is not None + assert BigInt.parse_value(2**31) is not Undefined + assert BigInt.parse_value("2.0") == 2 + assert BigInt.parse_value(-(2**31) - 1) is not Undefined assert BigInt.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1 assert BigInt.parse_literal(IntValueNode(value=str(2**31))) == 2**31 + + +def return_input(_parent, _info, input): + return input + + +class Optional(ObjectType): + int = Int(input=Int(), resolver=return_input) + big_int = BigInt(input=BigInt(), resolver=return_input) + float = Float(input=Float(), resolver=return_input) + bool = Boolean(input=Boolean(), resolver=return_input) + string = String(input=String(), resolver=return_input) + + +class Query(ObjectType): + optional = Field(Optional) + + def resolve_optional(self, info): + return Optional() + + def resolve_required(self, info, input): + return input + + +schema = Schema(query=Query) + + +class TestInt: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute("{ optional { int(input: 20) } }") + assert not result.errors + assert result.data == {"optional": {"int": 20}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { int(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"int": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { int(input: "20") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == 'Int cannot represent non-integer value: "20"' + ) + + result = schema.execute('{ optional { int(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == 'Int cannot represent non-integer value: "a"' + + result = schema.execute("{ optional { int(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Int cannot represent non-integer value: true" + ) + + +class TestBigInt: + def test_query(self): + """ + Test that a normal query works. + """ + value = 2**31 + result = schema.execute("{ optional { bigInt(input: %s) } }" % value) + assert not result.errors + assert result.data == {"optional": {"bigInt": value}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { bigInt(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"bigInt": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { bigInt(input: "20") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Expected value of type 'BigInt', found \"20\"." + ) + + result = schema.execute('{ optional { bigInt(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Expected value of type 'BigInt', found \"a\"." + ) + + result = schema.execute("{ optional { bigInt(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Expected value of type 'BigInt', found true." + ) + + +class TestFloat: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute("{ optional { float(input: 20) } }") + assert not result.errors + assert result.data == {"optional": {"float": 20.0}} + + result = schema.execute("{ optional { float(input: 20.2) } }") + assert not result.errors + assert result.data == {"optional": {"float": 20.2}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { float(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"float": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { float(input: "20") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == 'Float cannot represent non numeric value: "20"' + ) + + result = schema.execute('{ optional { float(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == 'Float cannot represent non numeric value: "a"' + ) + + result = schema.execute("{ optional { float(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Float cannot represent non numeric value: true" + ) + + +class TestBoolean: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute("{ optional { bool(input: true) } }") + assert not result.errors + assert result.data == {"optional": {"bool": True}} + + result = schema.execute("{ optional { bool(input: false) } }") + assert not result.errors + assert result.data == {"optional": {"bool": False}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { bool(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"bool": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { bool(input: "True") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == 'Boolean cannot represent a non boolean value: "True"' + ) + + result = schema.execute('{ optional { bool(input: "true") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == 'Boolean cannot represent a non boolean value: "true"' + ) + + result = schema.execute('{ optional { bool(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == 'Boolean cannot represent a non boolean value: "a"' + ) + + result = schema.execute("{ optional { bool(input: 1) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Boolean cannot represent a non boolean value: 1" + ) + + result = schema.execute("{ optional { bool(input: 0) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Boolean cannot represent a non boolean value: 0" + ) + + +class TestString: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute('{ optional { string(input: "something something") } }') + assert not result.errors + assert result.data == {"optional": {"string": "something something"}} + + result = schema.execute('{ optional { string(input: "True") } }') + assert not result.errors + assert result.data == {"optional": {"string": "True"}} + + result = schema.execute('{ optional { string(input: "0") } }') + assert not result.errors + assert result.data == {"optional": {"string": "0"}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { string(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"string": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute("{ optional { string(input: 1) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "String cannot represent a non string value: 1" + ) + + result = schema.execute("{ optional { string(input: 3.2) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "String cannot represent a non string value: 3.2" + ) + + result = schema.execute("{ optional { string(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "String cannot represent a non string value: true" + ) diff --git a/graphene/types/tests/test_scalars_serialization.py b/graphene/types/tests/test_scalars_serialization.py index a91efe2c..a0028c85 100644 --- a/graphene/types/tests/test_scalars_serialization.py +++ b/graphene/types/tests/test_scalars_serialization.py @@ -1,3 +1,4 @@ +from graphql import Undefined from ..scalars import Boolean, Float, Int, String @@ -9,12 +10,12 @@ def test_serializes_output_int(): 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(9876504321) is Undefined + assert Int.serialize(-9876504321) is Undefined + assert Int.serialize(1e100) is Undefined + assert Int.serialize(-1e100) is Undefined assert Int.serialize("-1.1") == -1 - assert Int.serialize("one") is None + assert Int.serialize("one") is Undefined assert Int.serialize(False) == 0 assert Int.serialize(True) == 1 @@ -27,7 +28,7 @@ def test_serializes_output_float(): 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("one") is Undefined assert Float.serialize(False) == 0 assert Float.serialize(True) == 1 diff --git a/graphene/types/tests/test_uuid.py b/graphene/types/tests/test_uuid.py index 2280b41f..d34f1664 100644 --- a/graphene/types/tests/test_uuid.py +++ b/graphene/types/tests/test_uuid.py @@ -1,14 +1,19 @@ from ..objecttype import ObjectType from ..schema import Schema from ..uuid import UUID +from ..structures import NonNull class Query(ObjectType): uuid = UUID(input=UUID()) + required_uuid = UUID(input=NonNull(UUID), required=True) def resolve_uuid(self, info, input): return input + def resolve_required_uuid(self, info, input): + return input + schema = Schema(query=Query) @@ -29,3 +34,35 @@ def test_uuidstring_query_variable(): ) assert not result.errors assert result.data == {"uuid": uuid_value} + + +def test_uuidstring_optional_uuid_input(): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ uuid(input: null) }") + assert not result.errors + assert result.data == {"uuid": None} + + +def test_uuidstring_invalid_query(): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute("{ uuid(input: 1) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'UUID', found 1." + + result = schema.execute('{ uuid(input: "a") }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Expected value of type 'UUID', found \"a\"; badly formed hexadecimal UUID string" + ) + + result = schema.execute("{ requiredUuid(input: null) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'UUID!', found null." diff --git a/graphene/types/uuid.py b/graphene/types/uuid.py index 4714a67f..f2ba1fcb 100644 --- a/graphene/types/uuid.py +++ b/graphene/types/uuid.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from uuid import UUID as _UUID from graphql.language.ast import StringValueNode +from graphql import Undefined from .scalars import Scalar @@ -24,6 +25,7 @@ class UUID(Scalar): def parse_literal(node, _variables=None): if isinstance(node, StringValueNode): return _UUID(node.value) + return Undefined @staticmethod def parse_value(value):