Skip to content

Commit 3d51700

Browse files
(api-break) renew APIs
1 parent 4a7f2ce commit 3d51700

File tree

5 files changed

+229
-95
lines changed

5 files changed

+229
-95
lines changed

src/asyncgui_ext/synctools/all.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
__all__ = (
2-
'Event',
2+
'Event', 'Box',
33
)
44
from .event import Event
5+
from .box import Box
6+

src/asyncgui_ext/synctools/box.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
__all__ = ('Box', )
2+
import types
3+
4+
5+
class Box:
6+
'''
7+
Similar to :class:`asyncgui.AsyncBox`, but this one can handle multiple tasks simultaneously.
8+
This is the closest thing to :class:`asyncio.Event` in this library.
9+
10+
.. code-block::
11+
12+
async def async_fn(b1, b2):
13+
args, kwargs = await b1.get()
14+
assert args == (1, )
15+
assert kwargs == {'crow': 'raven', }
16+
17+
args, kwargs = await b2.get()
18+
assert args == (2, )
19+
assert kwargs == {'frog': 'toad', }
20+
21+
args, kwargs = await b1.get()
22+
assert args == (1, )
23+
assert kwargs == {'crow': 'raven', }
24+
25+
b1 = Box()
26+
b2 = Box()
27+
b1.put(1, crow='raven')
28+
start(async_fn(b1, b2))
29+
b2.put(2, frog='toad')
30+
'''
31+
32+
__slots__ = ('_item', '_waiting_tasks', )
33+
34+
def __init__(self):
35+
self._item = None
36+
self._waiting_tasks = []
37+
38+
@property
39+
def is_empty(self) -> bool:
40+
return self._item is None
41+
42+
def put(self, *args, **kwargs):
43+
'''Put an item into the box if it's empty.'''
44+
if self._item is None:
45+
self.put_or_update(*args, **kwargs)
46+
47+
def update(self, *args, **kwargs):
48+
'''Replace the item in the box if there is one already.'''
49+
if self._item is not None:
50+
self.put_or_update(*args, **kwargs)
51+
52+
def put_or_update(self, *args, **kwargs):
53+
self._item = (args, kwargs, )
54+
tasks = self._waiting_tasks
55+
self._waiting_tasks = []
56+
for t in tasks:
57+
if t is not None:
58+
t._step(*args, **kwargs)
59+
60+
def clear(self):
61+
'''Remove the item from the box if there is one.'''
62+
self._item = None
63+
64+
@types.coroutine
65+
def get(self):
66+
'''Get the item from the box if there is one. Otherwise, wait until it's put.'''
67+
if self._item is not None:
68+
return self._item
69+
tasks = self._waiting_tasks
70+
idx = len(tasks)
71+
try:
72+
return (yield tasks.append)
73+
finally:
74+
tasks[idx] = None

src/asyncgui_ext/synctools/event.py

+22-40
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,46 @@
1-
__all__ = (
2-
'Event',
3-
)
1+
__all__ = ('Event', )
42
import types
5-
import typing as T
63

74

85
class Event:
96
'''
10-
Similar to :class:`asyncio.Event`.
11-
The differences are:
12-
13-
* :meth:`set` accepts any number of arguments but doesn't use them at all so it can be used as a callback function
14-
in any library.
15-
* :attr:`is_set` is a property not a function.
7+
Similar to :class:`asyncgui.AsyncEvent`, but this one can handle multiple tasks simultaneously.
168
179
.. code-block::
1810
11+
async def async_fn(e):
12+
args, kwargs = await e.wait()
13+
assert args == (2, )
14+
assert kwargs == {'crow': 'raven', }
15+
16+
args, kwargs = await e.wait()
17+
assert args == (3, )
18+
assert kwargs == {'toad': 'frog', }
19+
1920
e = Event()
20-
any_library.register_callback(e.set)
21+
e.fire(1, crocodile='alligator')
22+
start(async_fn(e))
23+
e.fire(2, crow='raven')
24+
e.fire(3, toad='frog')
2125
'''
2226

23-
__slots__ = ('_flag', '_waiting_tasks', )
27+
__slots__ = ('_waiting_tasks', )
2428

2529
def __init__(self):
26-
self._flag = False
2730
self._waiting_tasks = []
2831

