Skip to content

renew APIs #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 4, 2024
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Inter-task sychronization and communication.
Pin the minor version.

```
poetry add asyncgui-ext-synctools@~0.1
pip install "asyncgui-ext-synctools>=0.1,<0.2"
poetry add asyncgui-ext-synctools@~0.2
pip install "asyncgui-ext-synctools>=0.2,<0.3"
```

## Tested on
Expand Down
46 changes: 24 additions & 22 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "asyncgui-ext-synctools"
version = "0.1.0"
version = "0.2.0"
description = "Inter-task sychronization and communication."
authors = ["Nattōsai Mitō <[email protected]>"]
license = "MIT"
Expand Down
4 changes: 3 additions & 1 deletion src/asyncgui_ext/synctools/all.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
__all__ = (
'Event',
'Event', 'Box',
)
from .event import Event
from .box import Box

74 changes: 74 additions & 0 deletions src/asyncgui_ext/synctools/box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
__all__ = ('Box', )
import types


class Box:
'''
Similar to :class:`asyncgui.AsyncBox`, but this one can handle multiple tasks simultaneously.
This is the closest thing to :class:`asyncio.Event` in this library.

.. code-block::

async def async_fn(b1, b2):
args, kwargs = await b1.get()
assert args == (1, )
assert kwargs == {'crow': 'raven', }

args, kwargs = await b2.get()
assert args == (2, )
assert kwargs == {'frog': 'toad', }

args, kwargs = await b1.get()
assert args == (1, )
assert kwargs == {'crow': 'raven', }

b1 = Box()
b2 = Box()
b1.put(1, crow='raven')
start(async_fn(b1, b2))
b2.put(2, frog='toad')
'''

__slots__ = ('_item', '_waiting_tasks', )

def __init__(self):
self._item = None
self._waiting_tasks = []

@property
def is_empty(self) -> bool:
return self._item is None

def put(self, *args, **kwargs):
'''Put an item into the box if it's empty.'''
if self._item is None:
self.put_or_update(*args, **kwargs)

def update(self, *args, **kwargs):
'''Replace the item in the box if there is one already.'''
if self._item is not None:
self.put_or_update(*args, **kwargs)

def put_or_update(self, *args, **kwargs):
self._item = (args, kwargs, )
tasks = self._waiting_tasks
self._waiting_tasks = []
for t in tasks:
if t is not None:
t._step(*args, **kwargs)

def clear(self):
'''Remove the item from the box if there is one.'''
self._item = None

@types.coroutine
def get(self):
'''Get the item from the box if there is one. Otherwise, wait until it's put.'''
if self._item is not None:
return self._item
tasks = self._waiting_tasks
idx = len(tasks)
try:
return (yield tasks.append)
finally:
tasks[idx] = None
62 changes: 22 additions & 40 deletions src/asyncgui_ext/synctools/event.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,46 @@
__all__ = (
'Event',
)
__all__ = ('Event', )
import types
import typing as T


class Event:
'''
Similar to :class:`asyncio.Event`.
The differences are:

* :meth:`set` accepts any number of arguments but doesn't use them at all so it can be used as a callback function
in any library.
* :attr:`is_set` is a property not a function.
Similar to :class:`asyncgui.AsyncEvent`, but this one can handle multiple tasks simultaneously.

.. code-block::

async def async_fn(e):
args, kwargs = await e.wait()
assert args == (2, )
assert kwargs == {'crow': 'raven', }

args, kwargs = await e.wait()
assert args == (3, )
assert kwargs == {'toad': 'frog', }

e = Event()
any_library.register_callback(e.set)
e.fire(1, crocodile='alligator')
start(async_fn(e))
e.fire(2, crow='raven')
e.fire(3, toad='frog')
'''

__slots__ = ('_flag', '_waiting_tasks', )
__slots__ = ('_waiting_tasks', )

def __init__(self):
self._flag = False
self._waiting_tasks = []

