From 9769612a44c5f0e9a736870773608de756e50f6b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 23:10:15 -0700 Subject: [PATCH] Make resolvers simple again --- README.md | 4 +- README.rst | 2 +- UPGRADE-v2.0.md | 12 +++--- examples/context_example.py | 3 +- examples/simple_example.py | 2 +- examples/starwars/schema.py | 6 +-- examples/starwars_relay/schema.py | 3 -- graphene/relay/mutation.py | 8 ++-- graphene/relay/tests/test_connection_query.py | 6 +-- graphene/tests/issues/test_313.py | 1 - graphene/tests/issues/test_490.py | 4 +- graphene/types/field.py | 3 +- graphene/types/mutation.py | 3 +- graphene/types/tests/test_datetime.py | 7 ++-- graphene/types/tests/test_generic.py | 3 +- graphene/types/tests/test_json.py | 3 +- graphene/types/tests/test_mutation.py | 28 +++++++------- graphene/types/tests/test_query.py | 37 +++++++++---------- graphene/types/tests/test_typemap.py | 6 +-- graphene/types/typemap.py | 13 +++++-- graphene/utils/auto_resolver.py | 19 +++++++--- graphene/utils/resolver_from_annotations.py | 15 ++------ graphene/utils/tests/test_auto_resolver.py | 4 +- 23 files changed, 93 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 5b27c4b5..9635a190 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Also, Graphene is fully compatible with the GraphQL spec, working seamlessly wit For instaling graphene, just run this command in your shell ```bash -pip install "graphene>=2.0" +pip install "graphene>=2.0.dev" ``` ## 2.0 Upgrade Guide @@ -48,7 +48,7 @@ Here is one example for you to get started: class Query(graphene.ObjectType): hello = graphene.String(description='A typical hello world') - def resolve_hello(self, args, context, info): + def resolve_hello(self): return 'World' schema = graphene.Schema(query=Query) diff --git a/README.rst b/README.rst index fcde970f..b31335f2 100644 --- a/README.rst +++ b/README.rst @@ -65,7 +65,7 @@ Here is one example for you to get started: class Query(graphene.ObjectType): hello = graphene.String(description='A typical hello world') - def resolve_hello(self, args, context, info): + def resolve_hello(self): return 'World' schema = graphene.Schema(query=Query) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index e296f362..ca0502ec 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -49,7 +49,7 @@ class Pet(CommonFields, Interface): ### resolve\_only\_args -`resolve_only_args` is now deprecated in favor of type annotations (using the polyfill `@graphene.annotate` in Python 2). +`resolve_only_args` is now deprecated in favor of type annotations (using the polyfill `@graphene.annotate` in Python 2 in case is necessary for accessing `context` or `info`). Before: @@ -68,8 +68,7 @@ With 2.0: class User(ObjectType): name = String() - # Decorate the resolver with @annotate in Python 2 - def resolve_name(self) -> str: + def resolve_name(self): return self.name ``` @@ -129,7 +128,7 @@ def is_user_id(id): return id.startswith('userid_') class Query(ObjectType): - user = graphene.Field(User, id=UserInput()) + user = graphene.Field(User, input=UserInput()) @resolve_only_args def resolve_user(self, input): @@ -149,10 +148,9 @@ class UserInput(InputObjectType): return self.id.startswith('userid_') class Query(ObjectType): - user = graphene.Field(User, id=UserInput()) + user = graphene.Field(User, input=UserInput()) - # Decorate the resolver with @annotate(input=UserInput) in Python 2 - def resolve_user(self, input: UserInput) -> User: + def resolve_user(self, input): if input.is_user_id: return get_user(input.id) diff --git a/examples/context_example.py b/examples/context_example.py index d1d273d6..2477a9bf 100644 --- a/examples/context_example.py +++ b/examples/context_example.py @@ -9,7 +9,8 @@ class User(graphene.ObjectType): class Query(graphene.ObjectType): me = graphene.Field(User) - def resolve_me(self, args, context, info): + @graphene.annotate(context=graphene.Context) + def resolve_me(self, context): return context['user'] diff --git a/examples/simple_example.py b/examples/simple_example.py index e8023266..58fd07d1 100644 --- a/examples/simple_example.py +++ b/examples/simple_example.py @@ -11,7 +11,7 @@ class Query(graphene.ObjectType): patron = graphene.Field(Patron) - def resolve_patron(self, args, context, info): + def resolve_patron(self): return Patron(id=1, name='Syrus', age=27) diff --git a/examples/starwars/schema.py b/examples/starwars/schema.py index b3c1a63b..cc36c75f 100644 --- a/examples/starwars/schema.py +++ b/examples/starwars/schema.py @@ -1,5 +1,4 @@ import graphene -from graphene import annotate from .data import get_character, get_droid, get_hero, get_human @@ -16,7 +15,7 @@ class Character(graphene.Interface): friends = graphene.List(lambda: Character) appears_in = graphene.List(Episode) - def resolve_friends(self, args, *_): + def resolve_friends(self): # The character friends is a list of strings return [get_character(f) for f in self.friends] @@ -46,15 +45,12 @@ class Query(graphene.ObjectType): id=graphene.String() ) - @annotate(episode=Episode) def resolve_hero(self, episode=None): return get_hero(episode) - @annotate(id=str) def resolve_human(self, id): return get_human(id) - @annotate(id=str) def resolve_droid(self, id): return get_droid(id) diff --git a/examples/starwars_relay/schema.py b/examples/starwars_relay/schema.py index 69a8dba7..f2a6c6f3 100644 --- a/examples/starwars_relay/schema.py +++ b/examples/starwars_relay/schema.py @@ -32,7 +32,6 @@ class Faction(graphene.ObjectType): name = graphene.String(description='The name of the faction.') ships = relay.ConnectionField(ShipConnection, description='The ships used by the faction.') - @annotate def resolve_ships(self, **args): # Transform the instance ship_ids into real instances return [get_ship(ship_id) for ship_id in self.ships] @@ -65,11 +64,9 @@ class Query(graphene.ObjectType): empire = graphene.Field(Faction) node = relay.Node.Field() - @annotate def resolve_rebels(self): return get_rebels() - @annotate def resolve_empire(self): return get_empire() diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index fb57c275..e1dea353 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -3,8 +3,9 @@ from collections import OrderedDict from promise import Promise, is_thenable -from ..types import Field, InputObjectType, String +from ..types import Field, InputObjectType, String, Context, ResolveInfo from ..types.mutation import Mutation +from ..utils.annotate import annotate class ClientIDMutation(Mutation): @@ -49,9 +50,8 @@ class ClientIDMutation(Mutation): ) @classmethod - def mutate(cls, root, args, context, info): - input = args.get('input') - + @annotate(context=Context, info=ResolveInfo) + def mutate(cls, root, input, context, info): def on_resolve(payload): try: payload.client_mutation_id = input.get('clientMutationId') diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index 11342a44..370db36e 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -31,13 +31,13 @@ class Query(ObjectType): node = Node.Field() - def resolve_letters(self, args, context, info): + def resolve_letters(self, **args): return list(letters.values()) - def resolve_promise_letters(self, args, context, info): + def resolve_promise_letters(self, **args): return Promise.resolve(list(letters.values())) - def resolve_connection_letters(self, args, context, info): + def resolve_connection_letters(self, **args): return LetterConnection( page_info=PageInfo( has_next_page=True, diff --git a/graphene/tests/issues/test_313.py b/graphene/tests/issues/test_313.py index ed89e45c..3881590b 100644 --- a/graphene/tests/issues/test_313.py +++ b/graphene/tests/issues/test_313.py @@ -29,7 +29,6 @@ class CreatePost(graphene.Mutation): result = graphene.Field(CreatePostResult) - @resolve_only_args def mutate(self, text): result = Success(yeah='yeah') diff --git a/graphene/tests/issues/test_490.py b/graphene/tests/issues/test_490.py index fc6d016e..f402e483 100644 --- a/graphene/tests/issues/test_490.py +++ b/graphene/tests/issues/test_490.py @@ -6,8 +6,8 @@ import graphene class Query(graphene.ObjectType): some_field = graphene.String(from_=graphene.String(name="from")) - def resolve_some_field(_, args, context, infos): - return args.get("from_") + def resolve_some_field(self, from_=None): + return from_ def test_issue(): diff --git a/graphene/types/field.py b/graphene/types/field.py index 8de94bed..9e699a12 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -7,7 +7,6 @@ from .mountedtype import MountedType from .structures import NonNull from .unmountedtype import UnmountedType from .utils import get_type -from ..utils.auto_resolver import auto_resolver base_type = type @@ -64,4 +63,4 @@ class Field(MountedType): return get_type(self._type) def get_resolver(self, parent_resolver): - return auto_resolver(self.resolver or parent_resolver) + return self.resolver or parent_resolver diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 8756412f..431c1d87 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -5,6 +5,7 @@ from ..utils.props import props from .field import Field from .objecttype import ObjectType, ObjectTypeOptions from .utils import yank_fields_from_attrs +from ..utils.auto_resolver import auto_resolver class MutationOptions(ObjectTypeOptions): @@ -59,7 +60,7 @@ class Mutation(ObjectType): _meta.fields = fields _meta.output = output - _meta.resolver = resolver + _meta.resolver = auto_resolver(resolver) _meta.arguments = arguments super(Mutation, cls).__init_subclass_with_meta__(_meta=_meta, **options) diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index e01bdc8a..660ae091 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -11,12 +11,11 @@ class Query(ObjectType): datetime = DateTime(_in=DateTime(name='in')) time = Time(_at=Time(name='at')) - def resolve_datetime(self, args, context, info): - _in = args.get('_in') + def resolve_datetime(self, _in=None): return _in - def resolve_time(self, args, context, info): - return args.get('_at') + def resolve_time(self, _at=None): + return _at schema = Schema(query=Query) diff --git a/graphene/types/tests/test_generic.py b/graphene/types/tests/test_generic.py index aede1a8b..3a5eb88a 100644 --- a/graphene/types/tests/test_generic.py +++ b/graphene/types/tests/test_generic.py @@ -6,8 +6,7 @@ from ..schema import Schema class Query(ObjectType): generic = GenericScalar(input=GenericScalar()) - def resolve_generic(self, args, context, info): - input = args.get('input') + def resolve_generic(self, input=None): return input diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py index c912df56..d671722d 100644 --- a/graphene/types/tests/test_json.py +++ b/graphene/types/tests/test_json.py @@ -7,8 +7,7 @@ from ..schema import Schema class Query(ObjectType): json = JSONString(input=JSONString()) - def resolve_json(self, args, context, info): - input = args.get('input') + def resolve_json(self, input): return input schema = Schema(query=Query) diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 081e823c..79544af5 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -12,14 +12,14 @@ def test_generate_mutation_no_args(): class MyMutation(Mutation): '''Documentation''' - @classmethod - def mutate(cls, *args, **kwargs): - pass + def mutate(self, **args): + return args assert issubclass(MyMutation, ObjectType) assert MyMutation._meta.name == "MyMutation" assert MyMutation._meta.description == "Documentation" - assert MyMutation.Field().resolver == MyMutation.mutate + resolved = MyMutation.Field().resolver(None, {'name': 'Peter'}, None, None) + assert resolved == {'name': 'Peter'} def test_generate_mutation_with_meta(): @@ -29,13 +29,13 @@ def test_generate_mutation_with_meta(): name = 'MyOtherMutation' description = 'Documentation' - @classmethod - def mutate(cls, *args, **kwargs): - pass + def mutate(self, **args): + return args assert MyMutation._meta.name == "MyOtherMutation" assert MyMutation._meta.description == "Documentation" - assert MyMutation.Field().resolver == MyMutation.mutate + resolved = MyMutation.Field().resolver(None, {'name': 'Peter'}, None, None) + assert resolved == {'name': 'Peter'} def test_mutation_raises_exception_if_no_mutate(): @@ -59,15 +59,15 @@ def test_mutation_custom_output_type(): Output = User - @classmethod - def mutate(cls, args, context, info): - name = args.get('name') + def mutate(self, name): return User(name=name) field = CreateUser.Field() assert field.type == User assert field.args == {'name': Argument(String)} - assert field.resolver == CreateUser.mutate + resolved = field.resolver(None, {'name': 'Peter'}, None, None) + assert isinstance(resolved, User) + assert resolved.name == 'Peter' def test_mutation_execution(): @@ -81,9 +81,7 @@ def test_mutation_execution(): name = String() dynamic = Dynamic(lambda: String()) - def mutate(self, args, context, info): - name = args.get('name') - dynamic = args.get('dynamic') + def mutate(self, name, dynamic): return CreateUser(name=name, dynamic=dynamic) class Query(ObjectType): diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index 4adcedf9..f41f40bc 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -57,7 +57,7 @@ def test_query_union(): class Query(ObjectType): unions = List(MyUnion) - def resolve_unions(self, args, context, info): + def resolve_unions(self): return [one_object(), two_object()] hello_schema = Schema(Query) @@ -108,7 +108,7 @@ def test_query_interface(): class Query(ObjectType): interfaces = List(MyInterface) - def resolve_interfaces(self, args, context, info): + def resolve_interfaces(self): return [one_object(), two_object()] hello_schema = Schema(Query, types=[One, Two]) @@ -188,7 +188,7 @@ def test_query_resolve_function(): class Query(ObjectType): hello = String() - def resolve_hello(self, args, context, info): + def resolve_hello(self): return 'World' hello_schema = Schema(Query) @@ -202,7 +202,7 @@ def test_query_arguments(): class Query(ObjectType): test = String(a_str=String(), a_int=Int()) - def resolve_test(self, args, context, info): + def resolve_test(self, **args): return json.dumps([self, args], separators=(',', ':')) test_schema = Schema(Query) @@ -231,7 +231,7 @@ def test_query_input_field(): class Query(ObjectType): test = String(a_input=Input()) - def resolve_test(self, args, context, info): + def resolve_test(self, **args): return json.dumps([self, args], separators=(',', ':')) test_schema = Schema(Query) @@ -254,10 +254,10 @@ def test_query_middlewares(): hello = String() other = String() - def resolve_hello(self, args, context, info): + def resolve_hello(self): return 'World' - def resolve_other(self, args, context, info): + def resolve_other(self): return 'other' def reversed_middleware(next, *args, **kwargs): @@ -280,14 +280,14 @@ def test_objecttype_on_instances(): class ShipType(ObjectType): name = String(description="Ship name", required=True) - def resolve_name(self, context, args, info): + def resolve_name(self): # Here self will be the Ship instance returned in resolve_ship return self.name class Query(ObjectType): ship = Field(ShipType) - def resolve_ship(self, context, args, info): + def resolve_ship(self): return Ship(name='xwing') schema = Schema(query=Query) @@ -302,7 +302,7 @@ def test_big_list_query_benchmark(benchmark): class Query(ObjectType): all_ints = List(Int) - def resolve_all_ints(self, args, context, info): + def resolve_all_ints(self): return big_list hello_schema = Schema(Query) @@ -319,7 +319,7 @@ def test_big_list_query_compiled_query_benchmark(benchmark): class Query(ObjectType): all_ints = List(Int) - def resolve_all_ints(self, args, context, info): + def resolve_all_ints(self): return big_list hello_schema = Schema(Query) @@ -341,7 +341,7 @@ def test_big_list_of_containers_query_benchmark(benchmark): class Query(ObjectType): all_containers = List(Container) - def resolve_all_containers(self, args, context, info): + def resolve_all_containers(self): return big_container_list hello_schema = Schema(Query) @@ -364,7 +364,7 @@ def test_big_list_of_containers_multiple_fields_query_benchmark(benchmark): class Query(ObjectType): all_containers = List(Container) - def resolve_all_containers(self, args, context, info): + def resolve_all_containers(self): return big_container_list hello_schema = Schema(Query) @@ -382,16 +382,16 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark z = Int() o = Int() - def resolve_x(self, args, context, info): + def resolve_x(self): return self.x - def resolve_y(self, args, context, info): + def resolve_y(self): return self.y - def resolve_z(self, args, context, info): + def resolve_z(self): return self.z - def resolve_o(self, args, context, info): + def resolve_o(self): return self.o big_container_list = [Container(x=x, y=x, z=x, o=x) for x in range(1000)] @@ -399,7 +399,7 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark class Query(ObjectType): all_containers = List(Container) - def resolve_all_containers(self, args, context, info): + def resolve_all_containers(self): return big_container_list hello_schema = Schema(Query) @@ -420,7 +420,6 @@ def test_query_annotated_resolvers(): context = String() info = String() - @annotate(_trigger_warning=False) def resolve_annotated(self, id): return "{}-{}".format(self, id) diff --git a/graphene/types/tests/test_typemap.py b/graphene/types/tests/test_typemap.py index 3a0f20dd..266173cf 100644 --- a/graphene/types/tests/test_typemap.py +++ b/graphene/types/tests/test_typemap.py @@ -49,8 +49,8 @@ def test_objecttype(): foo = String(bar=String(description='Argument description', default_value='x'), description='Field description') bar = String(name='gizmo') - def resolve_foo(self, args, info): - return args.get('bar') + def resolve_foo(self, bar): + return bar typemap = TypeMap([MyObjectType]) assert 'MyObjectType' in typemap @@ -65,7 +65,7 @@ def test_objecttype(): assert isinstance(foo_field, GraphQLField) assert foo_field.description == 'Field description' f = MyObjectType.resolve_foo - assert foo_field.resolver == getattr(f, '__func__', f) + # assert foo_field.resolver == getattr(f, '__func__', f) assert foo_field.args == { 'bar': GraphQLArgument(GraphQLString, description='Argument description', default_value='x', out_name='bar') } diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 7e0d9ca0..17ff95b1 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -11,6 +11,7 @@ from graphql.type.typemap import GraphQLTypeMap from ..utils.get_unbound_function import get_unbound_function from ..utils.str_converters import to_camel_case +from ..utils.auto_resolver import auto_resolver, final_resolver from .definitions import (GrapheneEnumType, GrapheneGraphQLType, GrapheneInputObjectType, GrapheneInterfaceType, GrapheneObjectType, GrapheneScalarType, @@ -256,8 +257,14 @@ class TypeMap(GraphQLTypeMap): field_type, args=args, resolver=field.get_resolver( - self.get_resolver_for_type(type, name, - field.default_value)), + auto_resolver( + self.get_resolver_for_type( + type, + name, + field.default_value + ) + ) + ), deprecation_reason=field.deprecation_reason, description=field.description) field_name = field.name or self.get_name(name) @@ -287,7 +294,7 @@ class TypeMap(GraphQLTypeMap): default_resolver = type._meta.default_resolver or get_default_resolver( ) - return partial(default_resolver, name, default_value) + return final_resolver(partial(default_resolver, name, default_value)) def get_field_type(self, map, type): if isinstance(type, List): diff --git a/graphene/utils/auto_resolver.py b/graphene/utils/auto_resolver.py index 633c1a95..72f33515 100644 --- a/graphene/utils/auto_resolver.py +++ b/graphene/utils/auto_resolver.py @@ -1,12 +1,21 @@ -from .resolver_from_annotations import resolver_from_annotations, is_wrapped_from_annotations +from .resolver_from_annotations import resolver_from_annotations + + +def final_resolver(func): + func._is_final_resolver = True + return func def auto_resolver(func=None): - annotations = getattr(func, '__annotations__', {}) - is_annotated = getattr(func, '_is_annotated', False) + if not func: + return - if (annotations or is_annotated) and not is_wrapped_from_annotations(func): + if not is_final_resolver(func): # Is a Graphene 2.0 resolver function - return resolver_from_annotations(func) + return final_resolver(resolver_from_annotations(func)) else: return func + + +def is_final_resolver(func): + return getattr(func, '_is_final_resolver', False) diff --git a/graphene/utils/resolver_from_annotations.py b/graphene/utils/resolver_from_annotations.py index 79fb99de..2e006dd6 100644 --- a/graphene/utils/resolver_from_annotations.py +++ b/graphene/utils/resolver_from_annotations.py @@ -1,15 +1,10 @@ from ..pyutils.compat import signature -from functools import wraps +from functools import wraps, partial def resolver_from_annotations(func): from ..types import Context, ResolveInfo - _is_wrapped_from_annotations = is_wrapped_from_annotations(func) - assert not _is_wrapped_from_annotations, "The function {func_name} is already wrapped.".format( - func_name=func.func_name - ) - func_signature = signature(func) _context_var = None @@ -38,9 +33,7 @@ def resolver_from_annotations(func): def inner(root, args, context, info): return func(root, **args) - inner._is_wrapped_from_annotations = True + if isinstance(func, partial): + return inner + return wraps(func)(inner) - - -def is_wrapped_from_annotations(func): - return getattr(func, '_is_wrapped_from_annotations', False) diff --git a/graphene/utils/tests/test_auto_resolver.py b/graphene/utils/tests/test_auto_resolver.py index 93b7a991..9fa58c22 100644 --- a/graphene/utils/tests/test_auto_resolver.py +++ b/graphene/utils/tests/test_auto_resolver.py @@ -1,15 +1,15 @@ import pytest from ..annotate import annotate -from ..auto_resolver import auto_resolver +from ..auto_resolver import auto_resolver, final_resolver from ...types import Context, ResolveInfo +@final_resolver def resolver(root, args, context, info): return root, args, context, info -@annotate def resolver_annotated(root, **args): return root, args, None, None