I noticed the Sage library has something called an infix operator, to create objects that behave like operators (eg: u *dot* v
), so this gave me an idea. Can I implement an async_in
operator? I tried with the relatively-unused @
matmul operator here:
from typing import Any, AsyncGenerator, Awaitable, Callable
_marker = object()
class AsyncOp:
def __init__(
self,
func: Callable[[Any, Any], Awaitable[bool]],
/,
lhs: Any = _marker,
rhs: Any = _marker,
) -> None:
self._func = func
self._lhs = lhs
self._rhs = rhs
def __matmul__(self, rhs: Any):
if self._lhs is not _marker:
return self._func(self._lhs, rhs)
return AsyncOp(self._func, rhs=rhs)
def __rmatmul__(self, lhs: Any):
if self._rhs is not _marker:
return self._func(lhs, self._rhs)
return AsyncOp(self._func, lhs=lhs)
async def __call__(self, lhs: Any, rhs: Any) -> Awaitable[bool]:
return await self._func(lhs, rhs)
@AsyncOp
async def async_in(lhs: Any, rhs: Any) -> bool:
"""Async `in` operator."""
cls = type(rhs)
if hasattr(cls, "__acontains__"):
return bool(await cls.__acontains__(rhs, lhs))
async for item in rhs:
if item is lhs or item == lhs:
return True
return False
if __name__ == "__main__":
import asyncio
class EvenContainer:
async def __acontains__(self, item: Any) -> bool:
if isinstance(item, int):
return item % 2 == 0
class EvenIterator:
def __init__(self, limit: int = 100) -> None:
self.limit = limit
def __aiter__(self) -> AsyncGenerator[int, None]:
return self.iterator()
async def iterator(self) -> AsyncGenerator[int, None]:
for value in range(0, self.limit, 2):
yield value
class NotIterable:
pass
ec = EvenContainer()
ei = EvenIterator(100)
ni = NotIterable()
async def test():
# Functional syntax with `__acontains__`
print(await async_in(1, ec), "== False")
print(await async_in(10, ec), "== True")
# Operator syntax with `__acontains__`
print(await (1 @async_in@ ec), "== False")
print(await (10 @async_in@ ec), "== True")
# Functional syntax with `__aiter__`
print(await async_in(1, ei), "== False")
print(await async_in(10, ei), "== True")
# Operator syntax with `__aiter__`
print(await (1 @async_in@ ei), "== False")
print(await (10 @async_in@ ei), "== True")
# Non iterables
try:
await async_in(1, ni)
except TypeError as exc:
print(f"{exc.__class__.__name__}: {exc}")
else:
print("TypeError was not raised")
try:
await (1 @async_in@ ni)
except TypeError as exc:
print(f"{exc.__class__.__name__}: {exc}")
else:
print("TypeError was not raised")
asyncio.run(test())
This works, but with caveats:
- Code formatters will put spaces around the binary operators.
x @ async_in @ y
is not obvious at all.
- The
@
operator is lower precedence than the await
operator, so the expression needs brackets. As per the operator precedence chart, await
is higher than all binary operators. The @
operator is also far above in
and can cause other unintended mistakes. (|
is just above.)
- This will need some refactoring for type checkers to understand.