Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 96 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ out = loop.run(foo())
assert out == (4, 5)
```

- Somewhat unusually, our syntax uses `yield` rather than `await`, but the behaviour is the same. Await another coroutine with `yield coro`. Await on multiple with `yield [coro1, coro2, ...]` (a 'gather' in asyncio terminology; a 'nursery' in trio terminology).
- Somewhat unusually, our syntax uses `yield` rather than `await`, but the behaviour is the same. Await another coroutine with `yield coro`. Await on multiple with `yield [coro1, coro2, ...]` (a 'gather' in `asyncio` terminology; a 'nursery' in `trio` terminology).
- An error in one coroutine will cancel all coroutines across the entire event loop.
- If the erroring coroutine is sequentially depended on by a chain of other coroutines, then we chain their tracebacks for easier debugging.
- Errors propagate to and from synchronous operations ran in threads.
Expand Down Expand Up @@ -52,7 +52,7 @@ Coroutines can `yield` four possible things:

- `yield`: yield nothing, this just pauses and gives other coroutines a chance to run.
- `yield coro`: wait on a single coroutine, in which case we'll resume with the output of that coroutine once it is available.
- `yield [coro1, coro2, coro3]`: wait on multiple coroutines by putting them in a list, and resume with a list of outputs once all have completed. This is what asyncio calls a 'gather' or 'TaskGroup', and what trio calls a 'nursery'.
- `yield [coro1, coro2, coro3]`: wait on multiple coroutines by putting them in a list, and resume with a list of outputs once all have completed. This is what `asyncio` calls a 'gather' or 'TaskGroup', and what `trio` calls a 'nursery'.
- `yield {coro1, coro2, coro3}`: schedule one or more coroutines but do not wait on their result - they will run independently in the background.

If you `yield` on the same coroutine multiple times (e.g. in a diamond dependency pattern) then the coroutine will be scheduled once, and on completion all dependees will receive its output. (You can even do this if the coroutine has already finished: `yield` on it to retrieve its output.)
Expand Down Expand Up @@ -181,6 +181,100 @@ tinyio.Lock tinyio.TimeoutError

</details>

### Integration with `asyncio` and `trio`

We have support for putting `trio` event loops within `asyncio`/`trio` event loops, or vice-versa.

<details><summary>Click to expand</summary>

```python
tinyio.to_asyncio tinyio.to_trio
tinyio.from_asyncio tinyio.from_trio
```

---

- `tinyio.to_asyncio(coro, exception_group=None)`

This converts a `tinyio` coroutine into an `asyncio` coroutine.

For example:
```python
def add_one(x):
yield tinyio.sleep(1)
return x + 1

async def foo(x):
y = await tinyio.to_asyncio(add_one(x))
return y

asyncio.run(foo(3))
```

---

- `tinyio.from_asyncio(coro)`

This converts an `asyncio` coroutine into a `tinyio` coroutine.

> WARNING!
> This works by running the entire `asyncio` portion in a separate thread. This may lead to surprises if the `asyncio` and non-`asyncio` portions interact in non-threadsafe ways.

For example:
```python
async def add_one(x):
await asyncio.sleep(1)
return x + 1

def foo(x):
y = yield tinyio.from_asyncio(add_one(x))
return y

tinyio.Loop().run(foo(3))
```

---

- `tinyio.to_trio(coro, exception_group=None)`

This converts a `tinyio` coroutine into an `trio` coroutine.

For example:
```python
def add_one(x):
yield tinyio.sleep(1)
return x + 1

async def foo(x):
y = await tinyio.to_trio(add_one(x))
return y

trio.run(foo, 3)
```

---

- `tinyio.from_trio(async_fn, *args)`

This converts an `trio` coroutine into a `tinyio` coroutine.

For example:
```python
async def add_one(x):
await trio.sleep(1)
return x + 1

def foo(x):
y = yield tinyio.from_trio(add_one, x)
return y

