Improved automatic resolver args from annotations

This commit is contained in:
Syrus Akbary 2017-07-23 18:57:17 -07:00
parent fb4b4df500
commit 6ae9717415
14 changed files with 235 additions and 25 deletions

View File

@ -80,3 +80,64 @@
class Query(ObjectType):
user_connection = relay.ConnectionField(UserConnection)
```
## New Features
### InputObjectType
`InputObjectType`s are now a first class citizen in Graphene.
That means, if you are using a custom InputObjectType, you can access
it's fields via `getattr` (`my_input.myattr`) when resolving, instead of
the classic way `my_input['myattr']`.
And also use custom defined properties on your input class.
Example. Before:
```python
class User(ObjectType):
name = String()
class UserInput(InputObjectType):
id = ID()
def is_user_id(id):
return id.startswith('userid_')
class Query(ObjectType):
user = graphene.Field(User, id=UserInput())
@resolve_only_args
def resolve_user(self, input):
user_id = input.get('id')
if is_user_id(user_id):
return get_user(user_id)
```
With 2.0:
```python
class User(ObjectType):
id = ID()
class UserInput(InputObjectType):
id = ID()
@property
def is_user_id(self):
return id.startswith('userid_')
class Query(ObjectType):
user = graphene.Field(User, id=UserInput())
@annotate(input=UserInput)
def resolve_user(self, input):
if input.is_user_id:
return get_user(input.id)
# You can also do in Python 3:
def resolve_user(self, input: UserInput):
if input.is_user_id:
return get_user(input.id)
```

View File

@ -1,5 +1,5 @@
import graphene
from graphene import resolve_only_args
from graphene import annotate
from .data import get_character, get_droid, get_hero, get_human
@ -46,15 +46,15 @@ class Query(graphene.ObjectType):
id=graphene.String()
)
@resolve_only_args
@annotate(episode=Episode)
def resolve_hero(self, episode=None):
return get_hero(episode)
@resolve_only_args
@annotate(id=str)
def resolve_human(self, id):
return get_human(id)
@resolve_only_args
@annotate(id=str)
def resolve_droid(self, id):
return get_droid(id)

View File

@ -1,5 +1,5 @@
import graphene
from graphene import relay, resolve_only_args
from graphene import annotate, relay, annotate
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship
@ -32,7 +32,7 @@ class Faction(graphene.ObjectType):
name = graphene.String(description='The name of the faction.')
ships = relay.ConnectionField(ShipConnection, description='The ships used by the faction.')
@resolve_only_args
@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 +65,11 @@ class Query(graphene.ObjectType):
empire = graphene.Field(Faction)
node = relay.Node.Field()
@resolve_only_args
@annotate
def resolve_rebels(self):
return get_rebels()
@resolve_only_args
@annotate
def resolve_empire(self):
return get_empire()

View File

@ -46,6 +46,7 @@ if not __SETUP__:
)
from .utils.resolve_only_args import resolve_only_args
from .utils.module_loading import lazy_import
from .utils.annotate import annotate
__all__ = [
'ObjectType',
@ -76,6 +77,7 @@ if not __SETUP__:
'ConnectionField',
'PageInfo',
'lazy_import',
'annotate',
'Context',
'ResolveInfo',

View File

@ -1,5 +1,5 @@
# flake8: noqa
from graphql.execution.base import ResolveInfo
from graphql import ResolveInfo
from .objecttype import ObjectType
from .interface import Interface

View File

@ -7,6 +7,7 @@ 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
@ -63,4 +64,4 @@ class Field(MountedType):
return get_type(self._type)
def get_resolver(self, parent_resolver):
return self.resolver or parent_resolver
return auto_resolver(self.resolver or parent_resolver)

View File

@ -1,7 +1,7 @@
import json
from functools import partial
from graphql import GraphQLError, Source, execute, parse
from graphql import GraphQLError, Source, execute, parse, ResolveInfo
from ..dynamic import Dynamic
from ..field import Field
@ -13,6 +13,8 @@ from ..scalars import Int, String
from ..schema import Schema
from ..structures import List
from ..union import Union
from ..context import Context
from ...utils.annotate import annotate
def test_query():
@ -406,3 +408,42 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark
result = benchmark(big_list_query)
assert not result.errors
assert result.data == {'allContainers': [{'x': c.x, 'y': c.y, 'z': c.z, 'o': c.o} for c in big_container_list]}
def test_query_annotated_resolvers():
import json
context = Context(key="context")
class Query(ObjectType):
annotated = String(id=String())
context = String()
info = String()
@annotate
def resolve_annotated(self, id):
return "{}-{}".format(self, id)
@annotate(context=Context)
def resolve_context(self, context):
assert isinstance(context, Context)
return "{}-{}".format(self, context.key)
@annotate(info=ResolveInfo)
def resolve_info(self, info):
assert isinstance(info, ResolveInfo)
return "{}-{}".format(self, info.field_name)
test_schema = Schema(Query)
result = test_schema.execute('{ annotated(id:"self") }', "base")
assert not result.errors
assert result.data == {'annotated': 'base-self'}
result = test_schema.execute('{ context }', "base", context_value=context)
assert not result.errors
assert result.data == {'context': 'base-context'}
result = test_schema.execute('{ info }', "base")
assert not result.errors
assert result.data == {'info': 'base-info'}

View File

@ -2,10 +2,12 @@ import six
from functools import wraps
from ..pyutils.compat import signature
from .deprecated import warn_deprecation
def annotate(_func=None, _trigger_warning=True, **annotations):
if not six.PY2 and _trigger_warning:
print(
warn_deprecation(
"annotate is intended for use in Python 2 only, as you can use type annotations Python 3.\n"
"Read more in https://docs.python.org/3/library/typing.html"
)

View File

@ -1,11 +1,11 @@
from .resolver_from_annotations import resolver_from_annotations
from .resolver_from_annotations import resolver_from_annotations, is_wrapped_from_annotations
def auto_resolver(func=None):
annotations = getattr(func, '__annotations__', {})
is_annotated = getattr(func, '_is_annotated', False)
if annotations or is_annotated:
if (annotations or is_annotated) and not is_wrapped_from_annotations(func):
# Is a Graphene 2.0 resolver function
return resolver_from_annotations(func)
else:

View File

@ -0,0 +1,80 @@
import functools
import inspect
import warnings
string_types = (type(b''), type(u''))
def warn_deprecation(text):
warnings.simplefilter('always', DeprecationWarning)
warnings.warn(
text,
category=DeprecationWarning,
stacklevel=2
)
warnings.simplefilter('default', DeprecationWarning)
def deprecated(reason):
"""
This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted
when the function is used.
"""
if isinstance(reason, string_types):
# The @deprecated is used with a 'reason'.
#
# .. code-block:: python
#
# @deprecated("please, use another function")
# def old_function(x, y):
# pass
def decorator(func1):
if inspect.isclass(func1):
fmt1 = "Call to deprecated class {name} ({reason})."
else:
fmt1 = "Call to deprecated function {name} ({reason})."
@functools.wraps(func1)
def new_func1(*args, **kwargs):
warn_deprecation(
fmt1.format(name=func1.__name__, reason=reason),
)
return func1(*args, **kwargs)
return new_func1
return decorator
elif inspect.isclass(reason) or inspect.isfunction(reason):
# The @deprecated is used without any 'reason'.
#
# .. code-block:: python
#
# @deprecated
# def old_function(x, y):
# pass
func2 = reason
if inspect.isclass(func2):
fmt2 = "Call to deprecated class {name}."
else:
fmt2 = "Call to deprecated function {name}."
@functools.wraps(func2)
def new_func2(*args, **kwargs):
warn_deprecation(
fmt2.format(name=func2.__name__),
)
return func2(*args, **kwargs)
return new_func2
else:
raise TypeError(repr(type(reason)))

View File

@ -1,8 +1,19 @@
from six import PY2
from functools import wraps
from .annotate import annotate
from .deprecated import deprecated
if PY2:
deprecation_reason = (
'The decorator @resolve_only_args is deprecated.\n'
'Please use @annotate instead.'
)
else:
deprecation_reason = (
'The decorator @resolve_only_args is deprecated.\n'
'Please use Python 3 type annotations instead. Read more: https://docs.python.org/3/library/typing.html'
)
@deprecated(deprecation_reason)
def resolve_only_args(func):
@wraps(func)
def inner(root, args, context, info):
return func(root, **args)
return inner
return annotate(func)

View File

@ -1,10 +1,15 @@
from ..pyutils.compat import signature
from functools import wraps
from ..types import Context, ResolveInfo
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
@ -33,4 +38,9 @@ def resolver_from_annotations(func):
def inner(root, args, context, info):
return func(root, **args)
inner._is_wrapped_from_annotations = True
return wraps(func)(inner)
def is_wrapped_from_annotations(func):
return getattr(func, '_is_wrapped_from_annotations', False)

View File

@ -1,12 +1,13 @@
from ..resolve_only_args import resolve_only_args
from .. import deprecated
def test_resolve_only_args():
def test_resolve_only_args(mocker):
mocker.patch.object(deprecated, 'warn_deprecation')
def resolver(*args, **kwargs):
return kwargs
my_data = {'one': 1, 'two': 2}
wrapped = resolve_only_args(resolver)
assert wrapped(None, my_data, None, None) == my_data
deprecated.warn_deprecation.assert_called_once()

View File

@ -41,6 +41,7 @@ tests_require = [
'pytest>=2.7.2',
'pytest-benchmark',
'pytest-cov',
'pytest-mock',
'snapshottest',
'coveralls',
'six',