From 806c957d2a7ad7b7486bff2ac2a685f86b12b281 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 13 Mar 2020 20:03:40 +0000 Subject: [PATCH 01/11] Improve enum compatibility by supporting return enum as well as values and names --- graphene/types/definitions.py | 13 ++- graphene/types/schema.py | 2 +- graphene/types/tests/test_enum.py | 143 ++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) diff --git a/graphene/types/definitions.py b/graphene/types/definitions.py index 009169201..4dddaa90f 100644 --- a/graphene/types/definitions.py +++ b/graphene/types/definitions.py @@ -1,3 +1,5 @@ +from enum import Enum as PyEnum + from graphql import ( GraphQLEnumType, GraphQLInputObjectType, @@ -36,7 +38,16 @@ class GrapheneScalarType(GrapheneGraphQLType, GraphQLScalarType): class GrapheneEnumType(GrapheneGraphQLType, GraphQLEnumType): - pass + def serialize(self, value): + if not isinstance(value, PyEnum): + enum = self.graphene_type._meta.enum + try: + # Try and get enum by value + value = enum(value) + except ValueError: + # Try ang get enum by name + value = enum[value] + return super(GrapheneEnumType, self).serialize(value) class GrapheneInputObjectType(GrapheneGraphQLType, GraphQLInputObjectType): diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 29ead4a70..ce0c74398 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -172,7 +172,7 @@ def create_enum(graphene_type): deprecation_reason = graphene_type._meta.deprecation_reason(value) values[name] = GraphQLEnumValue( - value=value.value, + value=value, description=description, deprecation_reason=deprecation_reason, ) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 1b6181208..7f9fcc4ad 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -1,8 +1,11 @@ +from textwrap import dedent + from ..argument import Argument from ..enum import Enum, PyEnum from ..field import Field from ..inputfield import InputField from ..schema import ObjectType, Schema +from ..mutation import Mutation def test_enum_construction(): @@ -224,3 +227,143 @@ class Meta: "GREEN": RGB1.GREEN, "BLUE": RGB1.BLUE, } + + +def test_enum_types(): + from enum import Enum as PyEnum + + class Color(PyEnum): + RED = 1 + GREEN = 2 + BLUE = 3 + + GColor = Enum.from_enum(Color) + + class Query(ObjectType): + color = GColor(required=True) + + def resolve_color(_, info): + return Color.RED.value + + schema = Schema(query=Query) + + assert str(schema) == dedent( + '''\ + """An enumeration.""" + enum Color { + RED + GREEN + BLUE + } + + type Query { + color: Color! + } + ''' + ) + + +def test_enum_resolver(): + from enum import Enum as PyEnum + + class Color(PyEnum): + RED = 1 + GREEN = 2 + BLUE = 3 + + GColor = Enum.from_enum(Color) + + class Query(ObjectType): + color = GColor(required=True) + + def resolve_color(_, info): + return Color.RED + + schema = Schema(query=Query) + + results = schema.execute("query { color }") + assert not results.errors + + assert results.data["color"] == Color.RED.name + + +def test_enum_resolver_compat(): + from enum import Enum as PyEnum + + class Color(PyEnum): + RED = 1 + GREEN = 2 + BLUE = 3 + + GColor = Enum.from_enum(Color) + + class Query(ObjectType): + color = GColor(required=True) + color_by_name = GColor(required=True) + + def resolve_color(_, info): + return Color.RED.value + + def resolve_color_by_name(_, info): + return Color.RED.name + + schema = Schema(query=Query) + + results = schema.execute( + """query { + color + colorByName + }""" + ) + assert not results.errors + + assert results.data["color"] == Color.RED.name + assert results.data["colorByName"] == Color.RED.name + + +def test_enum_mutation(): + from enum import Enum as PyEnum + + class Color(PyEnum): + RED = 1 + GREEN = 2 + BLUE = 3 + + GColor = Enum.from_enum(Color) + + my_fav_color = None + + class Query(ObjectType): + fav_color = GColor(required=True) + + def resolve_fav_color(_, info): + return my_fav_color + + class SetFavColor(Mutation): + class Arguments: + fav_color = Argument(GColor, required=True) + + Output = Query + + def mutate(self, info, fav_color): + nonlocal my_fav_color + my_fav_color = fav_color + return Query() + + class MyMutations(ObjectType): + set_fav_color = SetFavColor.Field() + + schema = Schema(query=Query, mutation=MyMutations) + + results = schema.execute( + """mutation { + setFavColor(favColor: RED) { + favColor + } + }""" + ) + assert not results.errors + + assert my_fav_color == Color.RED + + assert results.data["setFavColor"]["favColor"] == Color.RED.name From 7d4593fcfaae27f90f90bc50098092ab3c674740 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 13 Mar 2020 20:08:39 +0000 Subject: [PATCH 02/11] Handle invalid enum values --- graphene/types/definitions.py | 7 +++++-- graphene/types/tests/test_enum.py | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/graphene/types/definitions.py b/graphene/types/definitions.py index 4dddaa90f..37fa78f56 100644 --- a/graphene/types/definitions.py +++ b/graphene/types/definitions.py @@ -45,8 +45,11 @@ def serialize(self, value): # Try and get enum by value value = enum(value) except ValueError: - # Try ang get enum by name - value = enum[value] + # Try and get enum by name + try: + value = enum[value] + except KeyError: + value = None return super(GrapheneEnumType, self).serialize(value) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 7f9fcc4ad..a4994246c 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -321,6 +321,32 @@ def resolve_color_by_name(_, info): assert results.data["colorByName"] == Color.RED.name +def test_enum_resolver_invalid(): + from enum import Enum as PyEnum + + class Color(PyEnum): + RED = 1 + GREEN = 2 + BLUE = 3 + + GColor = Enum.from_enum(Color) + + class Query(ObjectType): + color = GColor(required=True) + + def resolve_color(_, info): + return "BLACK" + + schema = Schema(query=Query) + + results = schema.execute("query { color }") + assert results.errors + assert ( + results.errors[0].message + == "Expected a value of type 'Color' but received: 'BLACK'" + ) + + def test_enum_mutation(): from enum import Enum as PyEnum From 9367ae5d86171066e6c0cea796e7c3869e662e0d Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sat, 14 Mar 2020 16:31:14 +0000 Subject: [PATCH 03/11] Rough implementation of compat middleware --- graphene/types/tests/test_enum.py | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index a4994246c..a57ed06ef 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -1,6 +1,9 @@ from textwrap import dedent +from graphql import OperationType +from graphene.utils.str_converters import to_snake_case from ..argument import Argument +from ..definitions import GrapheneEnumType from ..enum import Enum, PyEnum from ..field import Field from ..inputfield import InputField @@ -393,3 +396,75 @@ class MyMutations(ObjectType): assert my_fav_color == Color.RED assert results.data["setFavColor"]["favColor"] == Color.RED.name + + +def get_underlying_type(_type): + while hasattr(_type, "of_type"): + _type = _type.of_type + return _type + + +def test_enum_mutation_compat(): + from enum import Enum as PyEnum + + class Color(PyEnum): + RED = 1 + GREEN = 2 + BLUE = 3 + + GColor = Enum.from_enum(Color) + + my_fav_color = None + + class Query(ObjectType): + fav_color = GColor(required=True) + + def resolve_fav_color(_, info): + return my_fav_color + + class SetFavColor(Mutation): + class Arguments: + fav_color = Argument(GColor, required=True) + + Output = Query + + def mutate(self, info, fav_color): + nonlocal my_fav_color + my_fav_color = fav_color + return Query() + + class MyMutations(ObjectType): + set_fav_color = SetFavColor.Field() + + def enum_compat_middleware(next, root, info, **args): + operation = info.operation.operation + if operation == OperationType.MUTATION: + input_arguments = info.parent_type.fields[info.field_name].args + for arg_name, arg in input_arguments.items(): + _type = get_underlying_type(arg.type) + if isinstance(_type, GrapheneEnumType): + # Convert inputs to value + arg_name = to_snake_case(arg_name) + input_value = args.get(arg_name, None) + if input_value and isinstance( + input_value, _type.graphene_type._meta.enum + ): + args[arg_name] = args[arg_name].value + + return next(root, info, **args) + + schema = Schema(query=Query, mutation=MyMutations) + + results = schema.execute( + """mutation { + setFavColor(favColor: RED) { + favColor + } + }""", + middleware=[enum_compat_middleware], + ) + assert not results.errors + + assert my_fav_color == Color.RED.value + + assert results.data["setFavColor"]["favColor"] == Color.RED.name From c30d8d02316fe3776a99cb32d4e0523f755f923d Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sat, 14 Mar 2020 16:46:19 +0000 Subject: [PATCH 04/11] Move enum middleware into compat module --- graphene/compat/__init__.py | 0 graphene/compat/middleware.py | 28 ++++++++++++++++++++++++++++ graphene/types/tests/test_enum.py | 29 ++--------------------------- graphene/types/utils.py | 7 +++++++ 4 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 graphene/compat/__init__.py create mode 100644 graphene/compat/middleware.py diff --git a/graphene/compat/__init__.py b/graphene/compat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/graphene/compat/middleware.py b/graphene/compat/middleware.py new file mode 100644 index 000000000..d5b1f6825 --- /dev/null +++ b/graphene/compat/middleware.py @@ -0,0 +1,28 @@ +from graphql import OperationType + +from ..utils.str_converters import to_snake_case +from ..types.definitions import GrapheneEnumType +from ..types.utils import get_underlying_type + + +def enum_value_convertor_middleware(next, root, info, **args): + """ + Compatibility middleware for upgrading to v3: + + Convert enums to their values for mutation inputs, like the behaviour in v2 + """ + operation = info.operation.operation + if operation == OperationType.MUTATION: + input_arguments = info.parent_type.fields[info.field_name].args + for arg_name, arg in input_arguments.items(): + _type = get_underlying_type(arg.type) + if isinstance(_type, GrapheneEnumType): + # Convert inputs to value + arg_name = to_snake_case(arg_name) + input_value = args.get(arg_name, None) + if input_value and isinstance( + input_value, _type.graphene_type._meta.enum + ): + args[arg_name] = args[arg_name].value + + return next(root, info, **args) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index a57ed06ef..95e712b1b 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -1,9 +1,7 @@ from textwrap import dedent -from graphql import OperationType -from graphene.utils.str_converters import to_snake_case +from graphene.compat.middleware import enum_value_convertor_middleware from ..argument import Argument -from ..definitions import GrapheneEnumType from ..enum import Enum, PyEnum from ..field import Field from ..inputfield import InputField @@ -398,12 +396,6 @@ class MyMutations(ObjectType): assert results.data["setFavColor"]["favColor"] == Color.RED.name -def get_underlying_type(_type): - while hasattr(_type, "of_type"): - _type = _type.of_type - return _type - - def test_enum_mutation_compat(): from enum import Enum as PyEnum @@ -436,23 +428,6 @@ def mutate(self, info, fav_color): class MyMutations(ObjectType): set_fav_color = SetFavColor.Field() - def enum_compat_middleware(next, root, info, **args): - operation = info.operation.operation - if operation == OperationType.MUTATION: - input_arguments = info.parent_type.fields[info.field_name].args - for arg_name, arg in input_arguments.items(): - _type = get_underlying_type(arg.type) - if isinstance(_type, GrapheneEnumType): - # Convert inputs to value - arg_name = to_snake_case(arg_name) - input_value = args.get(arg_name, None) - if input_value and isinstance( - input_value, _type.graphene_type._meta.enum - ): - args[arg_name] = args[arg_name].value - - return next(root, info, **args) - schema = Schema(query=Query, mutation=MyMutations) results = schema.execute( @@ -461,7 +436,7 @@ def enum_compat_middleware(next, root, info, **args): favColor } }""", - middleware=[enum_compat_middleware], + middleware=[enum_value_convertor_middleware], ) assert not results.errors diff --git a/graphene/types/utils.py b/graphene/types/utils.py index 3b195d692..1976448aa 100644 --- a/graphene/types/utils.py +++ b/graphene/types/utils.py @@ -41,3 +41,10 @@ def get_type(_type): if inspect.isfunction(_type) or isinstance(_type, partial): return _type() return _type + + +def get_underlying_type(_type): + """Get the underlying type even if it is wrapped in structures like NonNull""" + while hasattr(_type, "of_type"): + _type = _type.of_type + return _type From 3327ca47c233937c822656211a34c58e9d42884d Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 25 Jun 2020 10:57:18 +0100 Subject: [PATCH 05/11] Fix tests --- graphene/types/definitions.py | 3 ++- graphene/types/tests/test_enum.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/graphene/types/definitions.py b/graphene/types/definitions.py index 37fa78f56..908cc7c86 100644 --- a/graphene/types/definitions.py +++ b/graphene/types/definitions.py @@ -7,6 +7,7 @@ GraphQLObjectType, GraphQLScalarType, GraphQLUnionType, + Undefined, ) @@ -49,7 +50,7 @@ def serialize(self, value): try: value = enum[value] except KeyError: - value = None + return Undefined return super(GrapheneEnumType, self).serialize(value) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 95e712b1b..57d3cfeec 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -234,8 +234,10 @@ def test_enum_types(): from enum import Enum as PyEnum class Color(PyEnum): + """Primary colors""" + RED = 1 - GREEN = 2 + YELLOW = 2 BLUE = 3 GColor = Enum.from_enum(Color) @@ -250,16 +252,16 @@ def resolve_color(_, info): assert str(schema) == dedent( '''\ - """An enumeration.""" + type Query { + color: Color! + } + + """Primary colors""" enum Color { RED - GREEN + YELLOW BLUE } - - type Query { - color: Color! - } ''' ) From 0dca8ef1c88b3ef8d721fd2cec023e93c5d7b41e Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 25 Jun 2020 10:59:22 +0100 Subject: [PATCH 06/11] Tweak enum examples --- docs/types/enums.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/types/enums.rst b/docs/types/enums.rst index 02cc267c6..a3215cada 100644 --- a/docs/types/enums.rst +++ b/docs/types/enums.rst @@ -61,7 +61,8 @@ you can add description etc. to your enum without changing the original: graphene.Enum.from_enum( AlreadyExistingPyEnum, - description=lambda v: return 'foo' if v == AlreadyExistingPyEnum.Foo else 'bar') + description=lambda v: return 'foo' if v == AlreadyExistingPyEnum.Foo else 'bar' + ) Notes @@ -76,6 +77,7 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu .. code:: python from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 @@ -89,6 +91,7 @@ However, in Graphene ``Enum`` you need to call get to have the same effect: .. code:: python from graphene import Enum + class Color(Enum): RED = 1 GREEN = 2 From 1ce5a97bd522475155cb401c15bc0d147d547498 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 25 Jun 2020 11:40:16 +0100 Subject: [PATCH 07/11] Add some tests for the middleware --- graphene/compat/middleware.py | 45 ++++-- graphene/types/tests/test_enum.py | 2 +- graphene/types/tests/test_mutation.py | 188 ++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 14 deletions(-) diff --git a/graphene/compat/middleware.py b/graphene/compat/middleware.py index d5b1f6825..7796e9785 100644 --- a/graphene/compat/middleware.py +++ b/graphene/compat/middleware.py @@ -1,10 +1,37 @@ from graphql import OperationType from ..utils.str_converters import to_snake_case -from ..types.definitions import GrapheneEnumType +from ..types.definitions import GrapheneEnumType, GrapheneInputObjectType from ..types.utils import get_underlying_type +def convert_enum_args(args, input_arguments): + new_args = {} + + for arg_name, arg in input_arguments.items(): + _type = get_underlying_type(arg.type) + + arg_name = to_snake_case(arg_name) + input_value = args.get(arg_name, None) + + if isinstance(_type, GrapheneEnumType): + # Convert inputs to value + if input_value and isinstance(input_value, _type.graphene_type._meta.enum): + new_args[arg_name] = input_value.value + else: + new_args[arg_name] = input_value + elif isinstance(_type, GrapheneInputObjectType): + _input_arguments = get_underlying_type(arg.type).fields + input_type = input_value.get_type() + new_args[arg_name] = input_type( + **convert_enum_args(input_value, _input_arguments) + ) + else: + new_args[arg_name] = input_value + + return new_args + + def enum_value_convertor_middleware(next, root, info, **args): """ Compatibility middleware for upgrading to v3: @@ -14,15 +41,7 @@ def enum_value_convertor_middleware(next, root, info, **args): operation = info.operation.operation if operation == OperationType.MUTATION: input_arguments = info.parent_type.fields[info.field_name].args - for arg_name, arg in input_arguments.items(): - _type = get_underlying_type(arg.type) - if isinstance(_type, GrapheneEnumType): - # Convert inputs to value - arg_name = to_snake_case(arg_name) - input_value = args.get(arg_name, None) - if input_value and isinstance( - input_value, _type.graphene_type._meta.enum - ): - args[arg_name] = args[arg_name].value - - return next(root, info, **args) + + new_args = convert_enum_args(args, input_arguments) + + return next(root, info, **new_args) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 57d3cfeec..1e8f3e51a 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -246,7 +246,7 @@ class Query(ObjectType): color = GColor(required=True) def resolve_color(_, info): - return Color.RED.value + return Color.RED schema = Schema(query=Query) diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 4a7ad3c7c..b5cc64b78 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -8,6 +8,9 @@ from ..schema import Schema from ..structures import NonNull from ..interface import Interface +from ..enum import Enum +from ..inputobjecttype import InputObjectType +from graphene.compat.middleware import enum_value_convertor_middleware class MyType(Interface): @@ -218,3 +221,188 @@ class Query(ObjectType): ) assert not result.errors assert result.data == {"createUserWithPlanet": {"name": "Peter", "planet": "earth"}} + + +def test_mutation_enum_input(): + class RGB(Enum): + """Available colors""" + + RED = 1 + GREEN = 2 + BLUE = 3 + + color_input = None + + class CreatePaint(Mutation): + class Arguments: + color = RGB(required=True) + + color = RGB(required=True) + + def mutate(_, info, color): + nonlocal color_input + color_input = color + return CreatePaint(color=color) + + class MyMutation(ObjectType): + create_paint = CreatePaint.Field() + + class Query(ObjectType): + a = String() + + schema = Schema(query=Query, mutation=MyMutation) + result = schema.execute( + """ mutation MyMutation { + createPaint(color: RED) { + color + } + } + """ + ) + assert not result.errors + assert result.data == {"createPaint": {"color": "RED"}} + + assert color_input == RGB.RED + + +def test_mutation_enum_input_type(): + class RGB(Enum): + """Available colors""" + + RED = 1 + GREEN = 2 + BLUE = 3 + + class ColorInput(InputObjectType): + color = RGB(required=True) + + color_input_value = None + + class CreatePaint(Mutation): + class Arguments: + color_input = ColorInput(required=True) + + color = RGB(required=True) + + def mutate(_, info, color_input): + nonlocal color_input_value + color_input_value = color_input.color + return CreatePaint(color=color_input.color) + + class MyMutation(ObjectType): + create_paint = CreatePaint.Field() + + class Query(ObjectType): + a = String() + + schema = Schema(query=Query, mutation=MyMutation) + result = schema.execute( + """ mutation MyMutation { + createPaint(colorInput: { color: RED }) { + color + } + } + """, + ) + assert not result.errors + assert result.data == {"createPaint": {"color": "RED"}} + + assert color_input_value == RGB.RED + + +def test_mutation_enum_input_compatability_middleware(): + """Test the `enum_value_convertor_middleware`""" + + class RGB(Enum): + """Available colors""" + + RED = 1 + GREEN = 2 + BLUE = 3 + + color_input = None + + class CreatePaint(Mutation): + class Arguments: + color = RGB(required=True) + + color = RGB(required=True) + + def mutate(_, info, color): + nonlocal color_input + color_input = color + return CreatePaint(color=color) + + class MyMutation(ObjectType): + create_paint = CreatePaint.Field() + + class Query(ObjectType): + a = String() + + schema = Schema(query=Query, mutation=MyMutation) + result = schema.execute( + """ mutation MyMutation { + createPaint(color: RED) { + color + } + } + """, + middleware=[enum_value_convertor_middleware], + ) + assert not result.errors + assert result.data == {"createPaint": {"color": "RED"}} + + assert color_input == 1 + assert type(color_input) == int + + +def test_mutation_enum_input_compatability_middleware_input_type(): + """Test the `enum_value_convertor_middleware`""" + + class RGB(Enum): + """Available colors""" + + RED = 1 + GREEN = 2 + BLUE = 3 + + class SecondColorInput(InputObjectType): + color = RGB(required=True) + + class ColorInput(InputObjectType): + color_input = SecondColorInput(required=True) + + color_input_value = None + + class CreatePaint(Mutation): + class Arguments: + color_input = ColorInput(required=True) + + color = RGB(required=True) + + def mutate(_, info, color_input): + nonlocal color_input_value + color_input_value = color_input.color_input.color + return CreatePaint(color=color_input_value) + + class MyMutation(ObjectType): + create_paint = CreatePaint.Field() + + class Query(ObjectType): + a = String() + + schema = Schema(query=Query, mutation=MyMutation) + result = schema.execute( + """ mutation MyMutation { + createPaint(colorInput: { colorInput: { color: RED } }) { + color + } + } + """, + middleware=[enum_value_convertor_middleware], + ) + assert not result.errors + assert result.data == {"createPaint": {"color": "RED"}} + + assert color_input_value == 1 + assert type(color_input_value) == int From 4ac2bd25e4a35d75deb3d8886c3464f85bd8663a Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Mon, 29 Jun 2020 16:38:09 +0100 Subject: [PATCH 08/11] Clean up tests --- graphene/types/tests/test_enum.py | 233 ++++++++++++++++++++------ graphene/types/tests/test_mutation.py | 188 --------------------- 2 files changed, 178 insertions(+), 243 deletions(-) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 1e8f3e51a..e509bdc46 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -350,98 +350,221 @@ def resolve_color(_, info): ) -def test_enum_mutation(): - from enum import Enum as PyEnum +def test_field_enum_argument(): + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + class Brick(ObjectType): + color = Color(required=True) + + color_filter = None + + class Query(ObjectType): + bricks_by_color = Field(Brick, color=Color(required=True)) + + def resolve_bricks_by_color(_, info, color): + nonlocal color_filter + color_filter = color + return Brick(color=color) + + schema = Schema(query=Query) + + results = schema.execute( + """ + query { + bricksByColor(color: RED) { + color + } + } + """ + ) + assert not results.errors + assert results.data == {"bricksByColor": {"color": "RED"}} + assert color_filter == Color.RED + + +def test_mutation_enum_input(): + class RGB(Enum): + """Available colors""" - class Color(PyEnum): RED = 1 GREEN = 2 BLUE = 3 - GColor = Enum.from_enum(Color) + color_input = None + + class CreatePaint(Mutation): + class Arguments: + color = RGB(required=True) + + color = RGB(required=True) + + def mutate(_, info, color): + nonlocal color_input + color_input = color + return CreatePaint(color=color) - my_fav_color = None + class MyMutation(ObjectType): + create_paint = CreatePaint.Field() class Query(ObjectType): - fav_color = GColor(required=True) + a = String() + + schema = Schema(query=Query, mutation=MyMutation) + result = schema.execute( + """ mutation MyMutation { + createPaint(color: RED) { + color + } + } + """ + ) + assert not result.errors + assert result.data == {"createPaint": {"color": "RED"}} - def resolve_fav_color(_, info): - return my_fav_color + assert color_input == RGB.RED - class SetFavColor(Mutation): + +def test_mutation_enum_input_type(): + class RGB(Enum): + """Available colors""" + + RED = 1 + GREEN = 2 + BLUE = 3 + + class ColorInput(InputObjectType): + color = RGB(required=True) + + color_input_value = None + + class CreatePaint(Mutation): class Arguments: - fav_color = Argument(GColor, required=True) + color_input = ColorInput(required=True) - Output = Query + color = RGB(required=True) - def mutate(self, info, fav_color): - nonlocal my_fav_color - my_fav_color = fav_color - return Query() + def mutate(_, info, color_input): + nonlocal color_input_value + color_input_value = color_input.color + return CreatePaint(color=color_input.color) - class MyMutations(ObjectType): - set_fav_color = SetFavColor.Field() + class MyMutation(ObjectType): + create_paint = CreatePaint.Field() - schema = Schema(query=Query, mutation=MyMutations) + class Query(ObjectType): + a = String() - results = schema.execute( - """mutation { - setFavColor(favColor: RED) { - favColor - } - }""" + schema = Schema(query=Query, mutation=MyMutation) + result = schema.execute( + """ mutation MyMutation { + createPaint(colorInput: { color: RED }) { + color + } + } + """, ) - assert not results.errors + assert not result.errors + assert result.data == {"createPaint": {"color": "RED"}} - assert my_fav_color == Color.RED + assert color_input_value == RGB.RED - assert results.data["setFavColor"]["favColor"] == Color.RED.name +def test_mutation_enum_input_compatability_middleware(): + """Test the `enum_value_convertor_middleware`""" -def test_enum_mutation_compat(): - from enum import Enum as PyEnum + class RGB(Enum): + """Available colors""" - class Color(PyEnum): RED = 1 GREEN = 2 BLUE = 3 - GColor = Enum.from_enum(Color) + color_input = None + + class CreatePaint(Mutation): + class Arguments: + color = RGB(required=True) + + color = RGB(required=True) + + def mutate(_, info, color): + nonlocal color_input + color_input = color + return CreatePaint(color=color) - my_fav_color = None + class MyMutation(ObjectType): + create_paint = CreatePaint.Field() class Query(ObjectType): - fav_color = GColor(required=True) + a = String() + + schema = Schema(query=Query, mutation=MyMutation) + result = schema.execute( + """ mutation MyMutation { + createPaint(color: RED) { + color + } + } + """, + middleware=[enum_value_convertor_middleware], + ) + assert not result.errors + assert result.data == {"createPaint": {"color": "RED"}} + + assert color_input == 1 + assert type(color_input) == int + + +def test_mutation_enum_input_compatability_middleware_input_type(): + """Test the `enum_value_convertor_middleware`""" + + class RGB(Enum): + """Available colors""" + + RED = 1 + GREEN = 2 + BLUE = 3 - def resolve_fav_color(_, info): - return my_fav_color + class SecondColorInput(InputObjectType): + color = RGB(required=True) - class SetFavColor(Mutation): + class ColorInput(InputObjectType): + color_input = SecondColorInput(required=True) + + color_input_value = None + + class CreatePaint(Mutation): class Arguments: - fav_color = Argument(GColor, required=True) + color_input = ColorInput(required=True) - Output = Query + color = RGB(required=True) - def mutate(self, info, fav_color): - nonlocal my_fav_color - my_fav_color = fav_color - return Query() + def mutate(_, info, color_input): + nonlocal color_input_value + color_input_value = color_input.color_input.color + return CreatePaint(color=color_input_value) - class MyMutations(ObjectType): - set_fav_color = SetFavColor.Field() + class MyMutation(ObjectType): + create_paint = CreatePaint.Field() - schema = Schema(query=Query, mutation=MyMutations) + class Query(ObjectType): + a = String() - results = schema.execute( - """mutation { - setFavColor(favColor: RED) { - favColor - } - }""", + schema = Schema(query=Query, mutation=MyMutation) + result = schema.execute( + """ mutation MyMutation { + createPaint(colorInput: { colorInput: { color: RED } }) { + color + } + } + """, middleware=[enum_value_convertor_middleware], ) - assert not results.errors - - assert my_fav_color == Color.RED.value + assert not result.errors + assert result.data == {"createPaint": {"color": "RED"}} - assert results.data["setFavColor"]["favColor"] == Color.RED.name + assert color_input_value == 1 + assert type(color_input_value) == int diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index b5cc64b78..4a7ad3c7c 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -8,9 +8,6 @@ from ..schema import Schema from ..structures import NonNull from ..interface import Interface -from ..enum import Enum -from ..inputobjecttype import InputObjectType -from graphene.compat.middleware import enum_value_convertor_middleware class MyType(Interface): @@ -221,188 +218,3 @@ class Query(ObjectType): ) assert not result.errors assert result.data == {"createUserWithPlanet": {"name": "Peter", "planet": "earth"}} - - -def test_mutation_enum_input(): - class RGB(Enum): - """Available colors""" - - RED = 1 - GREEN = 2 - BLUE = 3 - - color_input = None - - class CreatePaint(Mutation): - class Arguments: - color = RGB(required=True) - - color = RGB(required=True) - - def mutate(_, info, color): - nonlocal color_input - color_input = color - return CreatePaint(color=color) - - class MyMutation(ObjectType): - create_paint = CreatePaint.Field() - - class Query(ObjectType): - a = String() - - schema = Schema(query=Query, mutation=MyMutation) - result = schema.execute( - """ mutation MyMutation { - createPaint(color: RED) { - color - } - } - """ - ) - assert not result.errors - assert result.data == {"createPaint": {"color": "RED"}} - - assert color_input == RGB.RED - - -def test_mutation_enum_input_type(): - class RGB(Enum): - """Available colors""" - - RED = 1 - GREEN = 2 - BLUE = 3 - - class ColorInput(InputObjectType): - color = RGB(required=True) - - color_input_value = None - - class CreatePaint(Mutation): - class Arguments: - color_input = ColorInput(required=True) - - color = RGB(required=True) - - def mutate(_, info, color_input): - nonlocal color_input_value - color_input_value = color_input.color - return CreatePaint(color=color_input.color) - - class MyMutation(ObjectType): - create_paint = CreatePaint.Field() - - class Query(ObjectType): - a = String() - - schema = Schema(query=Query, mutation=MyMutation) - result = schema.execute( - """ mutation MyMutation { - createPaint(colorInput: { color: RED }) { - color - } - } - """, - ) - assert not result.errors - assert result.data == {"createPaint": {"color": "RED"}} - - assert color_input_value == RGB.RED - - -def test_mutation_enum_input_compatability_middleware(): - """Test the `enum_value_convertor_middleware`""" - - class RGB(Enum): - """Available colors""" - - RED = 1 - GREEN = 2 - BLUE = 3 - - color_input = None - - class CreatePaint(Mutation): - class Arguments: - color = RGB(required=True) - - color = RGB(required=True) - - def mutate(_, info, color): - nonlocal color_input - color_input = color - return CreatePaint(color=color) - - class MyMutation(ObjectType): - create_paint = CreatePaint.Field() - - class Query(ObjectType): - a = String() - - schema = Schema(query=Query, mutation=MyMutation) - result = schema.execute( - """ mutation MyMutation { - createPaint(color: RED) { - color - } - } - """, - middleware=[enum_value_convertor_middleware], - ) - assert not result.errors - assert result.data == {"createPaint": {"color": "RED"}} - - assert color_input == 1 - assert type(color_input) == int - - -def test_mutation_enum_input_compatability_middleware_input_type(): - """Test the `enum_value_convertor_middleware`""" - - class RGB(Enum): - """Available colors""" - - RED = 1 - GREEN = 2 - BLUE = 3 - - class SecondColorInput(InputObjectType): - color = RGB(required=True) - - class ColorInput(InputObjectType): - color_input = SecondColorInput(required=True) - - color_input_value = None - - class CreatePaint(Mutation): - class Arguments: - color_input = ColorInput(required=True) - - color = RGB(required=True) - - def mutate(_, info, color_input): - nonlocal color_input_value - color_input_value = color_input.color_input.color - return CreatePaint(color=color_input_value) - - class MyMutation(ObjectType): - create_paint = CreatePaint.Field() - - class Query(ObjectType): - a = String() - - schema = Schema(query=Query, mutation=MyMutation) - result = schema.execute( - """ mutation MyMutation { - createPaint(colorInput: { colorInput: { color: RED } }) { - color - } - } - """, - middleware=[enum_value_convertor_middleware], - ) - assert not result.errors - assert result.data == {"createPaint": {"color": "RED"}} - - assert color_input_value == 1 - assert type(color_input_value) == int From 0d083b1c6668abafdb754d4d52a97ac2e6bd00d4 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Mon, 29 Jun 2020 16:41:59 +0100 Subject: [PATCH 09/11] Add missing imports --- graphene/types/tests/test_enum.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index e509bdc46..d4bde6984 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -1,12 +1,15 @@ from textwrap import dedent from graphene.compat.middleware import enum_value_convertor_middleware + from ..argument import Argument from ..enum import Enum, PyEnum from ..field import Field from ..inputfield import InputField -from ..schema import ObjectType, Schema +from ..inputobjecttype import InputObjectType from ..mutation import Mutation +from ..scalars import String +from ..schema import ObjectType, Schema def test_enum_construction(): From 9f28d2fde48771d699ebc9299a52c8df706a6780 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 9 Jul 2020 16:17:09 +0100 Subject: [PATCH 10/11] Remove enum compat middleware --- graphene/compat/__init__.py | 0 graphene/compat/middleware.py | 47 -------------- graphene/types/tests/test_enum.py | 100 ------------------------------ 3 files changed, 147 deletions(-) delete mode 100644 graphene/compat/__init__.py delete mode 100644 graphene/compat/middleware.py diff --git a/graphene/compat/__init__.py b/graphene/compat/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/graphene/compat/middleware.py b/graphene/compat/middleware.py deleted file mode 100644 index 7796e9785..000000000 --- a/graphene/compat/middleware.py +++ /dev/null @@ -1,47 +0,0 @@ -from graphql import OperationType - -from ..utils.str_converters import to_snake_case -from ..types.definitions import GrapheneEnumType, GrapheneInputObjectType -from ..types.utils import get_underlying_type - - -def convert_enum_args(args, input_arguments): - new_args = {} - - for arg_name, arg in input_arguments.items(): - _type = get_underlying_type(arg.type) - - arg_name = to_snake_case(arg_name) - input_value = args.get(arg_name, None) - - if isinstance(_type, GrapheneEnumType): - # Convert inputs to value - if input_value and isinstance(input_value, _type.graphene_type._meta.enum): - new_args[arg_name] = input_value.value - else: - new_args[arg_name] = input_value - elif isinstance(_type, GrapheneInputObjectType): - _input_arguments = get_underlying_type(arg.type).fields - input_type = input_value.get_type() - new_args[arg_name] = input_type( - **convert_enum_args(input_value, _input_arguments) - ) - else: - new_args[arg_name] = input_value - - return new_args - - -def enum_value_convertor_middleware(next, root, info, **args): - """ - Compatibility middleware for upgrading to v3: - - Convert enums to their values for mutation inputs, like the behaviour in v2 - """ - operation = info.operation.operation - if operation == OperationType.MUTATION: - input_arguments = info.parent_type.fields[info.field_name].args - - new_args = convert_enum_args(args, input_arguments) - - return next(root, info, **new_args) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index d4bde6984..8d5e87af4 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -1,7 +1,5 @@ from textwrap import dedent -from graphene.compat.middleware import enum_value_convertor_middleware - from ..argument import Argument from ..enum import Enum, PyEnum from ..field import Field @@ -473,101 +471,3 @@ class Query(ObjectType): assert result.data == {"createPaint": {"color": "RED"}} assert color_input_value == RGB.RED - - -def test_mutation_enum_input_compatability_middleware(): - """Test the `enum_value_convertor_middleware`""" - - class RGB(Enum): - """Available colors""" - - RED = 1 - GREEN = 2 - BLUE = 3 - - color_input = None - - class CreatePaint(Mutation): - class Arguments: - color = RGB(required=True) - - color = RGB(required=True) - - def mutate(_, info, color): - nonlocal color_input - color_input = color - return CreatePaint(color=color) - - class MyMutation(ObjectType): - create_paint = CreatePaint.Field() - - class Query(ObjectType): - a = String() - - schema = Schema(query=Query, mutation=MyMutation) - result = schema.execute( - """ mutation MyMutation { - createPaint(color: RED) { - color - } - } - """, - middleware=[enum_value_convertor_middleware], - ) - assert not result.errors - assert result.data == {"createPaint": {"color": "RED"}} - - assert color_input == 1 - assert type(color_input) == int - - -def test_mutation_enum_input_compatability_middleware_input_type(): - """Test the `enum_value_convertor_middleware`""" - - class RGB(Enum): - """Available colors""" - - RED = 1 - GREEN = 2 - BLUE = 3 - - class SecondColorInput(InputObjectType): - color = RGB(required=True) - - class ColorInput(InputObjectType): - color_input = SecondColorInput(required=True) - - color_input_value = None - - class CreatePaint(Mutation): - class Arguments: - color_input = ColorInput(required=True) - - color = RGB(required=True) - - def mutate(_, info, color_input): - nonlocal color_input_value - color_input_value = color_input.color_input.color - return CreatePaint(color=color_input_value) - - class MyMutation(ObjectType): - create_paint = CreatePaint.Field() - - class Query(ObjectType): - a = String() - - schema = Schema(query=Query, mutation=MyMutation) - result = schema.execute( - """ mutation MyMutation { - createPaint(colorInput: { colorInput: { color: RED } }) { - color - } - } - """, - middleware=[enum_value_convertor_middleware], - ) - assert not result.errors - assert result.data == {"createPaint": {"color": "RED"}} - - assert color_input_value == 1 - assert type(color_input_value) == int From 1d3525e6f91a41201835d775abd8bd7212d72cb4 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 9 Jul 2020 16:36:41 +0100 Subject: [PATCH 11/11] Use custom dedent function and pin graphql-core to >3.1.2 --- graphene/relay/tests/test_node.py | 2 +- graphene/relay/tests/test_node_custom.py | 3 ++- graphene/tests/utils.py | 9 +++++++++ graphene/types/tests/test_schema.py | 4 ++-- setup.py | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 graphene/tests/utils.py diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index 92d851054..d46838acd 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -1,7 +1,7 @@ import re from graphql_relay import to_global_id -from graphql.pyutils import dedent +from graphene.tests.utils import dedent from ...types import ObjectType, Schema, String from ..node import Node, is_node diff --git a/graphene/relay/tests/test_node_custom.py b/graphene/relay/tests/test_node_custom.py index 30d62e7ba..76a2cad36 100644 --- a/graphene/relay/tests/test_node_custom.py +++ b/graphene/relay/tests/test_node_custom.py @@ -1,5 +1,6 @@ from graphql import graphql_sync -from graphql.pyutils import dedent + +from graphene.tests.utils import dedent from ...types import Interface, ObjectType, Schema from ...types.scalars import Int, String diff --git a/graphene/tests/utils.py b/graphene/tests/utils.py new file mode 100644 index 000000000..b9804d9be --- /dev/null +++ b/graphene/tests/utils.py @@ -0,0 +1,9 @@ +from textwrap import dedent as _dedent + + +def dedent(text: str) -> str: + """Fix indentation of given text by removing leading spaces and tabs. + Also removes leading newlines and trailing spaces and tabs, but keeps trailing + newlines. + """ + return _dedent(text.lstrip("\n").rstrip(" \t")) diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index 0c85e1708..fe4739c98 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -1,7 +1,7 @@ +from graphql.type import GraphQLObjectType, GraphQLSchema from pytest import raises -from graphql.type import GraphQLObjectType, GraphQLSchema -from graphql.pyutils import dedent +from graphene.tests.utils import dedent from ..field import Field from ..objecttype import ObjectType diff --git a/setup.py b/setup.py index 24bddcf90..48d7d285d 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ def run_tests(self): keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["examples*"]), install_requires=[ - "graphql-core>=3.1.1,<4", + "graphql-core>=3.1.2,<4", "graphql-relay>=3.0,<4", "aniso8601>=8,<9", ],