8000 gh-95601: restore support for awaitable objects that are not futures in `asyncio.wait` by graingert · Pull Request #95708 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-95601: restore support for awaitable objects that are not futures in asyncio.wait #95708

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

Closed
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
88580c9
gh-95601: forbid non-futures in asyncio.wait
graingert Aug 5, 2022
1363c9b
📜🤖 Added by blurb_it.
blurb-it[bot] Aug 5, 2022
543c4f2
Merge branch 'main' into forbid-non-futures-in-asyncio-wait
graingert Aug 5, 2022
6661052
clarify awaitable-not-future
graingert Aug 5, 2022
5b740df
update asyncio.wait deprecation notice
graingert Aug 5, 2022
5b974c5
Update Doc/library/asyncio-task.rst
graingert Aug 5, 2022
c4a7237
Merge branch 'main' into forbid-non-futures-in-asyncio-wait
graingert Aug 5, 2022
d4dac27
Merge branch 'main' into forbid-non-futures-in-asyncio-wait
graingert Aug 9, 2022
cb00279
Apply suggestions from code review
graingert Aug 9, 2022
38fb812
Update Misc/NEWS.d/next/Library/2022-08-05-11-16-33.gh-issue-95601.hJ…
graingert Aug 9, 2022
5383116
Update Lib/test/test_asyncio/test_tasks.py
graingert Aug 9, 2022
b6ea455
upgrade WaitTests to an IsolatedAsyncioTestCase
graingert Aug 9, 2022
980a3b9
Update Lib/test/test_asyncio/test_tasks.py
graingert Aug 9, 2022
d8354db
Update Lib/test/test_asyncio/test_tasks.py
graingert Aug 9, 2022
5f1d061
Update Lib/test/test_asyncio/test_tasks.py
graingert Aug 9, 2022
9181e25
Merge branch 'main' into forbid-non-futures-in-asyncio-wait
graingert Aug 11, 2022
f973c52
test that asyncio.wait gives the right type error for non-awaitable n…
graingert Aug 12, 2022
0430145
Merge branch 'main' into forbid-non-futures-in-asyncio-wait
graingert Aug 12, 2022
9189603
Merge branch 'main' into forbid-non-futures-in-asyncio-wait
graingert Sep 12, 2022
9c325b4
don't deprecate Awaitables in wait yet
graingert Sep 12, 2022
da9f2a2
Merge branch 'main' into forbid-non-futures-in-asyncio-wait
graingert Sep 12, 2022
7e35d9c
remove deprecation docs
graingert Sep 12, 2022
a5c5f4a
remove deprecation assertions from tests for asyncio.wait
graingert Sep 13, 2022
17cbfc9
Update Misc/NEWS.d/next/Library/2022-08-05-11-16-33.gh-issue-95601.hJ…
graingert Sep 13, 2022
69b2a0d
Merge branch 'main' into forbid-non-futures-in-asyncio-wait
graingert Sep 13, 2022
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
4 changes: 4 additions & 0 deletions Doc/library/asyncio-task.rst
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,10 @@ Waiting Primitives
.. versionchanged:: 3.11
Passing coroutine objects to ``wait()`` directly is forbidden.

.. deprecated-removed:: 3.11 3.14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot deprecate behaviour after beta freeze. You need a steering council or release manager exception for this unfortunately

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is restoring a behaviour that was supposed to be deprecated

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, I don't follow. Could you give me some context on what happened?

Copy link
Contributor Author
@graingert graingert Sep 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pablogsal from the original issue report:

this broke distributed and we missed the DeprecationWarning because ensure_future accepts a wider variety of objects than just futures tasks and coroutines

dask/distributed#6785

the removal happened here 903f0a0

This PR restores support for Awaitable objects in asyncio.wait which was intended to be deprecated but never threw a DeprecationWarning because of an oversight in the implementation of the deprecation

Passing awaitable objects that are not futures to ``wait()``
directly is deprecated.

.. function:: as_completed(aws, *, timeout=None)

