mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-22 17:46:57 +03:00
Improved automatic resolver args from annotations
This commit is contained in:
parent
fb4b4df500
commit
6ae9717415
|
@ -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)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
80
graphene/utils/deprecated.py
Normal file
80
graphene/utils/deprecated.py
Normal 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)))
|
|
@ -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
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user