Asyncio method chaining

i want implement an asyncio method chaining with full typing hint support like this. but I can’t get type hint work. Can someone help it?

when you type  `await Chain(Browser()).` this and a dot. you get hint for Browser’s method.
await Chain(Browser()).<hint for Browser method>
import asyncio
from typing import Any


class Browser:
    async def go(self) -> "Browser":
        print("go")
        return self

    async def click(self) -> "Browser":
        print("click")
        return self

    def text(self) -> str:
        print("text")
        return "result"


class Chain:
    def __init__(self, obj: Any) -> None:
        self._obj = obj
        self._queue = []

    def __getattr__(self, name: str) -> "Chain":
        self._queue.append({'type': 'getattr', 'name': name})
        return self

    def __call__(self, *args: Any, **kwargs: Any) -> "Chain":
        self._queue.append({'type': 'call', 'params': [args, kwargs]})
        return self

    async def execute(self) -> Any:
        res = self._obj
        while self._queue:
            action = self._queue.pop(0)
            if action['type'] == 'getattr':
                res = getattr(res, action['name'])
            elif action['type'] == 'call':
                args, kwargs = action['params']
                res = res(*args, **kwargs)
            if asyncio.iscoroutine(res):
                res = await res
        return res


async def main() -> None:
    text = await Chain(Browser()).go().click().go().text().execute()
    print(text)

if __name__ == '__main__':
    asyncio.run(main())

Anything relying on __getattr__ won’t get type hints and autocomplete support. Type checkers and language servers rely on static analysis to see which attributes are available and what their type is, they can’t see what __getattr__ is doing.

A common workaround for proxy objects like this is to just have the function that generates them lie about what it returns. So in your case, you’d rename the Chain class to something like _Chain and then make Chain a function typed as def Chain[T](arg: T) -> T. Of course, you’d still actually return an object of type _Chain, but you’d just ignore the type error on that line. This means that type checkers and autocomplete will treat the return value as an object of the argument type, giving you its autocompletions and types.

But that approach won’t work here since you don’t actually need a proxy object with the same interface as the original object, but one where all async methods are sync. Unfortunately, that isn’t something that’s currently expressible to static analysis tools. The best you can do is create a new protocol for each class and duplicate all the method signatures but without async. This would be solvable with a fairly nifty and complicated implementation of several type system features that are being thought about at the moment, but I wouldn’t hold my breath for anything soon.