29-
@property
30-
def is_set(self) -> bool:
31-
return self._flag
32-
33-
def set(self, *args, **kwargs):
34-
'''
35-
Set the event.
36-
Unlike asyncio's, all tasks waiting for this event to be set will be resumed *immediately*.
37-
'''
38-
if self._flag:
39-
return
40-
self._flag = True
32+
def fire(self, *args, **kwargs):
4133
tasks = self._waiting_tasks
4234
self._waiting_tasks = []
4335
for t in tasks:
4436
if t is not None:
45-
t._step()
46-
47-
def clear(self):
48-
'''Unset the event.'''
49-
self._flag = False
37+
t._step(*args, **kwargs)
5038

5139
@types.coroutine
52-
def wait(self) -> T.Awaitable:
53-
'''
54-
Wait for the event to be set.
55-
Return *immediately* if it's already set.
56-
'''
57-
if self._flag:
58-
return
40+
def wait(self):
41+
tasks = self._waiting_tasks
42+
idx = len(tasks)
5943
try:
60-
tasks = self._waiting_tasks
61-
idx = len(tasks)
62-
yield tasks.append
44+
return (yield tasks.append)
6345
finally:
6446
tasks[idx] = None

tests/test_box.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import pytest
2+
3+
4+
def test_get_then_put():
5+
import asyncgui as ag
6+
from asyncgui_ext.synctools.box import Box
7+
TS = ag.TaskState
8+
b = Box()
9+
t1 = ag.start(b.get())
10+
t2 = ag.start(b.get())
11+
assert t1.state is TS.STARTED
12+
assert t2.state is TS.STARTED
13+
b.put(7, crow='raven')
14+
assert t1.result == ((7, ), {'crow': 'raven', })
15+
assert t2.result == ((7, ), {'crow': 'raven', })
16+
17+
18+
def test_put_then_get():
19+
import asyncgui as ag
20+
from asyncgui_ext.synctools.box import Box
21+
TS = ag.TaskState
22+
b = Box()
23+
b.put(7, crow='raven')
24+
t1 = ag.start(b.get())
25+
t2 = ag.start(b.get())
26+
assert t1.state is TS.FINISHED
27+
assert t2.state is TS.FINISHED
28+
assert t1.result == ((7, ), {'crow': 'raven', })
29+
assert t2.result == ((7, ), {'crow': 'raven', })
30+
31+
32+
def test_clear():
33+
import asyncgui as ag
34+
from asyncgui_ext.synctools.box import Box
35+
b1 = Box()
36+
b2 = Box()
37+
38+
async def async_fn():
39+
assert (await b1.get()) == ((7, ), {'crow': 'raven', })
40+
assert (await b2.get()) == ((6, ), {'crocodile': 'alligator', })
41+
assert (await b1.get()) == ((5, ), {'toad': 'frog', })
42+
43+
task = ag.start(async_fn())
44+
b1.put(7, crow='raven')
45+
b1.clear()
46+
b2.put(6, crocodile='alligator')
47+
b1.put(5, toad='frog')
48+
assert task.finished
49+
50+
51+
def test_cancel():
52+
import asyncgui as ag
53+
from asyncgui_ext.synctools.box import Box
54+
TS = ag.TaskState
55+
56+
async def async_fn(ctx, b):
57+
async with ag.open_cancel_scope() as scope:
58+
ctx['scope'] = scope
59+
await b.get()
60+
pytest.fail()
61+
await ag.sleep_forever()
62+
63+
ctx = {}
64+
b = Box()
65+
task = ag.start(async_fn(ctx, b))
66+
assert task.state is TS.STARTED
67+
ctx['scope'].cancel()
68+
assert task.state is TS.STARTED
69+
b.put()
70+
assert task.state is TS.STARTED
71+
task._step()
72+
assert task.state is TS.FINISHED
73+
74+
75+
def test_complicated_cancel():
76+
import asyncgui as ag
77+
from asyncgui_ext.synctools.box import Box
78+
TS = ag.TaskState
79+
80+
async def async_fn_1(ctx, b):
81+
await b.get()
82+
ctx['scope'].cancel()
83+
84+
async def async_fn_2(ctx, b):
85+
async with ag.open_cancel_scope() as scope:
86+
ctx['scope'] = scope
87+
await b.get()
88+
pytest.fail()
89+
await ag.sleep_forever()
90+
91+
ctx = {}
92+
b = Box()
93+
t1 = ag.start(async_fn_1(ctx, b))
94+
t2 = ag.start(async_fn_2(ctx, b))
95+
assert b._waiting_tasks == [t1, t2, ]
96+
assert t2.state is TS.STARTED
97+
b.put()
98+
assert t1.state is TS.FINISHED
99+
assert t2.state is TS.STARTED
100+
assert b._waiting_tasks == []
101+
t2._step()
102+
assert t2.state is TS.FINISHED