Run :ref:`awaitable objects <asyncio-awaitables>` in the *aws*
Expand Down
34 changes: 26 additions & 8 deletions Lib/asyncio/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,19 +408,37 @@ async def wait(fs, *, timeout=None, return_when=ALL_COMPLETED):
when the timeout occurs are returned in the second set.
"""
if futures.isfuture(fs) or coroutines.iscoroutine(fs):
raise TypeError(f"expect a list of futures, not {type(fs).__name__}")
if not fs:
raise ValueError('Set of Tasks/Futures is empty.')
raise TypeError(f"expected an iterable of futures, not {type(fs).__name__}")
if return_when not in (FIRST_COMPLETED, FIRST_EXCEPTION, ALL_COMPLETED):
raise ValueError(f'Invalid return_when value: {return_when}')

fs = set(fs)
loop = events.get_running_loop()
new_fs = set()

for f in fs:
if coroutines.iscoroutine(f):
raise TypeError("Passing coroutines is forbidden, use tasks explicitly.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will happened with implicitly created futures added to new_fs? Would it emit a warning?

Copy link
Contributor Author
@graingert graingert Sep 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the warning at @tiran 's request, with a plan to add it in a follow up PR.
I'm happy to put it back in this PR if you'd prefer

if not futures.isfuture(f):
warnings.warn(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use warnings._deprecated?

Copy link
Contributor Author
@graingert graingert Aug 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I usually would, but I kinda want to try backport the deprecation fix to 3.10

also the wording warnings._deprecated doesn't work too well for changes in behaviour and I didn't want to try and fix it and open a new can of worms there

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hugovk are you already working on a plan to upbraid all the existing warnings.warn(..., DeprecationWarning) to warnings._deprecated ?

there's a slight usability/wording concern when using it for changes in behavior instead of actual full removals
and there also seems to be a desire to use it for totally unscheduled removals: remove=(4, )


in addition do you have a view on backporting deprecation, where it was clear from the code what the intention of the deprecation was, but the deprecation itself had a bug?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hugovk are you already working on a plan to upbraid all the existing warnings.warn(..., DeprecationWarning) to warnings._deprecated ?

I'm not but that's a good idea. From a quick check, there's not too many, and most are set to be removed in 3.12 anyway.

there's a slight usability/wording concern when using it for changes in behavior instead of actual full removals

Can you alter the wording using the message parameter?

and there also seems to be a desire to use it for totally unscheduled removals: remove=(4, )

Some removals scheduled for 4.0 were from before we knew if 3.10 or 4.0 would follow 3.9 (#80480 (comment)) and in at least one case is shorthand for "sometime after 2.7.final" (#80480 (comment)).

Anyway, we should review any remaining removals originally set for 4.0 (e.g. in new issues or relevant open issues) to decide if they can be removed in an upcoming 3.x.


in addition do you have a view on backporting deprecation, where it was clear from the code what the intention of the deprecation was, but the deprecation itself had a bug?

Generally, I think it's a good idea to increase visibility and help users upgrade in time to reduce friction. It's good to ask release managers when in doubt.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hugovk do you think we have enough here to make a new issue out of this side discussion. Or maybe there's two to four ideas here:

  • Do something about existing deprecation warnings that don't automatically get upgraded to errors
  • Do something about the wording and format-string templating in warnings._deprecated to make it work for even the most subtle behaviour change
  • Determine a backport strategy for when a deprecation warning was applied to the wrong thing
  • Determine a strategy for permanent deprecations, where a feature is a bit broken or an alias of another thing, and people should really use the not broken way or source of the alias, but CPython is unwilling or undecided on actually removing it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The explicit passing of awaitable objects that are not futures "
"to asyncio.wait() is deprecated since Python 3.11, and "
"scheduled for removal in Python 3.14.",
DeprecationWarning,
stacklevel=2,
)
try:
new_fs.add(ensure_future(f, loop=loop))
except TypeError:
raise TypeError(
f"An asyncio.Future was expected, got {f!r}"
) from None
else:
new_fs.add(f)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is getting added to the set with no checks whatsoever. It needs to check _asyncio_future_blocking

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed!


if any(coroutines.iscoroutine(f) for f in fs):
raise TypeError("Passing coroutines is forbidden, use tasks explicitly.")
if not new_fs:
raise ValueError('Iterable of Tasks/Futures is empty.')

loop = events.get_running_loop()
return await _wait(fs, timeout, return_when, loop)
return await _wait(new_fs, timeout, return_when, loop)


def _release_waiter(waiter, *args):
Expand Down
53 changes: 53 additions & 0 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import collections
import contextvars
import concurrent.futures
import gc
import io
import random
Expand Down Expand Up @@ -3239,6 +3240,58 @@ async def coro():
self.assertEqual(result, 11)


class WaitTests(unittest.IsolatedAsyncioTestCase):
async def test_awaitable_is_deprecated_in_wait(self):
# Remove test when passing awaitables to asyncio.wait() is removed in 3.14

class ExampleAwaitable:
def __await__(self):
return asyncio.sleep(0).__await__()

with self.assertWarnsRegex(
DeprecationWarning,
"awaitable objects that are not futures",
):
await asyncio.wait([ExampleAwaitable()])

task = asyncio.create_task(coroutine_function())
with (
self.assertWarnsRegex(
DeprecationWarning, "awaitable objects that are not futures"
),
self.assertRaises(TypeError),
):
await asyncio.wait([task, ExampleAwaitable(), coroutine_function()])

# avoid: Task was destroyed but it is pending!
await task

async def test_wait_supports_iterators(self):
with self.assertRaisesRegex(ValueError, "Iterable of .* is empty\."):
await asyncio.wait(iter(()))

task = asyncio.create_task(coroutine_function())
done, pending = await asyncio.wait(iter((task,)))
self.assertSetEqual(done, {task})
self.assertSetEqual(pending, set())

async def test_wait_does_not_support_cf_future(self):
fut = concurrent.futures.Future()
with self.assertRaisesRegex(
TypeError,
r"An asyncio\.Future was expected, got <Future at 0x.* state=pending>",
):
await asyncio.wait({fut})

fut.set_result(None)
with self.assertRaisesRegex(
TypeError,
r"An asyncio\.Future was expected, got <Future at 0x.* state=finished"
r" returned NoneType>",
):
await asyncio.wait({fut})


class CompatibilityTests(test_utils.TestCase):
# Tests for checking a bridge between old-styled coroutines
# and async/await syntax
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The explicit passing of awaitable objects that are not :class:`asyncio.Future` to :func:`asyncio.wait` is deprecated and will be removed in version 3.14.
0