Open couling opened 2 years ago
That's because the
yield
has a subtly different effect for async methods. An async generator method (such asFoo2.bar()
) are not coroutines. If you call them withoutawait
you get anAsyncIterable
not aCoroutine
.
Interesting 🤔 Tbh I wasn't aware of that.
However, I still think the error make sense here. Foo.bar
isn't a Generator, let alone an async one. The return type should probably be Iterable[str]
instead. With that, both mypy
and pyright
also report that Foo2.bar
overrides Foo.bar
in an incompatible manner.
@cdce8p
Foo.bar
isn't a Generator , let alone an async one.
That's putting the relationship the wrong way around. Generators are Iterators, not all Iterators are Generators.
This issue is about the async equivalent. If you've got an alternative to make an async equivalent of the code below then I'm open to suggestions.
class Foo(Protocol):
# Note baz correctly interacts with bar by calling to obtain an Iterable
def baz(self) -> Iterable[str]:
return self.bar()
def bar(self) -> Iterable[str]:
pass
class Foo2(Foo):
# It's legitimate to implement an Iterator using a Generator
# This doesn't break Foo.baz.
# This is incredibly useful and not something a linter should block you from doing.
def bar(self) -> Iterable[str]:
yield "hello"
There's three legitimate ways to use async combined with a for loop. BUT there's no overlap, only one of them will work for a given method. Whichever the parent class supported, must also be supported by the child class:
async for b in foo.bar():
calls (not awaits) foo.bar()
and then awaits __aiter__()
and __anext__()
on the resultasync for b in await foo.bar():
awaits foo.bar()
and then awaits __aiter__()
and __anext__()
on the resultfor b in await foo.bar():
awaits foo.bar()
but does not await anything else, it calls __iter__
and __next__
.If foo.bar is an async generator:
await
an AsyncIterator
which is not a Coroutine so will raise an error. __iter__()
instead of await __aiter__()
and will raise an error.For a protocol to support implementation by async generator it MUST be possible to use the method with (1) async for b in foo.bar():
.
To implement that same protocol without a generator requires a non-async method to return an AsyncIterator.
Otherwise an async method return
ing an AsyncIterator MUST be called with (2) async for b in await foo.bar():
. This would be incomparable with the protocol and incompatible with any implementation using an async generator.
With that, both mypy and pyright also report that Foo2.bar overrides Foo.bar in an incompatible manner.
Pycharm alerted me to the issue when working with a protocol. Though I now see Pycharm only raises it for protocols. I've edited the initial bug report to match. Pycharm will raise a warning from its Invalid protocol definitions and usages
checker. So in the case of protocols there's no way to get Pylint and Pycharm to agree (AFAIK).
This issue has caught out plenty of people (one discussion on StackOverflow here). If you accept my logic then maybe mypy
and pyright
will need issues raising against them too.
The problem comes down to what's practically compatible. Python's behaviour breaks the Principle of Least Astonishment IMHO and I have no doubt that the authors of other linters were not aware of this behaviour either.
Apologies for the lengthy follow up. I live in hope of being proved wrong.
I think the same issue is occuring with Abstract Base Classes; the code below makes mypy
happy; but pylint fails with:
************* Module publishing.ben
publishing/ben.py:14:4: W0236: Method 'my_method' was expected to be 'non-async', found it instead as 'async' (invalid-overridden-method)
from abc import ABC, abstractmethod
from contextlib import _AsyncGeneratorContextManager, asynccontextmanager
from typing import AsyncIterator
class BaseOperation(ABC):
@abstractmethod
def my_method(self) -> _AsyncGeneratorContextManager:
...
class ConcreteOperation(BaseOperation):
@asynccontextmanager
async def my_method(self) -> AsyncIterator[str]:
yield "Hello"
@benwah If you haven't found a nice workaround yet, there is one now: https://github.com/pylint-dev/pylint/issues/8939#issuecomment-1810894993 😉
Bug description
For classes
Pylint will give a false positive:
That's incorrect because the
yield
has a subtly different effect for async methods. An async generator method (such asFoo2.bar()
) are not coroutines. If you call them withoutawait
you get anAsyncIterable
not aCoroutine
.That is, to use an async generator you call:
You do not call:
Configuration
No response
Command used
Pylint output
Expected behavior
(If at all possible) pylint should detect the
yield
in an async method and interpret it as a a non-async method.Pylint version
OS / Environment
No response
Additional dependencies
No response