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):
|
||||
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
|
||||
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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# flake8: noqa
|
||||
from graphql.execution.base import ResolveInfo
|
||||
from graphql import ResolveInfo
|
||||
|
||||
from .objecttype import ObjectType
|
||||
from .interface import Interface
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -2,14 +2,16 @@ 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"
|
||||
)
|
||||
|
||||
|
||||
if not _func:
|
||||
def _func(f):
|
||||
return annotate(f, **annotations)
|
||||
|
|
|
@ -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:
|
||||
|
|
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 .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)
|
||||
|
|
|
@ -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
|
||||
|
@ -32,5 +37,10 @@ def resolver_from_annotations(func):
|
|||
else:
|
||||
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)
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue
Block a user