miniasync

miniasync is a small library build on top of asyncio to faciliate running small portions of asynchronous code in an otherwise synchronous application.

A typical use case is an application which is build as a synchronous application, but at some point needs to make an http request to two different web services and await the results before continuing. In synchronous Python you’d have to do each request in turn - miniasync makes it easier to run both requests in parallel without having to write asyncio boilerplate code.

Please note:

  • While miniasync helps get rid of boilerplate code, you still need to know and understand how asyncio works;
  • miniasync uses the async/await syntax introduced introduced in Python 3.5, and so only works from Python 3.5 up.

Getting started

The simplest way to use miniasync is to use the miniasync.run function. This function takes any number of coroutine objects (ie. the values returned by invoking a coroutine function), runs the associated tasks concurently with asyncio and returns the list of values returned by each of the coroutine, in the same order. miniasync.run only returns when all tasks are completed, cancelled or an exception has been raised (See Handling Exceptions).

An invocation of miniasync.run would look like this:

import aiofiles
import miniasync

async def get_file_content(filename):
    async with aiofiles.open(filename, mode='r') as f:
        return await f.read()

results = miniasync.run(
    get_file_content('file1.txt'),
    get_file_content('file2.txt'),
)

assert results == [
    '<the content of file1.txt>',
    '<the content of file2.txt>'
]

Handling exceptions

By default miniasync.run will raise an exception if any of the tasks raises an exception (and it then cancels the tasks that haven’t finished). Sometimes if one task raises an expected exception, you will still want the other tasks to complete and return their results. You can do this by specifying you want some exceptions returned, rather than raised. In such case, should one of the specified exception be raised, miniasync.run will return the exception instead of a result:

import miniasync
from aiohttp.client import ClientError

def notify_people(notification):
    results = miniasync.run(
         post_on_social_network_lambda(notification),
         post_on_social_network_delta(notification),
        return_exceptions=(ClientError,)
    )
    # Now entries in results may contain a result,
    # OR an instance of ClientError (or one of it's
    # descendant classes).
    # Other exceptions are raised normally.If this
    # happens, all other coroutines are cancelled

As noted above if there is an unhandled exceptions raised in one of the coroutines, the other coroutines are cancelled. With asyncio, cancelations happen by raising a CancelledError Exception. To account for this, you must make sure to enclose any await statement in a try/except or try/finally block.

Using the asyncio event loop

Whenever you invoke miniasync.run, it creates it’s own local asyncio event loop. This provides some isolation as a single invocation of miniasync.run will not run other async tasks defined outside of the invocation (though a library invoked by one of the coroutines could add tasks on the event loop).

Sometimes however you want to access the event loop before running miniasync.run - in particular because many async libraries (for example asyncio Queues) are linked to the asyncio event loop that exists at the point of invocation.

To do this miniasync provides a context manager, miniasync.loop. Instead of running miniasync.run you would run:

import miniasync

with miniasync.loop() as loop:
    loop.run(coroutine1(), coroutine2())

Note that we are not invoking miniasync.run (this would create yet another loop), but loop.run. You can now create objects that rely on the current loop within the block, for example:

import asyncio
import miniasync

async def coro1(q):
    q.put_nowait('world')
    return 'hello'

async def coro2(q):
    return await q.get()

with miniasync.loop() as loop:
    q = asyncio.Queue()
    results = loop.run(
        coro1(q),
        coro2(q)
    )

assert results == ['hello', 'world']

Some libraries are best used with some preparation that can only be done in an async function (for example sharing a single aiohttp Session object amongst coroutines). In that case you can nest calls to miniasync.run:

import aiohttp
import miniasync

async def post_on_social_network_lambda(session, notification):
    async with session.post('http://social_network.lambda', data=notification) as reply:
        return await reply.text()

async def post_on_social_network_delta(session, notification):
    async with session.post('http://social_network.delta', data=notification) as reply:
        return await reply.text()

async def post_on_social_networks(notification):
    with miniasync.loop() as loop:
        async with aiohttp.ClientSession() as session:
            results = loop.run(
                post_on_social_network_lambda(session, notification),
                post_on_social_network_delta(session, notification)
            )

    return "Social network lambda said %s, and social network delta said %s" % (
        results[0], results[1]
    )

def notify_people(notification):
    results = miniasync.run(post_on_social_networks(notification))
    return results[0]

API

miniasync.run(*coroutines, return_exceptions=None)

Runs a list of coroutines asynchronously

This is a shorthand for:

with miniasync.loop() as loop:
loop.run(coroutines, return_exceptions)
Parameters:
  • *coroutines – A list of coroutine objects to run
  • return_exceptions – A list of exceptions that should be returned rather than raised
Returns:

A list containing the values returned by each of the coroutines, in the same order as the coroutines were provided.

If any of the coroutines raised an exception listed in return_exceptions, then the returned value for that coroutine will be replaced by the raised exception

miniasync.loop()

This context manager creates a local event loop

The loop is set as the current event loop in the execution context. It is closed upon exiting the context manager, and the previous loop is restored as the current event loop.

The context manager yields a MiniAsync object that can be used to run coroutines

Yields:MiniAsync object
class miniasync.MiniAsync(asyncio_loop)

MiniAsync objects are used to run a set of co-routines within a loop.

MiniAsync objects should not be created directly, but obtained using the miniasync.loop context manager.

Parameters:asyncio_loop – The asyncio loop object to use to run the co-routines
run(*coroutines, return_exceptions=None)

Runs a list of coroutines asynchronously

Parameters:
  • *coroutines – A list of coroutine objects to run
  • return_exceptions – A list of exceptions that should be returned rather than raised
Returns:

A list containing the values returned by each of the coroutines, in the same order as the coroutines were provided.

If any of the coroutines raised an exception listed in return_exceptions, then the returned value for that coroutine will be replaced by the raised exception

License

Copyright © 2018, Alice Heaton. Released under the LGPL 3 License