Pleasant Python Promises
Promises in python are far from popular, but if you are using graphql and can’t use asyncio yet, chances are you’ll need to use promises for the purpose of batching IO operations. Promises mainly appear in two areas, resolvers and dataloaders.
In this post, examples will center around graphene and django, but it may also apply in other python codebases that make use of promises.
Getting promises to work with graphene 3
If you’re using graphene 2, then you can skip this part. Graphene 3 dropped support for promises and is encouraging everyone to use asyncio. Fortunately, graphql-core has left some hooks that allow promises to still be used. A graphql-core-promise package was created to fill this purpose,
pip install graphql-core-promise
This package exposes a PromiseExecutionContext
class we can use when we call graphene
from graphql_core_promise import PromiseExecutionContext
from graphql.execution.execute import execute
execute(schema=..., document=..., execution_context_class=PromiseExecutionContext)
Now you can use promises in resolvers as you did in graphene 2.
The case for prettier promises
Consider the following graphene type
# .../types/person.py
import graphene
class Person(graphene.ObjectType):
parent = graphene.Field(Person)
@staticmethod
def resolve_parent(person,info):
return PersonLoader(info.context.dataloaders).load(person.parent_id)
graphene allows returning promises in resolvers, which is exactly what dataloaders’ .load
methods return.
What if, for some reason, we want to add a parentName
field? We would resort to chaining promises.
def resolve_parent_name(person,info):
return PersonLoader(info.context.dataloaders).load(person.parent_id).then(lambda parent: parent.name)
So far, so good. What if we can’t use a lambda because it’s more complicated? For instance, what if we wanted to add a grandparent field? We could just use nested functions:
def resolve_grandparent(person,info):
loader = PersonLoader(info.context.dataloaders)
parent_prom = loader.load(person.parent_id)
def handle_parent(parent):
return loader.load(parent.parent_id)
return parent_prom.then(handle_parent)
This is already hard to read, but just to stretch the point, let’s consider what a great-greatparent field would look like:
def resolve_greatgrandparent(person,info):
loader = PersonLoader(info.context.dataloaders)
parent_prom = loader.load(person.parent_id)
def handle_parent(parent):
grandparent_prom = loader.load(parent.parent_id)
def handle_grandparent(grandparent):
return loader.load(grandparent.parent_id)
return grandparent_prom.then(handle_grandparent)
return parent_prom.then(handle_parent)
The remedy
Fortunately, we can write this code in a more readable way using generators, the end result looks pretty close to async/await code.
from pleasant_promise import genfunc_to_prom
@genfunc_to_prom
def resolve_greatgrandparent_name(person,info):
loader = PersonLoader(info.context.dataloaders)
parent = yield loader.load(person.parent_id)
grandparent = yield loader.load(parent.parent_id)
great_grandparent = yield loader.load(grandparent.parent_id)
return great_grandparent
you can install this pleasant_promise
package with pip install pleasant-promises
Resolvers aren’t the only place you’ll deal with promises, in my previous post on dataloader composition, we created a grandparent loader. As promised, here’s a simpler implementation:
class GrandparentLoader(Dataloader):
@genfunc_to_promise
def _get_grandparent_for_single_person(self,person_id):
loader = PersonLoader(info.context.dataloaders)
parent = yield loader.load(person_id)
return loader.load(parent.parent_id)
def batch_load_fn(self,person_ids):
return Promise.all([
self._get_grandparent_for_single_person(id)
for id in person_ids
])