8000 Locks improvements by Arnaud Faure: better repr(), change Condition s… · Python-Repository-Hub/asyncio@65abd4a · GitHub
[go: up one dir, main page]

Skip to content 8000

Commit 65abd4a

Browse files
committed
Locks improvements by Arnaud Faure: better repr(), change Condition structure.
1 parent a4f2204 commit 65abd4a

File tree

2 files changed

+124
-25
lines change 10000 d

2 files changed

+124
-25
lines changed

asyncio/locks.py

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,11 @@ def __init__(self, *, loop=None):
155155
self._loop = events.get_event_loop()
156156

157157
def __repr__(self):
158-
# TODO: add waiters:N if > 0.
159158
res = super().__repr__()
160-
return '<{} [{}]>'.format(res[1:-1], 'set' if self._value else 'unset')
159+
extra = 'set' if self._value else 'unset'
160+
if self._waiters:
161+
extra = '{},waiters:{}'.format(extra, len(self._waiters))
162+
return '<{} [{}]>'.format(res[1:-1], extra)
161163

162164
def is_set(self):
163165
"""Return true if and only if the internal flag is true."""
@@ -201,20 +203,38 @@ def wait(self):
201203
self._waiters.remove(fut)
202204

203205

204-
# TODO: Why is this a Lock subclass? threading.Condition *has* a lock.
205-
class Condition(Lock):
206-
"""A Condition implementation.
206+
class Condition:
207+
"""A Condition implementation, our equivalent to threading.Condition.
207208
208209
This class implements condition variable objects. A condition variable
209210
allows one or more coroutines to wait until they are notified by another
210211
coroutine.
212+
213+
A new Lock object is created and used as the underlying lock.
211214
"""
212215

213216
def __init__(self, *, loop=None):
214-
super().__init__(loop=loop)
215-
self._condition_waiters = collections.deque()
217+
if loop is not None:
218+
self._loop = loop
219+
else:
220+
self._loop = events.get_event_loop()
216221

217-
# TODO: Add __repr__() with len(_condition_waiters).
222+
# Lock as an attribute as in threading.Condition.
223+
lock = Lock(loop=self._loop)
224+
self._lock = lock
225+
# Export the lock's locked(), acquire() and release() methods.
226+
self.locked = lock.locked
227+
self.acquire = lock.acquire
228+
self.release = lock.release
229+
230+
self._waiters = collections.deque()
231+
232+
def __repr__(self):
233+
res = super().__repr__()
234+
extra = 'locked' if self.locked() else 'unlocked'
235+
if self._waiters:
236+
extra = '{},waiters:{}'.format(extra, len(self._waiters))
237+
return '<{} [{}]>'.format(res[1:-1], extra)
218238

219239
@tasks.coroutine
220240
def wait(self):
@@ -228,19 +248,19 @@ def wait(self):
228248
the same condition variable in another coroutine. Once
229249
awakened, it re-acquires the lock and returns True.
230250
"""
231-
if not self._locked:
251+
if not self.locked():
232252
raise RuntimeError('cannot wait on un-acquired lock')
233253

234254
keep_lock = True
235255
self.release()
236256
try:
237257
fut = futures.Future(loop=self._loop)
238-
self._condition_waiters.append(fut)
258+
self._waiters.append(fut)
239259
try:
240260
yield from fut
241261
return True
242262
finally:
243-
self._condition_waiters.remove(fut)
263+
self._waiters.remove(fut)
244264

245265
except GeneratorExit:
246266
keep_lock = False # Prevent yield in finally clause.
@@ -275,11 +295,11 @@ def notify(self, n=1):
275295
wait() call until it can reacquire the lock. Since notify() does
276296
not release the lock, its caller should.
277297
"""
278-
if not self._locked:
298+
if not self.locked():
279299
raise RuntimeError('cannot notify on un-acquired lock')
280300

281301
idx = 0
282-
for fut in self._condition_waiters:
302+
for fut in self._waiters:
283303
if idx >= n:
284304
break
285305

@@ -293,7 +313,17 @@ def notify_all(self):
293313
calling thread has not acquired the lock when this method is called,
294314
a RuntimeError is raised.
295315
"""
296-
self.notify(len(self._condition_waiters))
316+
self.notify(len(self._waiters))
317+
318+
def __enter__(self):
319+
return self._lock.__enter__()
320+
321+
def __exit__(self, *args):
322+
return self._lock.__exit__(*args)
323+
324+
def __iter__(self):
325+
yield from self.acquire()
326+
return self
297327

