Skip to content

Commit 4f43a90

Browse files
authored
Merge pull request #2 from nocarryr/aio-lock
Add async context management to emission lock
2 parents 1e2e1af + 971f5b8 commit 4f43a90

File tree

6 files changed

+202
-5
lines changed

6 files changed

+202
-5
lines changed

.travis.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ language: python
22
matrix:
33
include:
44
- python: "2.7"
5-
env: ALLOW_DEPLOY=false
5+
env:
6+
- ALLOW_DEPLOY=false
7+
- AIO_AVAILABLE=false
68
- python: "3.4"
7-
env: ALLOW_DEPLOY=false
9+
env:
10+
- ALLOW_DEPLOY=false
11+
- AIO_AVAILABLE=false
812
- python: "3.5"
9-
env: ALLOW_DEPLOY=true
13+
env:
14+
- ALLOW_DEPLOY=true
15+
- AIO_AVAILABLE=true
1016
addons:
1117
apt:
1218
packages:
@@ -15,6 +21,7 @@ install:
1521
- pip install -U pip setuptools wheel
1622
- sh -c "if [ '$ALLOW_DEPLOY' = 'true' ]; then pip install -r doc/requirements.txt & pip install travis-sphinx; fi"
1723
- pip install -U pytest pytest-cov coveralls pypandoc
24+
- sh -c "if [ '$AIO_AVAILABLE' = 'true' ]; then pip install -U pytest-asyncio; fi"
1825
- pip install -e .
1926
script:
2027
- sh -c "if [ '$ALLOW_DEPLOY' = 'true' ]; then travis-sphinx --source=doc/source --nowarn build; fi"

pydispatch/aioutils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import asyncio
2+
3+
class AioEmissionHoldLock(object):
4+
@property
5+
def aio_lock(self):
6+
l = getattr(self, '_aio_lock', None)
7+
if l is None:
8+
l = self._aio_lock = asyncio.Lock()
9+
return l
10+
async def __aenter__(self):
11+
await self.aio_lock.acquire()
12+
self.acquire()
13+
return self
14+
async def __aexit__(self, *args):
15+
self.aio_lock.release()
16+
self.release()

