asyncio-redis icon indicating copy to clipboard operation
asyncio-redis copied to clipboard

Async Class Constructors

Open RGBCube opened this issue 3 years ago • 4 comments

Ive noticed that this library uses classmethods to mimic an async init magic method, but by using the new magic method in a clever way we can do it directly in the constructor.

Also, this makes it so you can't have a un-initialized class by initializing the class without using the async classmethod.

Here is an example:

class AsyncNeededToInit:
    def __new__(cls, foo):
        async def init():
            self = super(cls, cls).__new__(cls)
            self.foo = foo
            self.bar = await async_func()

            return self

        return init()

Which can be used by:

baz = await AsyncNeededToInit(myfoo)

Could this be added? I could make a PR too.

RGBCube avatar Jun 22 '22 18:06 RGBCube

My main concern here is that mypy doesn't allow this behavior, and I'd very much like everything to be rewritten to use type annotations in a mypy-compliant way.

jonathanslenders avatar Jun 23 '22 11:06 jonathanslenders

Instead of doing the whole init in __new__, we can just run the async functions in __new__ and if they return anything pass them to __init__, which is fully type compliant:

class AsyncNeededToInit:
    def __new__(cls):
        async def init():
            foo = await async_func()
            
            self = super(cls, cls).__new__(cls)
            self.__init__(foo)

            return self

        return init()
        
    def __init__(self, foo):
        self.foo = foo

RGBCube avatar Jun 23 '22 20:06 RGBCube

Could you add type annotations to that example and share the mypy output?

If I try it, it doesn't look compliant.

/tmp/test.py:4: error: Incompatible return type for "__new__" (returns "Awaitable[None]", but must return a subtype of "AsyncNeededToInit")

jonathanslenders avatar Jun 23 '22 20:06 jonathanslenders

We can get the class type-checked by adding annotations to the class and type: ignoring the __new__ since mypy expects it to return the instance of the class. Also ive found the simplest way to do this by using classmethods and making __new__ use them:

from __future__ import annotations

import asyncio
from typing import Awaitable

async def async_func() -> int:
    return 123


class AsyncNeededToInit:
    foo: int
    def __new__(cls) -> Awaitable[AsyncNeededToInit]:  # type: ignore
        return cls.__async_init()

    @classmethod
    async def __async_init(cls) -> AsyncNeededToInit:
        self = super(cls, cls).__new__(cls)  # this must be this way or it will start an infinite loop
        
        self.foo = await async_func()
        
        return self

async def main() -> None:
    myclass = await AsyncNeededToInit()

    print(myclass.foo)

asyncio.run(main())

Also, for type checking i generally recommend pyright since it is better than mypy. With pyright we can do Awaitable[Self] instead of Awaitable[AsyncNeededToInit] becuase mypy doesn't accept typing_extensions.Self a valid type.

RGBCube avatar Jun 23 '22 20:06 RGBCube