Skip to content

Instantly share code, notes, and snippets.

@toolness
Last active August 11, 2018 23:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save toolness/c5a718cde31d863a3381d92ab7621bb5 to your computer and use it in GitHub Desktop.
Save toolness/c5a718cde31d863a3381d92ab7621bb5 to your computer and use it in GitHub Desktop.
Simple Graphene wrapper that uses type annotations to generate GraphQL schemas.
import inspect
from typing import Type, Any, Callable
import graphene
from graphql import ResolveInfo
class AnnoQuery:
'''
Base class for defining a GraphQL query schema and resolvers that relies
on type annotations rather than more verbose (and un-type-checkable)
Graphene definitions.
'''
info: ResolveInfo
def __init__(self, info: ResolveInfo) -> None:
'''
This class is instantiated when resolving a query, so that
the method definitions don't always have to take 'info' as an
argument.
'''
self.info = info
@classmethod
def build_graphene_object_type(cls) -> Type[graphene.ObjectType]:
'''
Build a Graphene ObjectType class that represents the
query schema and its resolvers.
'''
fields = {}
resolvers = {}
for name in vars(cls):
method = getattr(cls, name)
if not name.startswith('_') and callable(method):
sig = inspect.signature(method)
field_class = cls._get_graphene_type(sig.return_annotation)
field_kwargs = {}
for param in list(sig.parameters.values())[1:]:
arg_field_class = cls._get_graphene_type(param.annotation)
field_kwargs[param.name] = arg_field_class(required=True)
desc = method.__doc__
fields[name] = field_class(
**field_kwargs,
required=True,
description=desc
)
resolver = cls._build_graphene_resolver(method, name)
resolvers[resolver.__name__] = resolver
class Query(graphene.ObjectType):
__doc__ = cls.__doc__
locals().update(fields)
locals().update(resolvers)
return Query
@classmethod
def _get_graphene_type(cls, anno: Any) -> Type[graphene.Scalar]:
if anno is int:
return graphene.Int
elif anno is str:
return graphene.String
raise ValueError(f'unknown graphene type for annotation {anno}')
@classmethod
def _build_graphene_resolver(cls, method: Callable, name: str) -> Callable:
def resolver(self, info: ResolveInfo, *args, **kwargs):
instance = cls(info)
return method(instance, *args, **kwargs)
resolver.__name__ = f'resolve_{name}'
return resolver
@toolness
Copy link
Author

toolness commented Aug 11, 2018

The default way to define a query using Graphene is a bit verbose:

class MyQuery(graphene.ObjectType):
    hello = graphene.String(
        thing=graphene.String(required=True),
        required=True,
        description='say hello!'
    )
    there = graphene.Int()

    def resolve_hello(self, info: ResolveInfo, thing: str) -> str:
        if info.context.user.is_authenticated:
            status = "logged in"
        else:
            status = "not logged in"
        return f'Hello from GraphQL! You passed in "{thing}" and are {status}'

    def resolve_there(self, info) -> int:
        return 123

In addition to being verbose, this can't be fully type-checked by a tool like mypy. For example, we could change the hello property from a graphene.String to a graphene.Int and there wouldn't be any complaints from static type checkers.

In contrast, the above class as a AnnoQuery subclass is more concise, readable, and can be type-checked by mypy:

class MyAnnoQuery(AnnoQuery):
    def hello(self, thing: str) -> str:
        'say hello!'

        if self.info.context.user.is_authenticated:
            status = "logged in"
        else:
            status = "not logged in"
        return f'Hello from GraphQL! You passed in "{thing}" and are {status}'

    def there(self) -> int:
        return 123

# This generates the same `graphene.ObjectType` subclass as the example above.
MyQuery = MyAnnoQuery.build_graphene_object_type()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment