2

Here's a sample code.

class Foo:
    def __init__(self):
        self._run_coro()

    def _run_coro(self):
        async def init():
            bar = #some I/O op
            self.bar = bar
        loop = asyncio.get_event_loop()
        loop.run_until_complete(init())

    async def spam(self):
        return await #I/O op

async def main():
    foo = Foo()
    await foo.spam()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

When I run this code, I get following exception: RuntimeError: This event loop is already running

If I initialize Foo outside main, the code runs without any exception. I want to initialize Foo such that during initialization it runs a coroutine which creates a class attribute bar.

I am unable to figure how to do it correctly. How can I run a coroutine from __init__.

Any help would be highly appreciated.

class Foo:
     def __init__(self):
         self.session = requests.Session()
         self.async_session = None
         #I guess this can be done to initialize it. 
         s = self.init_async_session()
         try:
             s.send(None)
         except StopIteration:
             pass
         finally:
             s.close()

     async def init_async_session(self):
         #ClientSession should be created inside a coroutine. 
         self.async_session = aiohttp.ClientSession()

What would be the right way to initialize self.async_session

user6037143
  • 516
  • 5
  • 20

2 Answers2

3

If some method uses something asynchronous it should be explicitly defined as asynchronous either. This is a core idea behind asyncio: make you write code a way you always know if some arbitrary method may do something asynchronous.

In your snippet you want to do async thing (bar I/O) inside sync method __init__ and asyncio prohibits it. You should make _run_coro async and initialize Foo asynchronously, for example, using __await__ method:

import asyncio


class Foo:
    def __await__(self):
        return self._run_coro().__await__()

    async def _run_coro(self):  # real async initializer
        async def init():
            await asyncio.sleep(1)  # bar I/O
            self.bar = 123
        await init()
        return self

    async def spam(self):
        return await asyncio.sleep(1)  # I/O op


async def main():
    foo = await Foo()
    await foo.spam()


asyncio.run(main())  # instead of two lines in Python 3.7+

You may be interested in reading this answer to understand better how asyncio works and how to handle it.

Upd:

s = self.init_async_session()
try:
    s.send(None)

Don't do such things: generator's method are only details of implementation in regard of coroutines. You can predict how coroutine will react on calling .send() method and you can rely on this behavior.

If you want to execute coroutine use await, if you want to start it "in background" use task or other functions from asyncio doc.

What would be the right way to initialize self.async_session

When it comes to aiohttp.ClientSession it should not only be created, but properly closed also. Best way to do it is to use async context manager as shown in aiohttp doc.

If you want to hide this operation inside Foo you can make it async manager either. Complete example:

import aiohttp


class Foo:
    async def __aenter__(self):
        self._session = aiohttp.ClientSession()
        await self._session.__aenter__()
        return self

    async def __aexit__(self, *args):
        await self._session.__aexit__(*args)

    async def spam(self):
        url = 'http://httpbin.org/delay/1'
        resp = await self._session.get(url)
        text = await resp.text()
        print(text)


async def main():
    async with Foo() as foo:
        await foo.spam()


asyncio.run(main())

Upd2:

You can combine ways to init/close object from above to achive result you like. As long as you keep in mind both operations are asynchronous and thus should be awaited, everything should be fine.

One more possible way:

import asyncio
import aiohttp


class Foo:
    def __await__(self):
        return self._init().__await__()

    async def _init(self):
        self._session = aiohttp.ClientSession()
        await self._session.__aenter__()
        return self

    async def close(self):
        await self._session.__aexit__(None, None, None)

    async def spam(self):
        url = 'http://httpbin.org/delay/1'
        resp = await self._session.get(url)
        text = await resp.text()
        print(text)


async def main():
    foo = await Foo()
    try:
        await foo.spam()
    finally:
        await foo.close()


asyncio.run(main())
Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159
  • Thanks Michael! The `Foo` class contains synchronous and asynchronous methods both. I must have an `__init__` method to set instance variables and one of those instance variables will be initialized by executing the coroutine. – user6037143 Mar 28 '19 at 14:02
  • I have updated my original question with another example. – user6037143 Mar 28 '19 at 14:14
  • The reason I want to create a `self.async_session` attribute is I want to persist it `ClienSession`. I understand `ClientSession` must be closed and I have a cleanup method to do that. Moreover, `Foo` is like a base class that provides synchronous (`resquests.Session`) as well as asynchronous (`aiohtto.ClientSession`) methods to its caller. – user6037143 Mar 28 '19 at 15:07
  • @user6037143 I updated answer. You can add wrappers to `Foo` to add sync interface running each async method inside event loop. You can see the example [here](https://stackoverflow.com/a/45215779/1113207). – Mikhail Gerasimov Mar 28 '19 at 15:37
  • I have come up with a solution and posted it as an answer. – user6037143 Apr 08 '19 at 22:16
0

Here's my solution.

class Session:
    def __init__(self, headers):
        self._headers = headers
        self._session = requests.Session()
        self._async_session = None

    async def _init(self):
        self._session = aiohttp.ClientSession(headers=headers)

    async def async_request(self, url):
       while True:
            try:
                async with self._async_session.get(url) as resp:
                    resp.raise_for_status()
                    return await resp.text()
            except aiohttp.client_exceptions.ClientError:
                 #retry or raise
            except AttributeError:
                if isinstance(self._async_session, aiohttp.ClientSession):
                    raise
                await self._init()

    def request(self, url):
        return self._session.get(url).text

    async def close(self):
        if isinstance(self._async_session, aiohttp.ClientSession):
            await self._session.close()

async def main():
    session = Session({})
    print(await session.async_request('https://httpstat.us/200')
    await session.close()

asyncio.run(main())

I can initialize the Session class and make synchronous as well as asynchronous requests. I do not have to explicitly call await session._init() to initialize self._async_session as when session._async_request is called and self._async_session is None, then await session._init() will be called and the request will be retried.

user6037143
  • 516
  • 5
  • 20