Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async Class Constructors #148

Open
RGBCube opened this issue Jun 22, 2022 · 4 comments
Open

Async Class Constructors #148

RGBCube opened this issue Jun 22, 2022 · 4 comments

Comments

@RGBCube
Copy link

RGBCube commented Jun 22, 2022

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.

@jonathanslenders
Copy link
Owner

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.

@RGBCube
Copy link
Author

RGBCube commented Jun 23, 2022

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

@jonathanslenders
Copy link
Owner

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")

@RGBCube
Copy link
Author

RGBCube commented Jun 23, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants