dry-python / dry-python/returns

can't compose @future_safe coroutines that specify picky exceptions

Open
#2,080 3 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
4.4k
Forks
155
Avg merge
2h 27m
Merged PRs (30d)
22

Description

Given coroutines `coro1`, `coro2`:

```python
@future_safe(exceptions=(ConnectionError,))
async def coro1(c: int | None) -> int:
if c is None:
raise ConnectionError("not connected")
await asyncio.sleep(0) # emulate I/O
return c

@future_safe(exceptions=(ZeroDivisionError,))
async def coro2(n: int) -> float:
await asyncio.sleep(0) # emulate I/O
return 1 / n
```

I want to combine them via bind: `coro1(c).bind(coro2)`. It results in type error:

```shell
$ uvx --with 'returns[compatible-mypy]==0.25.0' mypy mre.py; ./mre.py
mre.py:30: error: Argument 1 to "bind" of "FutureResult" has incompatible type "Callable[[int], FutureResult[float, ZeroDivisionError]]"; expected "Callable[[int], KindN[FutureResult[Any, Any], float, ConnectionError, Any]]" [arg-type]
```

where `mre.py`:

```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "returns >=0.25.0",
# ]
# ///
from typing import assert_never
from returns.result import Success, Failure
from returns.io import IOSuccess, IOFailure
from returns.future import future_safe

@future_safe(exceptions=(ConnectionError,))
async def coro1(c: int | None) -> int:
if c is None:
raise ConnectionError("not connected")
await asyncio.sleep(0) # emulate I/O
return c

@future_safe(exceptions=(ZeroDivisionError,))
async def coro2(n: int) -> float:
await asyncio.sleep(0) # emulate I/O
return 1 / n

async def run() -> None:
for c in [2, 0, None]:
match await coro1(c).bind(coro2):
case IOSuccess(Success(r)):
assert r == 1 / 2, r
case IOFailure(Failure(ZeroDivisionError(args=(msg,)))):
assert msg == "division by zero", msg
case IOFailure(Failure(ConnectionError(args=(msg,)))):
assert msg == "not connected", msg
case _ as unreachable:
assert_never(unreachable) # type: ignore[arg-type]

if __name__ == "__main__":
import asyncio

asyncio.run(run())
```

If picky exceptions are removed:
```python
@future_safe
async def coro1(c: int | None) -> int: ...

@future_safe
async def coro2(n: int) -> float: ...
```
I get the desired behavior that coroutines are composed without type errors but these declarations catch too much.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.