298328

299329
class Semaphore:
@@ -310,10 +340,10 @@ class Semaphore:
310340
counter; it defaults to 1. If the value given is less than 0,
311341
ValueError is raised.
312342
313-
The second optional argument determins can semophore be released more than
314-
initial internal counter value; it defaults to False. If the value given
315-
is True and number of release() is more than number of successfull
316-
acquire() calls ValueError is raised.
343+
The second optional argument determines if the semaphore can be released
344+
more than initial internal counter value; it defaults to False. If the
345+
value given is True and number of release() is more than number of
346+
successful acquire() calls ValueError is raised.
317347
"""
318348

319349
def __init__(self, value=1, bound=False, *, loop=None):
@@ -330,12 +360,12 @@ def __init__(self, value=1, bound=False, *, loop=None):
330360
self._loop = events.get_event_loop()
331361

332362
def __repr__(self):
333-
# TODO: add waiters:N if > 0.
334363
res = super().__repr__()
335-
return '<{} [{}]>'.format(
336-
res[1:-1],
337-
'locked' if self._locked else 'unlocked,value:{}'.format(
338-
self._value))
364+
extra = 'locked' if self._locked else 'unlocked,value:{}'.format(
365+
self._value)
366+
if self._waiters:
367+
extra = '{},waiters:{}'.format(extra, len(self._waiters))
368+
return '<{} [{}]>'.format(res[1:-1], extra)
339369

340370
def locked(self):
341371
"""Returns True if semaphore can not be acquired immediately."""
@@ -373,7 +403,7 @@ def release(self):
373403
When it was zero on entry and another coroutine is waiting for it to
374404
become larger than zero again, wake up that coroutine.
375405
376-
If Semaphore is create with "bound" paramter equals true, then
406+
If Semaphore is created with "bound" parameter equals true, then
377407
release() method checks to make sure its current value doesn't exceed
378408
its initial value. If it does, ValueError is raised.
379409
"""

tests/test_locks.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import unittest
44
import unittest.mock
5+
import re
56

67
from asyncio import events
78
from asyncio import futures
@@ -10,6 +11,15 @@
1011
from asyncio import test_utils
1112

1213

14+
STR_RGX_REPR = (
15+
r'^<(?P<class>.*?) object at (?P<address>.*?)'
16+
r'\[(?P<extras>'
17+
r'(set|unset|locked|unlocked)(,value:\d)?(,waiters:\d+)?'
18+
r')\]>\Z'
19+
)
20+
RGX_REPR = re.compile(STR_RGX_REPR)
21+
22+
1323
class LockTests(unittest.TestCase):
1424

1525
def setUp(self):
@@ -38,13 +48,15 @@ def test_ctor_noloop(self):
3848
def test_repr(self):
3949
lock = locks.Lock(loop=self.loop)
4050
self.assertTrue(repr(lock).endswith('[unlocked]>'))
51+
self.assertTrue(RGX_REPR.match(repr(lock)))
4152

4253
@tasks.coroutine
4354
def acquire_lock():
4455
yield from lock
4556

4657
self.loop.run_until_complete(acquire_lock())
4758
self.assertTrue(repr(lock).endswith('[locked]>'))
59+
self.assertTrue(RGX_REPR.match(repr(lock)))
4860

