Make resolvers simple again

This commit is contained in:
Syrus Akbary 2017-07-23 23:10:15 -07:00
parent 800fbdf820
commit 9769612a44
23 changed files with 93 additions and 99 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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']

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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')

View File

@ -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,

View File

@ -29,7 +29,6 @@ class CreatePost(graphene.Mutation):
result = graphene.Field(CreatePostResult)
@resolve_only_args
def mutate(self, text):
result = Success(yeah='yeah')

View File

@ -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():

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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')
}

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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