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): class Query(ObjectType):
user_connection = relay.ConnectionField(UserConnection) 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 import graphene
from graphene import resolve_only_args from graphene import annotate
from .data import get_character, get_droid, get_hero, get_human from .data import get_character, get_droid, get_hero, get_human
@ -46,15 +46,15 @@ class Query(graphene.ObjectType):
id=graphene.String() id=graphene.String()
) )
@resolve_only_args @annotate(episode=Episode)
def resolve_hero(self, episode=None): def resolve_hero(self, episode=None):
return get_hero(episode) return get_hero(episode)
@resolve_only_args @annotate(id=str)
def resolve_human(self, id): def resolve_human(self, id):
return get_human(id) return get_human(id)
@resolve_only_args @annotate(id=str)
def resolve_droid(self, id): def resolve_droid(self, id):
return get_droid(id) return get_droid(id)

View File

@ -1,5 +1,5 @@
import graphene 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 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.') name = graphene.String(description='The name of the faction.')
ships = relay.ConnectionField(ShipConnection, description='The ships used by the faction.') ships = relay.ConnectionField(ShipConnection, description='The ships used by the faction.')
@resolve_only_args @annotate
def resolve_ships(self, **args): def resolve_ships(self, **args):
# Transform the instance ship_ids into real instances # Transform the instance ship_ids into real instances
return [get_ship(ship_id) for ship_id in self.ships] return [get_ship(ship_id) for ship_id in self.ships]
@ -65,11 +65,11 @@ class Query(graphene.ObjectType):
empire = graphene.Field(Faction) empire = graphene.Field(Faction)
node = relay.Node.Field() node = relay.Node.Field()
@resolve_only_args @annotate
def resolve_rebels(self): def resolve_rebels(self):
return get_rebels() return get_rebels()
@resolve_only_args @annotate
def resolve_empire(self): def resolve_empire(self):
return get_empire() return get_empire()

View File

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

View File

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

View File

@ -7,6 +7,7 @@ from .mountedtype import MountedType
from .structures import NonNull from .structures import NonNull
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
from .utils import get_type from .utils import get_type
from ..utils.auto_resolver import auto_resolver
base_type = type base_type = type
@ -63,4 +64,4 @@ class Field(MountedType):
return get_type(self._type) return get_type(self._type)
def get_resolver(self, parent_resolver): 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 import json
from functools import partial from functools import partial
from graphql import GraphQLError, Source, execute, parse from graphql import GraphQLError, Source, execute, parse, ResolveInfo
from ..dynamic import Dynamic from ..dynamic import Dynamic
from ..field import Field from ..field import Field
@ -13,6 +13,8 @@ from ..scalars import Int, String
from ..schema import Schema from ..schema import Schema
from ..structures import List from ..structures import List
from ..union import Union from ..union import Union
from ..context import Context
from ...utils.annotate import annotate
def test_query(): def test_query():
@ -406,3 +408,42 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark
result = benchmark(big_list_query) result = benchmark(big_list_query)
assert not result.errors 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]} 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 functools import wraps
from ..pyutils.compat import signature from ..pyutils.compat import signature
from .deprecated import warn_deprecation
def annotate(_func=None, _trigger_warning=True, **annotations): def annotate(_func=None, _trigger_warning=True, **annotations):
if not six.PY2 and _trigger_warning: 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" "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" "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): def auto_resolver(func=None):
annotations = getattr(func, '__annotations__', {}) annotations = getattr(func, '__annotations__', {})
is_annotated = getattr(func, '_is_annotated', False) 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 # Is a Graphene 2.0 resolver function
return resolver_from_annotations(func) return resolver_from_annotations(func)
else: 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 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): def resolve_only_args(func):
@wraps(func) return annotate(func)
def inner(root, args, context, info):
return func(root, **args)
return inner

View File

@ -1,10 +1,15 @@
from ..pyutils.compat import signature from ..pyutils.compat import signature
from functools import wraps from functools import wraps
from ..types import Context, ResolveInfo
def resolver_from_annotations(func): 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) func_signature = signature(func)
_context_var = None _context_var = None
@ -33,4 +38,9 @@ def resolver_from_annotations(func):
def inner(root, args, context, info): def inner(root, args, context, info):
return func(root, **args) return func(root, **args)
inner._is_wrapped_from_annotations = True
return wraps(func)(inner) 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 ..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): def resolver(*args, **kwargs):
return kwargs return kwargs
my_data = {'one': 1, 'two': 2} my_data = {'one': 1, 'two': 2}
wrapped = resolve_only_args(resolver) 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>=2.7.2',
'pytest-benchmark', 'pytest-benchmark',
'pytest-cov', 'pytest-cov',
'pytest-mock',
'snapshottest', 'snapshottest',
'coveralls', 'coveralls',
'six', 'six',