bpo-32253: Deprecate with statement and bare await for asyncio locks (GH-4764)

* Add test for 'with (yield from lock)'
* Deprecate with statement for asyncio locks
* Document the deprecation
This commit is contained in:
Andrew Svetlov 2017-12-09 20:00:05 +02:00 committed by GitHub
parent a9f8df646a
commit 28d8d14013
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 108 additions and 55 deletions

View file

@ -23,11 +23,9 @@ module (:class:`~threading.Lock`, :class:`~threading.Event`,
:class:`~threading.BoundedSemaphore`), but it has no *timeout* parameter. The :class:`~threading.BoundedSemaphore`), but it has no *timeout* parameter. The
:func:`asyncio.wait_for` function can be used to cancel a task after a timeout. :func:`asyncio.wait_for` function can be used to cancel a task after a timeout.
Locks
-----
Lock Lock
^^^^ ----
.. class:: Lock(\*, loop=None) .. class:: Lock(\*, loop=None)
@ -37,8 +35,9 @@ Lock
particular coroutine when locked. A primitive lock is in one of two states, particular coroutine when locked. A primitive lock is in one of two states,
'locked' or 'unlocked'. 'locked' or 'unlocked'.
It is created in the unlocked state. It has two basic methods, :meth:`acquire` The lock is created in the unlocked state.
and :meth:`release`. When the state is unlocked, acquire() changes the state to It has two basic methods, :meth:`acquire` and :meth:`release`.
When the state is unlocked, acquire() changes the state to
locked and returns immediately. When the state is locked, acquire() blocks locked and returns immediately. When the state is locked, acquire() blocks
until a call to release() in another coroutine changes it to unlocked, then until a call to release() in another coroutine changes it to unlocked, then
the acquire() call resets it to locked and returns. The release() method the acquire() call resets it to locked and returns. The release() method
@ -51,38 +50,12 @@ Lock
resets the state to unlocked; first coroutine which is blocked in acquire() resets the state to unlocked; first coroutine which is blocked in acquire()
is being processed. is being processed.
:meth:`acquire` is a coroutine and should be called with ``yield from``. :meth:`acquire` is a coroutine and should be called with ``await``.
Locks also support the context management protocol. ``(yield from lock)`` Locks support the :ref:`context management protocol <async-with-locks>`.
should be used as the context manager expression.
This class is :ref:`not thread safe <asyncio-multithreading>`. This class is :ref:`not thread safe <asyncio-multithreading>`.
Usage::
lock = Lock()
...
yield from lock
try:
...
finally:
lock.release()
Context manager usage::
lock = Lock()
...
with (yield from lock):
...
Lock objects can be tested for locking state::
if not lock.locked():
yield from lock
else:
# lock is acquired
...
.. method:: locked() .. method:: locked()
Return ``True`` if the lock is acquired. Return ``True`` if the lock is acquired.
@ -110,7 +83,7 @@ Lock
Event Event
^^^^^ -----
.. class:: Event(\*, loop=None) .. class:: Event(\*, loop=None)
@ -151,7 +124,7 @@ Event
Condition Condition
^^^^^^^^^ ---------
.. class:: Condition(lock=None, \*, loop=None) .. class:: Condition(lock=None, \*, loop=None)
@ -166,6 +139,9 @@ Condition
object, and it is used as the underlying lock. Otherwise, object, and it is used as the underlying lock. Otherwise,
a new :class:`Lock` object is created and used as the underlying lock. a new :class:`Lock` object is created and used as the underlying lock.
Conditions support the :ref:`context management protocol
<async-with-locks>`.
This class is :ref:`not thread safe <asyncio-multithreading>`. This class is :ref:`not thread safe <asyncio-multithreading>`.
.. coroutinemethod:: acquire() .. coroutinemethod:: acquire()
@ -239,11 +215,8 @@ Condition
This method is a :ref:`coroutine <coroutine>`. This method is a :ref:`coroutine <coroutine>`.
Semaphores
----------
Semaphore Semaphore
^^^^^^^^^ ---------
.. class:: Semaphore(value=1, \*, loop=None) .. class:: Semaphore(value=1, \*, loop=None)
@ -254,12 +227,13 @@ Semaphore
counter can never go below zero; when :meth:`acquire` finds that it is zero, counter can never go below zero; when :meth:`acquire` finds that it is zero,
it blocks, waiting until some other coroutine calls :meth:`release`. it blocks, waiting until some other coroutine calls :meth:`release`.
Semaphores also support the context management protocol.
The optional argument gives the initial value for the internal counter; it The optional argument gives the initial value for the internal counter; it
defaults to ``1``. If the value given is less than ``0``, :exc:`ValueError` defaults to ``1``. If the value given is less than ``0``, :exc:`ValueError`
is raised. is raised.
Semaphores support the :ref:`context management protocol
<async-with-locks>`.
This class is :ref:`not thread safe <asyncio-multithreading>`. This class is :ref:`not thread safe <asyncio-multithreading>`.
.. coroutinemethod:: acquire() .. coroutinemethod:: acquire()
@ -285,7 +259,7 @@ Semaphore
BoundedSemaphore BoundedSemaphore
^^^^^^^^^^^^^^^^ ----------------
.. class:: BoundedSemaphore(value=1, \*, loop=None) .. class:: BoundedSemaphore(value=1, \*, loop=None)
@ -293,3 +267,39 @@ BoundedSemaphore
This raises :exc:`ValueError` in :meth:`~Semaphore.release` if it would This raises :exc:`ValueError` in :meth:`~Semaphore.release` if it would
increase the value above the initial value. increase the value above the initial value.
Bounded semapthores support the :ref:`context management
protocol <async-with-locks>`.
This class is :ref:`not thread safe <asyncio-multithreading>`.
.. _async-with-locks:
Using locks, conditions and semaphores in the :keyword:`async with` statement
-----------------------------------------------------------------------------
:class:`Lock`, :class:`Condition`, :class:`Semaphore`, and
:class:`BoundedSemaphore` objects can be used in :keyword:`async with`
statements.
The :meth:`acquire` method will be called when the block is entered,
and :meth:`release` will be called when the block is exited. Hence,
the following snippet::
async with lock:
# do something...
is equivalent to::
await lock.acquire()
try:
# do something...
finally:
lock.release()
.. deprecated:: 3.7
Lock acquiring using ``await lock`` or ``yield from lock`` and
:keyword:`with` statement (``with await lock``, ``with (yield from
lock)``) are deprecated.