@property
def is_set(self) -> bool:
return self._flag

def set(self, *args, **kwargs):
'''
Set the event.
Unlike asyncio's, all tasks waiting for this event to be set will be resumed *immediately*.
'''
if self._flag:
return
self._flag = True
def fire(self, *args, **kwargs):
tasks = self._waiting_tasks
self._waiting_tasks = []
for t in tasks:
if t is not None:
t._step()

def clear(self):
'''Unset the event.'''
self._flag = False
t._step(*args, **kwargs)

@types.coroutine
def wait(self) -> T.Awaitable:
'''
Wait for the event to be set.
Return *immediately* if it's already set.
'''
if self._flag:
return
def wait(self):
tasks = self._waiting_tasks
idx = len(tasks)
try:
tasks = self._waiting_tasks
idx = len(tasks)
yield tasks.append
return (yield tasks.append)
finally:
tasks[idx] = None
102 changes: 102 additions & 0 deletions tests/test_box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import pytest


def test_get_then_put():
import asyncgui as ag
from asyncgui_ext.synctools.box import Box
TS = ag.TaskState
b = Box()
t1 = ag.start(b.get())
t2 = ag.start(b.get())
assert t1.state is TS.STARTED
assert t2.state is TS.STARTED
b.put(7, crow='raven')
assert t1.result == ((7, ), {'crow': 'raven', })
assert t2.result == ((7, ), {'crow': 'raven', })


def test_put_then_get():
import asyncgui as ag
from asyncgui_ext.synctools.box import Box
TS = ag.TaskState
b = Box()
b.put(7, crow='raven')
t1 = ag.start(b.get())
t2 = ag.start(b.get())
assert t1.state is TS.FINISHED
assert t2.state is TS.FINISHED
assert t1.result == ((7, ), {'crow': 'raven', })
assert t2.result == ((7, ), {'crow': 'raven', })


def test_clear():
import asyncgui as ag
from asyncgui_ext.synctools.box import Box
b1 = Box()
b2 = Box()

async def async_fn():
assert (await b1.get()) == ((7, ), {'crow': 'raven', })
assert (await b2.get()) == ((6, ), {'crocodile': 'alligator', })
assert (await b1.get()) == ((5, ), {'toad': 'frog', })

task = ag.start(async_fn())
b1.put(7, crow='raven')
b1.clear()
b2.put(6, crocodile='alligator')
b1.put(5, toad='frog')
assert task.finished


def test_cancel():
import asyncgui as ag
from asyncgui_ext.synctools.box import Box
TS = ag.TaskState

async def async_fn(ctx, b):
async with ag.open_cancel_scope() as scope:
ctx['scope'] = scope
await b.get()
pytest.fail()
await ag.sleep_forever()

ctx = {}
b = Box()
task = ag.start(async_fn(ctx, b))
assert task.state is TS.STARTED
ctx['scope'].cancel()
assert task.state is TS.STARTED
b.put()
assert task.state is TS.STARTED
task._step()
assert task.state is TS.FINISHED


def test_complicated_cancel():
import asyncgui as ag
from asyncgui_ext.synctools.box import Box
TS = ag.TaskState

async def async_fn_1(ctx, b):
await b.get()
ctx['scope'].cancel()

async def async_fn_2(ctx, b):
async with ag.open_cancel_scope() as scope:
ctx['scope'] = scope
await b.get()
pytest.fail()
await ag.sleep_forever()

ctx = {}
b = Box()
t1 = ag.start(async_fn_1(ctx, b))
t2 = ag.start(async_fn_2(ctx, b))
assert b._waiting_tasks == [t1, t2, ]
assert t2.state is TS.STARTED
b.put()
assert t1.state is TS.FINISHED
assert t2.state is TS.STARTED
assert b._waiting_tasks == []
t2._step()
assert t2.state is TS.FINISHED
Loading
Loading