8

I know, it's a mouthful.

I am using pytest-asyncio, which gives you an async event loop for running async code in your tests.

I want to use factory-boy with an async ORM. The only problem is that factory-boy does not support async whatsoever. I want to override the _create function on a factory (which is a synchronous function), however, I need that function to run asynchronous code. It doesn't matter if it blocks, it's just tests.

Something like this:


class AsyncModelFactory(factory.alchemy.SQLAlchemyFactory):
    class Meta:
        sqlalchemy_session = async_session
        abstract = True

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        # run_until_complete only works if there isn't already an event loop running
        return asyncio.get_event_loop().run_until_complete(
            cls._async_create(model_class, *args, **kwargs)
        )

    @classmethod
    async def _async_create(cls, model_class, *args, **kwargs):
        obj = model_class(*args, **kwargs)
        session = self._meta.sqlalchemy_session
        session.add(obj)

        await session.commit()  # needs to be awaited
        return obj

The above actually works, except in the case where an async event loop is already running. Ironically, that factory works from synchronous tests, but not from asynchronous tests.

Is there a way to await _async_create from _create in an asynchronous context? All of factory-boy with factories creating sub-factories, etc, is synchronous, so this can only work if _create remains a synchronous call

1 Answer 1

3

You can't await anything in an ordinary function, of course. But there are ways that ordinary functions and async code can interact. An ordinary function can create tasks and return awaitables, for example. Look at this little program:

import asyncio
import time

async def main():
    print("Start", time.asctime())
    awaitable1 = f1()
    awaitable2 = f2()
    await asyncio.gather(awaitable1, awaitable2)
    print("Finish", time.asctime())
    
def f1():
    return asyncio.create_task(f3())

def f2():
    return asyncio.sleep(2.0)

async def f3():
    await asyncio.sleep(1.0)
    print("Task f3", time.asctime())
    
asyncio.run(main())

Result:

Start Thu Aug 12 18:55:20 2021
Task f3 Thu Aug 12 18:55:21 2021
Finish Thu Aug 12 18:55:22 2021

Here are two ordinary functions, one of which creates a task and one which creates a simple awaitable. Those objects are returned up the stack to an async function, which then awaits on them.

You asked specifically about the situation where the event loop is already running. When that's the case, except for code that's invoked by callback functions, everything that runs in your program is part of some task or other. Somewhere in the call stack will be an async function. If you figure out how to pass awaitable objects up to that level, I think you can solve your problem.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.