tests/test_event.py

+28-54
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,33 @@
11
import pytest
22

33

4-
def test_wait_then_set():
4+
def test_wait_then_fire():
55
import asyncgui as ag
66
from asyncgui_ext.synctools.event import Event
77
TS = ag.TaskState
88
e = Event()
9-
task1 = ag.start(e.wait())
10-
task2 = ag.start(e.wait())
11-
assert task1.state is TS.STARTED
12-
assert task2.state is TS.STARTED
13-
e.set()
14-
assert task1.state is TS.FINISHED
15-
assert task2.state is TS.FINISHED
9+
t1 = ag.start(e.wait())
10+
t2 = ag.start(e.wait())
11+
assert t1.state is TS.STARTED
12+
assert t2.state is TS.STARTED
13+
e.fire(7, crow='raven')
14+
assert t1.result == ((7, ), {'crow': 'raven', })
15+
assert t2.result == ((7, ), {'crow': 'raven', })
1616

1717

18-
def test_set_then_wait():
18+
def test_fire_then_wait_then_fire():
1919
import asyncgui as ag
2020
from asyncgui_ext.synctools.event import Event
2121
TS = ag.TaskState
2222
e = Event()
23-
e.set()
24-
task1 = ag.start(e.wait())
25-
task2 = ag.start(e.wait())
26-
assert task1.state is TS.FINISHED
27-
assert task2.state is TS.FINISHED
28-
29-
30-
def test_clear():
31-
import asyncgui as ag
32-
from asyncgui_ext.synctools.event import Event
33-
e1 = Event()
34-
e2 = Event()
35-
36-
async def main():
37-
nonlocal task_state
38-
task_state = 'A'
39-
await e1.wait()
40-
task_state = 'B'
41-
await e2.wait()
42-
task_state = 'C'
43-
await e1.wait()
44-
task_state = 'D'
45-
46-
task_state = None
47-
ag.start(main())
48-
assert task_state == 'A'
49-
e1.set()
50-
assert task_state == 'B'
51-
e1.clear()
52-
assert task_state == 'B'
53-
e2.set()
54-
assert task_state == 'C'
55-
e1.set()
56-
assert task_state == 'D'
23+
e.fire(8, crocodile='alligator')
24+
t1 = ag.start(e.wait())
25+
t2 = ag.start(e.wait())
26+
assert t1.state is TS.STARTED
27+
assert t2.state is TS.STARTED
28+
e.fire(7, crow='raven')
29+
assert t1.result == ((7, ), {'crow': 'raven', })
30+
assert t2.result == ((7, ), {'crow': 'raven', })
5731

5832

5933
def test_cancel():
@@ -74,7 +48,7 @@ async def async_fn(ctx, e):
7448
assert task.state is TS.STARTED
7549
ctx['scope'].cancel()
7650
assert task.state is TS.STARTED
77-
e.set()
51+
e.fire()
7852
assert task.state is TS.STARTED
7953
task._step()
8054
assert task.state is TS.FINISHED
@@ -86,7 +60,7 @@ def test_complicated_cancel():
8660
TS = ag.TaskState
8761

8862
async def async_fn_1(ctx, e):
89-
await e.wait()
63+
assert (await e.wait()) == ((7, ), {'crow': 'raven', })
9064
ctx['scope'].cancel()
9165

9266
async def async_fn_2(ctx, e):
@@ -98,13 +72,13 @@ async def async_fn_2(ctx, e):
9872

9973
ctx = {}
10074
e = Event()
101-
task1 = ag.start(async_fn_1(ctx, e))
102-
task2 = ag.start(async_fn_2(ctx, e))
103-
assert e._waiting_tasks == [task1, task2, ]
104-
assert task2.state is TS.STARTED
105-
e.set()
106-
assert task1.state is TS.FINISHED
107-
assert task2.state is TS.STARTED
75+
t1 = ag.start(async_fn_1(ctx, e))
76+
t2 = ag.start(async_fn_2(ctx, e))
77+
assert e._waiting_tasks == [t1, t2, ]
78+
assert t2.state is TS.STARTED
79+
e.fire(7, crow='raven')
80+
assert t1.state is TS.FINISHED
81+
assert t2.state is TS.STARTED
10882
assert e._waiting_tasks == []
109-
task2._step()
110-
assert task2.state is TS.FINISHED
83+
t2._step()
84+
assert t2.result is None

0 commit comments

Comments
 (0)