Skip to content

bpo-32253: Deprecate with statement and bare await for asyncio locks #4764

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 10 commits into from
Dec 9, 2017
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
94 changes: 52 additions & 42 deletions Doc/library/asyncio-sync.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,9 @@ module (:class:`~threading.Lock`, :class:`~threading.Event`,
: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.

Locks
-----

Lock
^^^^
----

.. class:: Lock(\*, loop=None)

Expand All @@ -37,8 +35,9 @@ Lock
particular coroutine when locked. A primitive lock is in one of two states,
'locked' or 'unlocked'.

It is created in the unlocked state. It has two basic methods, :meth:`acquire`
and :meth:`release`. When the state is unlocked, acquire() changes the state to
The lock is created in the unlocked state.
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
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
Expand All @@ -51,38 +50,12 @@ Lock
resets the state to unlocked; first coroutine which is blocked in acquire()
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)``
should be used as the context manager expression.
Locks support the :ref:`context management protocol <async-with-locks>`.

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()

Return ``True`` if the lock is acquired.
Expand Down Expand Up @@ -110,7 +83,7 @@ Lock


Event
^^^^^
-----

.. class:: Event(\*, loop=None)

Expand Down Expand Up @@ -151,7 +124,7 @@ Event


Condition
^^^^^^^^^
---------

.. class:: Condition(lock=None, \*, loop=None)

Expand All @@ -166,6 +139,9 @@ Condition
object, and it is used as the underlying lock. Otherwise,
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>`.

.. coroutinemethod:: acquire()
Expand Down Expand Up @@ -239,11 +215,8 @@ Condition
This method is a :ref:`coroutine <coroutine>`.


Semaphores
----------

Semaphore
^^^^^^^^^
---------

.. class:: Semaphore(value=1, \*, loop=None)

Expand All @@ -254,12 +227,13 @@ Semaphore
counter can never go below zero; when :meth:`acquire` finds that it is zero,
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
defaults to ``1``. If the value given is less than ``0``, :exc:`ValueError`
is raised.

Semaphores support the :ref:`context management protocol
<async-with-locks>`.

This class is :ref:`not thread safe <asyncio-multithreading>`.

.. coroutinemethod:: acquire()
Expand All @@ -285,11 +259,47 @@ Semaphore


BoundedSemaphore
^^^^^^^^^^^^^^^^
----------------

.. class:: BoundedSemaphore(value=1, \*, loop=None)

A bounded semaphore implementation. Inherit from :class:`Semaphore`.

This raises :exc:`ValueError` in :meth:`~Semaphore.release` if it would
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.
7 changes: 7 additions & 0 deletions Lib/asyncio/locks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
__all__ = ['Lock', 'Event', 'Condition', 'Semaphore', 'BoundedSemaphore']

import collections
import warnings

from . import events
from . import futures
Expand Down Expand Up @@ -63,6 +64,9 @@ def __iter__(self):
# <block>
# finally:
# lock.release()
warnings.warn("'with (yield from lock)' is deprecated "
"use 'async with lock' instead",
DeprecationWarning, stacklevel=2)
yield from self.acquire()
return _ContextManager(self)

Expand All @@ -71,6 +75,9 @@ async def __acquire_ctx(self):
return _ContextManager(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.
return self.__acquire_ctx().__await__()

Expand Down
47 changes: 40 additions & 7 deletions Lib/test/test_asyncio/test_locks.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ def test_repr(self):

@asyncio.coroutine
def acquire_lock():
yield from lock
with self.assertWarns(DeprecationWarning):
yield from lock

self.loop.run_until_complete(acquire_lock())
self.assertTrue(repr(lock).endswith('[locked]>'))
Expand All @@ -53,7 +54,8 @@ def test_lock(self):

@asyncio.coroutine
def acquire_lock():
return (yield from lock)
with self.assertWarns(DeprecationWarning):
return (yield from lock)

res = self.loop.run_until_complete(acquire_lock())

Expand All @@ -63,6 +65,32 @@ def acquire_lock():
lock.release()
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):
lock = asyncio.Lock(loop=self.loop)
result = []
Expand Down Expand Up @@ -212,7 +240,8 @@ def test_context_manager(self):

@asyncio.coroutine
def acquire_lock():
return (yield from lock)
with self.assertWarns(DeprecationWarning):
return (yield from lock)

with self.loop.run_until_complete(acquire_lock()):
self.assertTrue(lock.locked())
Expand All @@ -224,7 +253,8 @@ def test_context_manager_cant_reuse(self):

@asyncio.coroutine
def acquire_lock():
return (yield from lock)
with self.assertWarns(DeprecationWarning):
return (yield from lock)

# This spells "yield from lock" outside a generator.
cm = self.loop.run_until_complete(acquire_lock())
Expand Down Expand Up @@ -668,7 +698,8 @@ def test_context_manager(self):

@asyncio.coroutine
def acquire_cond():
return (yield from cond)
with self.assertWarns(DeprecationWarning):
return (yield from cond)

with self.loop.run_until_complete(acquire_cond()):
self.assertTrue(cond.locked())
Expand Down Expand Up @@ -751,7 +782,8 @@ def test_semaphore(self):

@asyncio.coroutine
def acquire_lock():
return (yield from sem)
with self.assertWarns(DeprecationWarning):
return (yield from sem)

res = self.loop.run_until_complete(acquire_lock())

Expand Down Expand Up @@ -893,7 +925,8 @@ def test_context_manager(self):

@asyncio.coroutine
def acquire_lock():
return (yield from sem)
with self.assertWarns(DeprecationWarning):
return (yield from sem)

with self.loop.run_until_complete(acquire_lock()):
self.assertFalse(sem.locked())
Expand Down
13 changes: 7 additions & 6 deletions Lib/test/test_asyncio/test_pep492.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,13 @@ def test_context_manager_with_await(self):
async def test(lock):
await asyncio.sleep(0.01, loop=self.loop)
self.assertFalse(lock.locked())
with await lock as _lock:
self.assertIs(_lock, None)
self.assertTrue(lock.locked())
await asyncio.sleep(0.01, loop=self.loop)
self.assertTrue(lock.locked())
self.assertFalse(lock.locked())
with self.assertWarns(DeprecationWarning):
with await lock as _lock:
self.assertIs(_lock, None)
self.assertTrue(lock.locked())
await asyncio.sleep(0.01, loop=self.loop)
self.assertTrue(lock.locked())
self.assertFalse(lock.locked())

for primitive in primitives:
self.loop.run_until_complete(test(primitive))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Deprecate ``yield from lock``, ``await lock``, ``with (yield from lock)``
and ``with await lock`` for asyncio synchronization primitives.