Last active
August 11, 2018 23:13
-
-
Save toolness/c5a718cde31d863a3381d92ab7621bb5 to your computer and use it in GitHub Desktop.
Simple Graphene wrapper that uses type annotations to generate GraphQL schemas.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The default way to define a query using Graphene is a bit verbose:
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 agraphene.String
to agraphene.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: