python-trio / trio-typing

Type hints for Trio and related projects
Other
27 stars 14 forks source link

mypy error on async_generator.aclosing on an async generator object #31

Closed ZeeD closed 2 years ago

ZeeD commented 3 years ago

I tried to make a simple snippet to reproduce the error

from typing import AsyncGenerator
from async_generator import aclosing

async def async_generator_factory() -> AsyncGenerator[int, None]:
    yield 123

async def amain() -> None:
    async with aclosing(async_generator_factory()) as async_generator:
        async for element in async_generator:
            print(element)

running mypy I have this error:

> mypy foo.py
foo.py:8: error: Value of type variable "_T_closeable" of "aclosing" cannot be "AsyncGenerator[int, None]"
Found 1 error in 1 file (checked 1 source file)

I'm using Python 3.8.0, mypy 0.800, async-generator 1.10, trio-typing 0.5.0 and my mypy.ini is

[mypy]
plugins = trio_typing.plugin
strict = True
florimondmanca commented 3 years ago

aclosing(thing) expects a thing that has a aclose() method:

https://github.com/python-trio/trio-typing/blob/f32f17b0f242daf2d42407f383ca581d64b6c299/async_generator-stubs/__init__.pyi#L50-L55

Yet AsyncGenerator objects do have such an aclose() method:

$ python -m asyncio
>>> async def agen():
...     yield 0
...
>>> g = agen()
>>> g.aclose
<built-in method aclose of async_generator object at 0x10734da60>

And they are also spec'd as having one:

https://docs.python.org/3/library/collections.abc.html

ABC Inherits from Abstract Methods Mixin Methods
AsyncGenerator AsyncIterator asend, athrow aclose, aiter, anext

This alternative snippet (independent of trio-typing) seems to say that the problem is not with the presence of aclose(), but with its signature

from typing import Protocol, AsyncContextManager, AsyncGenerator, Awaitable

class _AsyncCloseable(Protocol):
    async def aclose(self) -> None:
        ...

def aclosing(obj: _AsyncCloseable) -> AsyncContextManager[_AsyncCloseable]:
    ...

async def agen() -> AsyncGenerator[int, None]:
    yield 0

async def main() -> None:
    async with aclosing(agen()) as _:  # error
        pass
example.py:18: error: Argument 1 to "aclosing" has incompatible type "AsyncGenerator[int, None]"; expected "_AsyncCloseable"
example.py:18: note: Following member(s) of "AsyncGenerator[int, None]" have conflicts:
example.py:18: note:     Expected:
example.py:18: note:         def aclose(self) -> Coroutine[Any, Any, None]
example.py:18: note:     Got:
example.py:18: note:         def aclose(self) -> Awaitable[None]
Found 1 error in 1 file (checked 1 source file)

So it seems _AsyncClosable should actually be this…?

class _AsyncCloseable(Protocol):
    def aclose(self) -> Awaitable[None]:
        ...

Which would work with both async generators, and arbitrary classes that have an async def aclose(self) -> None method…

class A:
    async def aclose(self) -> None:
        pass

async def agen() -> AsyncGenerator[int, None]:
    yield 0

async def main() -> None:
    async with aclosing(agen()) as _:  # OK
        pass

    async with aclosing(A()) as _:  # OK
        pass

The reason is that Awaitable, which is what an async generator's aclose() returns, is a parent of Coroutine, which is what a regular async def () -> None returns.

ABC Inherits from Abstract Methods Mixin Methods
Awaitable   await  
Coroutine Awaitable send, throw close

I don't know why async generator's aclose() are typed as returning an Awaitable, but heh…

graingert commented 3 years ago

I don't know why async generator's aclose() are typed as returning an Awaitable, but heh… here's the def: https://github.com/python/typeshed/blob/2b64f54008f6dbaded7970336e26c4f02fa82fd9/stdlib/types.pyi#L192

it's an abc so needs to be as wide as possible to support any subclasses