python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.5k stars 2.83k forks source link

Error with Protocols and AsyncContextManager #8276

Open junqed opened 4 years ago

junqed commented 4 years ago

import asyncio import typing as t from contextlib import asynccontextmanager

import typing_extensions as te

class LockProto(te.Protocol): async def with_lock(self) -> t.AsyncContextManager[str]: ...

class Consumer: def init(self, locker: LockProto) -> None: self.locker = locker

def run(self) -> None:
    async with self.locker.with_lock() as name:
        print(name)

class SockLock: @asynccontextmanager async def with_lock(self) -> t.AsyncGenerator[str, None]: yield 'some-string'

async def main() -> None: cons = Consumer(SockLock()) await cons.run()

if name == 'main': asyncio.run(main())


* What is the actual behavior/output?

bug.py:18: error: "Coroutine[Any, Any, AsyncContextManager[str]]" has no attribute "enter" bug.py:18: error: "Coroutine[Any, Any, AsyncContextManager[str]]" has no attribute "exit" bug.py:29: error: Argument 1 to "Consumer" has incompatible type "SockLock"; expected "LockProto" bug.py:29: note: Following member(s) of "SockLock" have conflicts: bug.py:29: note: Expected: bug.py:29: note: def with_lock(self) -> Coroutine[Any, Any, AsyncContextManager[str]] bug.py:29: note: Got: bug.py:29: note: def with_lock(*Any, **Any) -> AsyncContextManager[Any]



* What is the behavior/output you expect?
No errors

* What are the versions of mypy and Python you are using?
0.761

* What are the mypy flags you are using? (For example --strict-optional)
No flags, just `mypy bug.py`
Michael0x2a commented 4 years ago

Not sure about the error on line 29, but the error from line 18 at least seems to be a legitimate one -- you need to use async with (and consequently make your run function an async def). Otherwise you get the following runtime error:

Traceback (most recent call last):
  File "test.py", line 34, in <module>
    main()
  File "test.py", line 30, in main
    cons.run()
  File "test.py", line 18, in run
    with self.locker.with_lock() as name:
AttributeError: __enter__
junqed commented 4 years ago

Not sure about the error on line 29, but the error from line 18 at least seems to be a legitimate one -- you need to use async with (and consequently make your run function an async def). Otherwise you get the following runtime error:

Sorry, I forgot about it, fixed in my example

JukkaL commented 4 years ago

It looks like the type annotations were incorrect and the errors were legitimate. This version passes without errors (I'm not 100% sure if the annotations are correct here, though):

import asyncio
from contextlib import asynccontextmanager

from typing import AsyncGenerator, AsyncContextManager
from typing_extensions import Protocol

class LockProto(Protocol):
    def with_lock(self) -> AsyncContextManager[str]: ...

class Consumer:
    def __init__(self, locker: LockProto) -> None:
        self.locker = locker

    async def run(self) -> None:
        async with self.locker.with_lock() as name:
            print(name)

class SockLock:
    @asynccontextmanager
    async def with_lock(self) -> AsyncGenerator[str, None]:
        yield 'some-string'

async def main() -> None:
    cons = Consumer(SockLock())
    await cons.run()

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

Changes I made:

I wonder if some error messages could be improved here?

junqed commented 4 years ago

Thanks, moving away async from the protocol fixed the error. But why? Do I understand correctly that the decorator asynccontextmanager changes the function signature implicitly and mypy understands it? Is it a good idea to add such cases to the docs?

JukkaL commented 4 years ago

Yeah, asynccontextmanager changes the signature and mypy understands (to a certain extent, at least). Mentioning this in the documentation may be a good idea, if we find a good place to put it. contextmanager is another typical example and much more common, so it should be documented as well (or at first).

junqed commented 4 years ago

https://mypy.readthedocs.io/en/stable/kinds_of_types.html is it a good place for such kind of examples? Or at least just to put them in the examples https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html#miscellaneous

JukkaL commented 4 years ago

I'd say that appending to https://mypy.readthedocs.io/en/stable/kinds_of_types.html is a good place for contextmanager. asynccontextmanager could be documented in https://mypy.readthedocs.io/en/stable/more_types.html#typing-async-await. These don't feel important enough to be included in the cheat sheet.

joybh98 commented 4 years ago

I'm taking this issue

joybh98 commented 4 years ago

@JukkaL where should I add contextmanager in https://mypy.readthedocs.io/en/stable/kinds_of_types.html ?

ethanhs commented 4 years ago

@joybhallaa I would create a new section called "Context Managers".