pydispatch/dispatch.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,16 @@ def on_my_event(value):
184184
name (str): The name of the :class:`Event` or :class:`Property`
185185
186186
Returns:
187-
A context manager to be used by the ``with`` statement
187+
A context manager to be used by the ``with`` statement.
188+
189+
If available, this will also be an async context manager to be used
190+
with the ``async with`` statement (see `PEP 492`_).
188191
189192
Note:
190193
The context manager is re-entrant, meaning that multiple calls to
191194
this method within nested context scopes are possible.
192195
196+
.. _PEP 492: https://www.python.org/dev/peps/pep-0492/#asynchronous-context-managers-and-async-with
193197
"""
194198
e = self.__property_events.get(name)
195199
if e is None:

pydispatch/utils.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
if not PY2:
77
basestring = str
88

9+
AIO_AVAILABLE = sys.version_info >= (3, 5)
10+
911
def get_method_vars(m):
1012
if PY2:
1113
f = m.im_func
@@ -70,7 +72,7 @@ def __init__(self, **kwargs):
7072
def _data_del_callback(self, key):
7173
self.del_callback(key)
7274

73-
class EmissionHoldLock(object):
75+
class EmissionHoldLock_(object):
7476
def __init__(self, event_instance):
7577
self.event_instance = event_instance
7678
self.last_event = None
@@ -93,3 +95,10 @@ def __enter__(self):
9395
return self
9496
def __exit__(self, *args):
9597
self.release()
98+
99+
if AIO_AVAILABLE:
100+
from pydispatch.aioutils import AioEmissionHoldLock
101+
class EmissionHoldLock(EmissionHoldLock_, AioEmissionHoldLock):
102+
pass
103+
else:
104+
EmissionHoldLock = EmissionHoldLock_

tests/conftest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
import sys
12
import pytest
23

4+
AIO_AVAILABLE = sys.version_info >= (3, 5)
5+
6+
collect_ignore = []
7+
if not AIO_AVAILABLE:
8+
collect_ignore.append('test_aio_lock.py')
39

410
@pytest.fixture
511
def listener():
@@ -9,6 +15,7 @@ def __init__(self):
915
self.received_event_data = []
1016
self.property_events = []
1117
self.property_event_kwargs = []
18+
self.property_event_map = {}
1219
def on_event(self, *args, **kwargs):
1320
self.received_event_data.append({'args':args, 'kwargs':kwargs})
1421
name = kwargs.get('triggered_event')
@@ -17,6 +24,10 @@ def on_event(self, *args, **kwargs):
1724
def on_prop(self, obj, value, **kwargs):
1825
self.property_events.append(value)
1926
self.property_event_kwargs.append(kwargs)
27+
prop = kwargs['property']
28+
if prop.name not in self.property_event_map:
29+
self.property_event_map[prop.name] = []
30+
self.property_event_map[prop.name].append({'value':value, 'kwargs':kwargs})
2031

2132
return Listener()
2233

tests/test_aio_lock.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import asyncio
2+
import pytest
3+
4+
@pytest.mark.asyncio
5+
async def test_aio_event_lock(listener, sender):
6+
sender.register_event('on_test')
7+
sender.bind(on_test=listener.on_event)
8+
9+
letters = 'abcdefghijkl'
10+
11+
sender.emit('on_test', letters[0], emit_count=0)
12+
assert len(listener.received_event_data) == 1
13+
listener.received_event_data = []
14+
15+
async with sender.emission_lock('on_test') as elock:
16+
assert elock.aio_lock.locked()
17+
for i in range(len(letters)):
18+
sender.emit('on_test', letters[i], emit_count=i)
19+
assert not elock.aio_lock.locked()
20+
21+
assert len(listener.received_event_data) == 1
22+
e = listener.received_event_data[0]
23+
assert e['args'] == (letters[i], )
24+
assert e['kwargs']['emit_count'] == i
25+
26+
listener.received_event_data = []
27+
28+
async def do_emit(i):
29+
async with sender.emission_lock('on_test') as elock:
30+
assert elock.aio_lock.locked()
31+
sender.emit('on_test', i, 'first')
32+
sender.emit('on_test', i, 'second')
33+
assert not elock.aio_lock.locked()
34+
35+
tx_indecies = [i for i in range(8)]
36+
coros = [asyncio.ensure_future(do_emit(i)) for i in tx_indecies]
37+
await asyncio.wait(coros)
38+
39+
assert len(listener.received_event_data) == len(coros)
40+
41+
rx_indecies = set()
42+
for edata in listener.received_event_data:
43+
i, s = edata['args']
44+
assert i not in rx_indecies
45+
assert s == 'second'
46+
rx_indecies.add(i)
47+
48+
assert rx_indecies == set(tx_indecies)
49+
50+
@pytest.mark.asyncio
51+
async def test_aio_property_lock(listener):
52+
from pydispatch import Dispatcher, Property
53+
from pydispatch.properties import ListProperty, DictProperty
54+
55+
class A(Dispatcher):
56+
test_prop = Property()
57+
test_dict = DictProperty()
58+
test_list = ListProperty()
59+
60+
a = A()
61+
a.test_list = [-1] * 4
62+
a.test_dict = {'a':0, 'b':1, 'c':2, 'd':3}
63+
a.bind(test_prop=listener.on_prop, test_list=listener.on_prop, test_dict=listener.on_prop)
64+
65+
async with a.emission_lock('test_prop') as elock:
66+
assert elock.aio_lock.locked()
67+
for i in range(4):
68+
a.test_prop = i
69+
assert not elock.aio_lock.locked()
70+
assert len(listener.property_events) == 1
71+
assert listener.property_event_kwargs[0]['property'].name == 'test_prop'
72+
assert listener.property_events[0] == i
73+
74+
listener.property_events = []
75+
listener.property_event_kwargs = []
76+
77+
async with a.emission_lock('test_list'):
78+
a.test_prop = 'foo'
79+
for i in range(4):
80+
a.test_list = [i] * 4
81+
assert len(listener.property_events) == 2
82+
assert listener.property_event_kwargs[0]['property'].name == 'test_prop'
83+
assert listener.property_events[0] == 'foo'
84+
assert listener.property_event_kwargs[1]['property'].name == 'test_list'
85+
assert listener.property_events[1] == [i] * 4
86+
87+
listener.property_events = []
88+
listener.property_event_kwargs = []
89+
90+
async with a.emission_lock('test_dict'):
91+
a.test_prop = 'bar'
92+
a.test_list[0] = 'a'
93+
for i in range(4):
94+
for key in a.test_dict.keys():
95+
a.test_dict[key] = i
96+
assert len(listener.property_events) == 3
97+
assert listener.property_event_kwargs[0]['property'].name == 'test_prop'
98+
assert listener.property_events[0] == 'bar'
99+
assert listener.property_event_kwargs[1]['property'].name == 'test_list'
100+
assert listener.property_events[1][0] == 'a'
101+
assert listener.property_event_kwargs[2]['property'].name == 'test_dict'
102+
assert listener.property_events[2] == {k:i for k in a.test_dict.keys()}
103+
104+
listener.property_events = []
105+
listener.property_event_kwargs = []
106+
107+
async with a.emission_lock('test_prop'):
108+
async with a.emission_lock('test_list'):
109+
async with a.emission_lock('test_dict'):
110+
for i in range(4):
111+
a.test_prop = i
112+
a.test_list[0] = i
113+
a.test_dict[i] = 'foo'
114+
assert len(listener.property_events) == 3
115+
assert listener.property_event_kwargs[0]['property'].name == 'test_dict'
116+
for k in range(4):
117+
assert listener.property_events[0][k] == 'foo'
118+
assert listener.property_event_kwargs[1]['property'].name == 'test_list'
119+
assert listener.property_events[1][0] == i
120+
assert listener.property_event_kwargs[2]['property'].name == 'test_prop'
121+
assert listener.property_events[2] == i
122+
123+
124+
listener.property_event_map.clear()
125+
126+
async def set_property(prop_name, *values):
127+
async with a.emission_lock(prop_name):
128+
for value in values:
129+
setattr(a, prop_name, value)
130+
131+
prop_vals = {
132+
'test_prop':[None, 0, 1, 2],
133+
'test_list':[[None]*4, [0]*4, [0, 1, 2, 3], ['a', 'b', 'c', 'd']],
134+
'test_dict':[
135+
{k:None for k in a.test_dict.keys()},
136+
{k:0 for k in a.test_dict.keys()},
137+
{k:v for k, v in zip(a.test_dict.keys(), range(4))},
138+
{k:v for k, v in zip(a.test_dict.keys(), ['a', 'b', 'c', 'd'])},
139+
],
140+
}
141+
142+
coros = []
143+
for prop_name, vals in prop_vals.items():
144+
coros.append(asyncio.ensure_future(set_property(prop_name, *vals)))
145+
await asyncio.wait(coros)
146+
147+
for prop_name, vals in prop_vals.items():
148+
event_list = listener.property_event_map[prop_name]
149+
assert len(event_list) == 1
150+
assert event_list[0]['value'] == vals[-1] == getattr(a, prop_name)

0 commit comments

Comments
 (0)