4961
def test_lock(self):
5062
lock = locks.Lock(loop=self.loop)
@@ -239,9 +251,16 @@ def test_ctor_noloop(self):
239251
def test_repr(self):
240252
ev = locks.Event(loop=self.loop)
241253
self.assertTrue(repr(ev).endswith('[unset]>'))
254+
match = RGX_REPR.match(repr(ev))
255+
self.assertEqual(match.group('extras'), 'unset')
242256

243257
ev.set()
244258
self.assertTrue(repr(ev).endswith('[set]>'))
259+
self.assertTrue(RGX_REPR.match(repr(ev)))
260+
261+
ev._waiters.append(unittest.mock.Mock())
262+
self.assertTrue('waiters:1' in repr(ev))
263+
self.assertTrue(RGX_REPR.match(repr(ev)))
245264

246265
def test_wait(self):
247266
ev = locks.Event(loop=self.loop)
@@ -440,7 +459,7 @@ def test_wait_cancel(self):
440459
self.assertRaises(
441460
futures.CancelledError,
442461
self.loop.run_until_complete, wait)
443-
self.assertFalse(cond._condition_waiters)
462+
self.assertFalse(cond._waiters)
444463
self.assertTrue(cond.locked())
445464

446465
def test_wait_unacquired(self):
@@ -600,6 +619,45 @@ def test_notify_all_unacquired(self):
600619
cond = locks.Condition(loop=self.loop)
601620
self.assertRaises(RuntimeError, cond.notify_all)
602621

622+
def test_repr(self):
623+
cond = locks.Condition(loop=self.loop)
624+
self.assertTrue('unlocked' in repr(cond))
625+
self.assertTrue(RGX_REPR.match(repr(cond)))
626+
627+
self.loop.run_until_complete(cond.acquire())
628+
self.assertTrue('locked' in repr(cond))
629+
630+
cond._waiters.append(unittest.mock.Mock())
631+
self.assertTrue('waiters:1' in repr(cond))
632+
self.assertTrue(RGX_REPR.match(repr(cond)))
633+
634+
cond._waiters.append(unittest.mock.Mock())
635+
self.assertTrue('waiters:2' in repr(cond))
636+
self.assertTrue(RGX_REPR.match(repr(cond)))
637+
638+
def test_context_manager(self):
639+
cond = locks.Condition(loop=self.loop)
640+
641+
@tasks.coroutine
642+
def acquire_cond():
643+
return (yield from cond)
644+
645+
with self.loop.run_until_complete(acquire_cond()):
646+
self.assertTrue(cond.locked())
647+
648+
self.assertFalse(cond.locked())
649+
650+
def test_context_manager_no_yield(self):
651+
cond = locks.Condition(loop=self.loop)
652+
653+
try:
654+
with cond:
655+
self.fail('RuntimeError is not raised in with expression')
656+
except RuntimeError as err:
657+
self.assertEqual(
658+
str(err),
659+
'"yield from" should be used as context manager expression')
660+
603661

604662
class SemaphoreTests(unittest.TestCase):
605663

@@ -629,9 +687,20 @@ def test_ctor_noloop(self):
629687
def test_repr(self):
630688
sem = locks.Semaphore(loop=self.loop)
631689
self.assertTrue(repr(sem).endswith('[unlocked,value:1]>'))
690+
self.assertTrue(RGX_REPR.match(repr(sem)))
632691

633692
self.loop.run_until_complete(sem.acquire())
634693
self.assertTrue(repr(sem).endswith('[locked]>'))
694+
self.assertTrue('waiters' not in repr(sem))
695+
self.assertTrue(RGX_REPR.match(repr(sem)))
696+
697+
sem._waiters.append(unittest.mock.Mock())
698+
self.assertTrue('waiters:1' in repr(sem))
699+
self.assertTrue(RGX_REPR.match(repr(sem)))
700+
701+
sem._waiters.append(unittest.mock.Mock())
702+
self.assertTrue('waiters:2' in repr(sem))
703+
self.assertTrue(RGX_REPR.match(repr(sem)))
635704

636705
def test_semaphore(self):
637706
sem = locks.Semaphore(loop=self.loop)

0 commit comments

Comments
 (0)
0