View file

@ -3,6 +3,7 @@
__all__ = ['Lock', 'Event', 'Condition', 'Semaphore', 'BoundedSemaphore'] __all__ = ['Lock', 'Event', 'Condition', 'Semaphore', 'BoundedSemaphore']
import collections import collections
import warnings
from . import events from . import events
from . import futures from . import futures
@ -63,6 +64,9 @@ class _ContextManagerMixin:
# <block> # <block>
# finally: # finally:
# lock.release() # lock.release()
warnings.warn("'with (yield from lock)' is deprecated "
"use 'async with lock' instead",
DeprecationWarning, stacklevel=2)
yield from self.acquire() yield from self.acquire()
return _ContextManager(self) return _ContextManager(self)
@ -71,6 +75,9 @@ class _ContextManagerMixin:
return _ContextManager(self) return _ContextManager(self)
def __await__(self): def __await__(self):
warnings.warn("'with await lock' is deprecated "
"use 'async with lock' instead",
DeprecationWarning, stacklevel=2)
# To make "with await lock" work. # To make "with await lock" work.
return self.__acquire_ctx().__await__() return self.__acquire_ctx().__await__()

View file

@ -42,6 +42,7 @@ class LockTests(test_utils.TestCase):
@asyncio.coroutine @asyncio.coroutine
def acquire_lock(): def acquire_lock():
with self.assertWarns(DeprecationWarning):
yield from lock yield from lock
self.loop.run_until_complete(acquire_lock()) self.loop.run_until_complete(acquire_lock())
@ -53,6 +54,7 @@ class LockTests(test_utils.TestCase):
@asyncio.coroutine @asyncio.coroutine
def acquire_lock(): def acquire_lock():
with self.assertWarns(DeprecationWarning):
return (yield from lock) return (yield from lock)
res = self.loop.run_until_complete(acquire_lock()) res = self.loop.run_until_complete(acquire_lock())
@ -63,6 +65,32 @@ class LockTests(test_utils.TestCase):
lock.release() lock.release()
self.assertFalse(lock.locked()) self.assertFalse(lock.locked())
def test_lock_by_with_statement(self):
loop = asyncio.new_event_loop() # don't use TestLoop quirks
self.set_event_loop(loop)
primitives = [
asyncio.Lock(loop=loop),
asyncio.Condition(loop=loop),
asyncio.Semaphore(loop=loop),
asyncio.BoundedSemaphore(loop=loop),
]
@asyncio.coroutine
def test(lock):
yield from asyncio.sleep(0.01, loop=loop)
self.assertFalse(lock.locked())
with self.assertWarns(DeprecationWarning):
with (yield from lock) as _lock:
self.assertIs(_lock, None)
self.assertTrue(lock.locked())
yield from asyncio.sleep(0.01, loop=loop)
self.assertTrue(lock.locked())
self.assertFalse(lock.locked())
for primitive in primitives:
loop.run_until_complete(test(primitive))
self.assertFalse(primitive.locked())
def test_acquire(self): def test_acquire(self):
lock = asyncio.Lock(loop=self.loop) lock = asyncio.Lock(loop=self.loop)
result = [] result = []
@ -212,6 +240,7 @@ class LockTests(test_utils.TestCase):
@asyncio.coroutine @asyncio.coroutine
def acquire_lock(): def acquire_lock():
with self.assertWarns(DeprecationWarning):
return (yield from lock) return (yield from lock)
with self.loop.run_until_complete(acquire_lock()): with self.loop.run_until_complete(acquire_lock()):
@ -224,6 +253,7 @@ class LockTests(test_utils.TestCase):
@asyncio.coroutine @asyncio.coroutine
def acquire_lock(): def acquire_lock():
with self.assertWarns(DeprecationWarning):
return (yield from lock) return (yield from lock)
# This spells "yield from lock" outside a generator. # This spells "yield from lock" outside a generator.
@ -668,6 +698,7 @@ class ConditionTests(test_utils.TestCase):
@asyncio.coroutine @asyncio.coroutine
def acquire_cond(): def acquire_cond():
with self.assertWarns(DeprecationWarning):
return (yield from cond) return (yield from cond)
with self.loop.run_until_complete(acquire_cond()): with self.loop.run_until_complete(acquire_cond()):
@ -751,6 +782,7 @@ class SemaphoreTests(test_utils.TestCase):
@asyncio.coroutine @asyncio.coroutine
def acquire_lock(): def acquire_lock():
with self.assertWarns(DeprecationWarning):
return (yield from sem) return (yield from sem)
res = self.loop.run_until_complete(acquire_lock()) res = self.loop.run_until_complete(acquire_lock())
@ -893,6 +925,7 @@ class SemaphoreTests(test_utils.TestCase):
@asyncio.coroutine @asyncio.coroutine
def acquire_lock(): def acquire_lock():
with self.assertWarns(DeprecationWarning):
return (yield from sem) return (yield from sem)
with self.loop.run_until_complete(acquire_lock()): with self.loop.run_until_complete(acquire_lock()):

View file

@ -59,6 +59,7 @@ class LockTests(BaseTest):
async def test(lock): async def test(lock):
await asyncio.sleep(0.01, loop=self.loop) await asyncio.sleep(0.01, loop=self.loop)
self.assertFalse(lock.locked()) self.assertFalse(lock.locked())
with self.assertWarns(DeprecationWarning):
with await lock as _lock: with await lock as _lock:
self.assertIs(_lock, None) self.assertIs(_lock, None)
self.assertTrue(lock.locked()) self.assertTrue(lock.locked())

View file

@ -0,0 +1,2 @@
Deprecate ``yield from lock``, ``await lock``, ``with (yield from lock)``
and ``with await lock`` for asyncio synchronization primitives.