tinyio.Loop().run(foo(3))
```

---

</details>

## FAQ

<details>
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ name = "tinyio"
readme = "README.md"
requires-python = ">=3.11"
urls = {repository = "https://github.com/patrick-kidger/tinyio"}
version = "0.2.0"
version = "0.2.1"

[project.optional-dependencies]
dev = ["pre-commit"]
tests = ["pytest"]
tests = ["pytest", "trio"]

[tool.hatch.build]
include = ["tinyio/*"]
Expand All @@ -42,7 +42,7 @@ src = []

[tool.ruff.lint]
fixable = ["I001", "F401", "UP"]
ignore = ["E402", "E721", "E731", "E741", "F722", "UP038"]
ignore = ["E402", "E721", "E731", "E741", "F722"]
select = ["E", "F", "I001", "UP"]

[tool.ruff.lint.flake8-import-conventions.extend-aliases]
Expand Down
63 changes: 63 additions & 0 deletions tests/test_asyncio_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import asyncio

import tinyio


def test_asyncio_inside_tinyio_basic():
async def add_one(x):
await asyncio.sleep(0.00001)
return x + 1

async def f(x):
y = await add_one(x)
z = await add_one(y)
return z

out = tinyio.Loop().run(tinyio.from_asyncio(f(3)))
assert out == 5


def test_asyncio_inside_tinyio_complex():
async def add_one(x):
await asyncio.sleep(0.00001)
return x + 1

async def f(x):
y = await add_one(x)
z = await add_one(y)
return z

def g(x):
for _ in range(20):
yield
return x + 10

def h(x):
ff = tinyio.from_asyncio(f(3))
a, b = yield [ff, g(x)]
a2 = yield ff
return a + a2 + b

out = tinyio.Loop().run(h(3))
assert out == 23


def test_tinyio_inside_asyncio():
def _add_one(x: int) -> tinyio.Coro[int]:
yield tinyio.sleep(0.1)
return x + 1

def _diamond1(x: int) -> tinyio.Coro[int]:
y = _add_one(x)
a, b = yield [_diamond2(y, 1), _diamond2(y, 2)]
return a + b

def _diamond2(y: tinyio.Coro[int], factor: int):
z = yield y
return z * factor

async def f():
out = await tinyio.to_asyncio(_diamond1(2))
return out

assert asyncio.run(f()) == 9
51 changes: 51 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,3 +503,54 @@ def g():
loop = tinyio.Loop()
with pytest.raises(RuntimeError, match="has already finished"):
loop.run(g())


def test_run_finished_coroutine1():
loop = tinyio.Loop()

def f():
return (yield gg)

def g():
yield
return 4

gg = g()

assert loop.run(f()) == 4
assert loop.run(gg) == 4


def test_run_finished_coroutine2():
loop = tinyio.Loop()

def f():
yield
return 4

ff = f()
assert loop.run(ff) == 4
assert loop.run(ff) == 4


@pytest.mark.parametrize("num_yields", (0, 1, 20))
def test_reentrant(num_yields):
loop = tinyio.Loop()

def f():
return (yield [gg, h()])

def g():
for _ in range(num_yields):
yield
return 4

gg = g()

def h():
yield
out = loop.run(gg)
return out

with pytest.raises(RuntimeError, match="whilst the loop is currently running"):
assert loop.run(f()) == [4, 4]
62 changes: 62 additions & 0 deletions tests/test_trio_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import tinyio
import trio # pyright: ignore[reportMissingImports]


def test_trio_inside_tinyio_basic():
async def add_one(x):
await trio.sleep(0.00001)
return x + 1

async def f(x):
y = await add_one(x)
z = await add_one(y)
return z

out = tinyio.Loop().run(tinyio.from_trio(f, 3))
assert out == 5


def test_trio_inside_tinyio_complex():
async def add_one(x):
await trio.sleep(0.00001)
return x + 1

async def f(x):
y = await add_one(x)
z = await add_one(y)
return z

def g(x):
for _ in range(20):
yield
return x + 10

def h(x):
ff = tinyio.from_trio(f, 3)
a, b = yield [ff, g(x)]
a2 = yield ff
return a + a2 + b

out = tinyio.Loop().run(h(3))
assert out == 23


def test_tinyio_inside_trio():
def _add_one(x: int) -> tinyio.Coro[int]:
yield tinyio.sleep(0.1)
return x + 1

def _diamond1(x: int) -> tinyio.Coro[int]:
y = _add_one(x)
a, b = yield [_diamond2(y, 1), _diamond2(y, 2)]
return a + b

def _diamond2(y: tinyio.Coro[int], factor: int):
z = yield y
return z * factor

async def f():
out = await tinyio.to_trio(_diamond1(2))
return out

assert trio.run(f) == 9
6 changes: 6 additions & 0 deletions tinyio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
Event as Event,
Loop as Loop,
)
from ._integrations import (
from_asyncio as from_asyncio,
from_trio as from_trio,
to_asyncio as to_asyncio,
to_trio as to_trio,
)
from ._sync import Barrier as Barrier, Lock as Lock, Semaphore as Semaphore
from ._thread import ThreadPool as ThreadPool, run_in_thread as run_in_thread
from ._time import TimeoutError as TimeoutError, sleep as sleep, timeout as timeout
Loading