Skip to content

Commit 1b5387c

Browse files
Merge remote-tracking branch 'upstream/main' into clinic-param-aliases
2 parents f99bad0 + 61818b6 commit 1b5387c

18 files changed

Lines changed: 746 additions & 192 deletions

Doc/library/asyncio-task.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -843,17 +843,13 @@ Timeouts
843843
Wait for the *fut* :ref:`awaitable <asyncio-awaitables>`
844844
to complete with a timeout.
845845

846-
If *fut* is a coroutine it is automatically scheduled as a Task.
847-
848846
*timeout* can either be ``None`` or a float or int number of seconds
849847
to wait for. If *timeout* is ``None``, block until the future
850848
completes.
851849

852-
If a timeout occurs, it cancels the task and raises
853-
:exc:`TimeoutError`.
850+
If a timeout occurs, it cancels *fut* and raises :exc:`TimeoutError`.
854851

855-
To avoid the task :meth:`cancellation <Task.cancel>`,
856-
wrap it in :func:`shield`.
852+
To prevent *fut* from being cancelled, wrap it in :func:`shield`.
857853

858854
The function will wait until the future is actually cancelled,
859855
so the total wait time may exceed the *timeout*. If an exception
@@ -894,6 +890,10 @@ Timeouts
894890
.. versionchanged:: 3.11
895891
Raises :exc:`TimeoutError` instead of :exc:`asyncio.TimeoutError`.
896892

893+
.. versionchanged:: 3.12
894+
Implemented using :func:`asyncio.timeout`, a coroutine passed as *fut*
895+
is no longer wrapped in a :class:`Task` when *timeout* is positive.
896+
897897

898898
Waiting primitives
899899
==================

Include/internal/pycore_interpframe_structs.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ struct _PyInterpreterFrame {
6666
PyObject *prefix##_qualname; \
6767
_PyErr_StackItem prefix##_exc_state; \
6868
PyObject *prefix##_origin_or_finalizer; \
69-
char prefix##_hooks_inited; \
70-
char prefix##_closed; \
71-
char prefix##_running_async; \
69+
int8_t prefix##_hooks_inited; \
70+
int8_t prefix##_closed; \
71+
int8_t prefix##_running_async; \
7272
/* The frame */ \
7373
int8_t prefix##_frame_state; \
7474
_PyInterpreterFrame prefix##_iframe; \

Lib/asyncio/streams.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,17 @@ def connection_made(self, transport):
239239
self._over_ssl = transport.get_extra_info('sslcontext') is not None
240240
if self._client_connected_cb is not None:
241241
writer = StreamWriter(transport, self, reader, self._loop)
242-
res = self._client_connected_cb(reader, writer)
242+
try:
243+
res = self._client_connected_cb(reader, writer)
244+
except Exception as exc:
245+
self._loop.call_exception_handler({
246+
'message': 'Unhandled exception in client_connected_cb',
247+
'exception': exc,
248+
'transport': transport,
249+
})
250+
transport.close()
251+
self._strong_reader = None
252+
return
243253
if coroutines.iscoroutine(res):
244254
def callback(task):
245255
if task.cancelled():

Lib/asyncio/tasks.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -440,15 +440,13 @@ def _release_waiter(waiter, *args):
440440
async def wait_for(fut, timeout):
441441
"""Wait for the single Future or coroutine to complete, with timeout.
442442
443-
Coroutine will be wrapped in Task.
444-
445443
Returns result of the Future or coroutine. When a timeout occurs,
446-
it cancels the task and raises TimeoutError. To avoid the task
447-
cancellation, wrap it in shield().
444+
it cancels fut and raises TimeoutError. To prevent fut from being
445+
cancelled, wrap it in shield().
448446
449-
If the wait is cancelled, the task is also cancelled.
447+
If the wait is cancelled, fut is also cancelled.
450448
451-
If the task suppresses the cancellation and returns a value instead,
449+
If fut suppresses the cancellation and returns a value instead,
452450
that value is returned.
453451
454452
This function is a coroutine.

Lib/platform.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -427,11 +427,16 @@ def _win32_ver(version, csd, ptype):
427427

428428
winver = getwindowsversion()
429429
is_client = (getattr(winver, 'product_type', 1) == 1)
430-
try:
431-
version = _syscmd_ver()[2]
432-
major, minor, build = map(int, version.split('.'))
433-
except ValueError:
434-
major, minor, build = winver.platform_version or winver[:3]
430+
431+
if winver.device_family == "Desktop":
432+
try:
433+
version = _syscmd_ver()[2]
434+
major, minor, build = map(int, version.split('.'))
435+
except ValueError:
436+
major, minor, build = winver.platform_version or winver[:3]
437+
version = '{0}.{1}.{2}'.format(major, minor, build)
438+
else:
439+
major, minor, build = winver[:3]
435440
version = '{0}.{1}.{2}'.format(major, minor, build)
436441

437442
# getwindowsversion() reflect the compatibility mode Python is

Lib/test/test_asyncgen.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,33 @@ async def agenfn():
619619
with self.assertRaisesRegex(RuntimeError, "coroutine ignored GeneratorExit"):
620620
gen.close()
621621

622+
def test_async_gen_athrow_send_non_none(self):
623+
# gh-120321: sending a non-None value to a just-started athrow()
624+
# awaitable must not claim the generator, so the generator stays
625+
# usable and the awaitable can still be awaited afterwards.
626+
class MyExc(Exception):
627+
pass
628+
629+
async def agenfn():
630+
try:
631+
yield 1
632+
except MyExc:
633+
yield 2
634+
635+
agen = agenfn()
636+
with self.assertRaises(StopIteration):
637+
agen.asend(None).send(None)
638+
639+
gen = agen.athrow(MyExc)
640+
with self.assertRaisesRegex(RuntimeError, "non-None value"):
641+
gen.send(42)
642+
self.assertFalse(agen.ag_running)
643+
644+
# The awaitable is still in its initial state and works normally.
645+
with self.assertRaises(StopIteration) as cm:
646+
gen.send(None)
647+
self.assertEqual(cm.exception.value, 2)
648+
622649

623650
class AsyncGenAsyncioTest(unittest.TestCase):
624651

@@ -1950,6 +1977,41 @@ class MyException(Exception):
19501977
):
19511978
nxt.throw(MyException)
19521979

1980+
def test_async_gen_send_same_athrow_coro_after_completion(self):
1981+
# gh-120321: an athrow() awaitable that needs more than one send()
1982+
# to complete must be closed on completion; sending to it again
1983+
# must raise instead of resuming the generator.
1984+
class YieldOnce:
1985+
def __await__(self):
1986+
yield
1987+
1988+
async def async_iterate():
1989+
try:
1990+
yield 1
1991+
except ValueError:
1992+
await YieldOnce()
1993+
yield 2
1994+
1995+
it = async_iterate()
1996+
with self.assertRaises(StopIteration):
1997+
it.__anext__().send(None)
1998+
1999+
nxt = it.athrow(ValueError)
2000+
# The exception handler suspends before the operation completes.
2001+
nxt.send(None)
2002+
with self.assertRaises(StopIteration) as cm:
2003+
nxt.send(None)
2004+
self.assertEqual(cm.exception.value, 2)
2005+
2006+
with self.assertRaisesRegex(
2007+
RuntimeError,
2008+
r"cannot reuse already awaited aclose\(\)/athrow\(\)"
2009+
):
2010+
nxt.send(None)
2011+
2012+
with self.assertRaises(StopIteration):
2013+
it.aclose().send(None)
2014+
19532015
def test_async_gen_aclose_twice_with_different_coros(self):
19542016
# Regression test for https://bugs.python.org/issue39606
19552017
async def async_iterate():

Lib/test/test_asyncio/test_streams.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,38 @@ async def handle_echo(reader, writer):
12671267
messages = self._basetest_unhandled_exceptions(handle_echo)
12681268
self.assertEqual(messages, [])
12691269

1270+
def test_unhandled_exception_sync_callback(self):
1271+
# An exception raised by a plain-function client_connected_cb is
1272+
# reported like the coroutine case and the transport is closed.
1273+
port = socket_helper.find_unused_port()
1274+
1275+
messages = []
1276+
self.loop.set_exception_handler(lambda loop, ctx: messages.append(ctx))
1277+
1278+
async def client():
1279+
rd, wr = await asyncio.open_connection('localhost', port)
1280+
async with asyncio.timeout(60):
1281+
data = await rd.read()
1282+
self.assertEqual(data, b'') # the server closed the connection
1283+
wr.close()
1284+
await wr.wait_closed()
1285+
1286+
async def main():
1287+
def handle_echo(reader, writer):
1288+
raise Exception('test')
1289+
1290+
server = await asyncio.start_server(
1291+
handle_echo, 'localhost', port)
1292+
await server.start_serving()
1293+
await client()
1294+
server.close()
1295+
await server.wait_closed()
1296+
1297+
self.loop.run_until_complete(main())
1298+
1299+
self.assertEqual(messages[0]['message'],
1300+
'Unhandled exception in client_connected_cb')
1301+
12701302
def test_open_connection_happy_eyeball_refcycles(self):
12711303
port = socket_helper.find_unused_port()
12721304
async def main():

Lib/test/test_clinic.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3337,6 +3337,64 @@ def test_no_change(self):
33373337
# if the content does not change
33383338
self.assertEqual(pre_mtime, post_mtime)
33393339

3340+
TOUCH_CODE = dedent("""
3341+
/*[clinic input]
3342+
module m
3343+
[clinic start generated code]*/
3344+
3345+
/*[clinic input]
3346+
output everything file
3347+
m.func
3348+
a: int
3349+
/
3350+
3351+
Docstring.
3352+
[clinic start generated code]*/
3353+
""")
3354+
3355+
def test_touch_source(self):
3356+
# gh-64595: The build system does not know that the source file
3357+
# depends on the file generated from it, so the modification
3358+
# times are updated to force the recompilation.
3359+
def mtimes():
3360+
return os.stat(fn).st_mtime_ns, os.stat(dest).st_mtime_ns
3361+
3362+
def set_mtimes(source, generated):
3363+
os.utime(fn, ns=(source, source))
3364+
os.utime(dest, ns=(generated, generated))
3365+
3366+
with os_helper.temp_dir() as tmp_dir:
3367+
fn = os.path.join(tmp_dir, "test.c")
3368+
with open(fn, "w", encoding="utf-8") as f:
3369+
f.write(self.TOUCH_CODE)
3370+
dest = self.dest_file(fn)
3371+
self.expect_success(fn)
3372+
source_mtime, generated_mtime = mtimes()
3373+
self.assertGreaterEqual(generated_mtime, source_mtime)
3374+
3375+
# The generated file is changed, so both files are touched.
3376+
os.unlink(dest)
3377+
old = source_mtime - 10**10
3378+
os.utime(fn, ns=(old, old))
3379+
self.expect_success(fn)
3380+
source_mtime, generated_mtime = mtimes()
3381+
self.assertGreater(source_mtime, old)
3382+
self.assertGreaterEqual(generated_mtime, source_mtime)
3383+
3384+
# Nothing is changed, but the source file is newer, so only
3385+
# the generated file is touched.
3386+
set_mtimes(source_mtime - 10**10, source_mtime - 2 * 10**10)
3387+
old_source_mtime = os.stat(fn).st_mtime_ns
3388+
self.expect_success(fn)
3389+
source_mtime, generated_mtime = mtimes()
3390+
self.assertEqual(source_mtime, old_source_mtime)
3391+
self.assertGreaterEqual(generated_mtime, source_mtime)
3392+
3393+
# Nothing is changed and the generated file is newer,
3394+
# so no file is touched.
3395+
self.expect_success(fn)
3396+
self.assertEqual(mtimes(), (source_mtime, generated_mtime))
3397+
33403398
def test_cli_force(self):
33413399
invalid_input = dedent("""
33423400
/*[clinic input]

0 commit comments

Comments
 (0)