Skip to content

Commit 89b8204

Browse files
Merge remote-tracking branch 'upstream/main' into gh-113318-getset-rework
# Conflicts: # Tools/clinic/libclinic/dsl_parser.py # Tools/clinic/libclinic/parse_args.py
2 parents 091bdef + 20e6c2f commit 89b8204

39 files changed

Lines changed: 1367 additions & 407 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
==================

Doc/library/tarfile.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,10 @@ reused in custom filters:
11121112
paths (in case the name is absolute
11131113
even after stripping slashes, e.g. ``C:/foo`` on Windows).
11141114
This raises :class:`~tarfile.AbsolutePathError`.
1115+
- Normalize filenames (:attr:`TarInfo.name`) that contain ``..`` components
1116+
using :func:`os.path.normpath`.
1117+
Note that this removes internal ``..`` components, which may change the
1118+
meaning of the name if it traverses symbolic links.
11151119
- :ref:`Refuse <tarfile-extraction-refuse>` to extract files whose absolute
11161120
path (after following symlinks) would end up outside the destination.
11171121
This raises :class:`~tarfile.OutsideDestinationError`.
@@ -1120,6 +1124,10 @@ reused in custom filters:
11201124

11211125
Return the modified ``TarInfo`` member.
11221126

1127+
.. versionchanged:: next
1128+
1129+
Filenames containing ``..`` components are now normalized.
1130+
11231131
.. function:: data_filter(member, path)
11241132

11251133
Implements the ``'data'`` filter.

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/tarfile.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,13 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
833833
# For example, 'C:/foo' on Windows.
834834
raise AbsolutePathError(member)
835835
# Ensure we stay in the destination
836+
if '..' in name.replace(os.sep, '/').split('/'):
837+
# Directories are created from the name as given, so a name that
838+
# leaves the destination part-way through would create them
839+
# outside it even if the resolved path stays inside.
840+
normalized = os.path.normpath(name)
841+
if normalized != name:
842+
name = new_attrs['name'] = normalized
836843
target_path = os.path.realpath(os.path.join(dest_path, name),
837844
strict=os.path.ALLOW_MISSING)
838845
if os.path.commonpath([target_path, dest_path]) != dest_path:

Lib/test/support/__init__.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,15 +1271,20 @@ def set_memlimit(limit: str) -> None:
12711271

12721272

12731273
def _memory_watchdog(pid):
1274-
"""Return a function printing the memory usage of process *pid*."""
1274+
"""Return a function printing the memory usage of process *pid*.
1275+
1276+
The largest value it saw is kept in its ``peak`` attribute.
1277+
"""
12751278
# Imported here: test.support does not depend on test.libregrtest.
12761279
from test.libregrtest.utils import get_process_memory_usage
12771280

12781281
def watch():
12791282
mem = get_process_memory_usage(pid)
12801283
if mem is not None:
1284+
watch.peak = max(watch.peak, mem)
12811285
print(f" ... process data size: {mem / (1024 ** 3):.1f} GiB",
12821286
flush=True)
1287+
watch.peak = 0
12831288
return watch
12841289

12851290

@@ -1333,7 +1338,23 @@ def wrapper(self):
13331338
qualname = f'{cls.__qualname__}.{f.__name__}'
13341339
proc = isolation._start_test(cls.__module__, qualname)
13351340
watchdog = _memory_watchdog(proc.pid) if verbose else None
1336-
isolation._replay_test(self, *proc.wait(tick=watchdog))
1341+
payload, output, returncode = proc.wait(tick=watchdog)
1342+
if watchdog:
1343+
# The subprocess measures its own peak exactly. What the
1344+
# parent sampled is only a lower bound.
1345+
maxrss = payload and payload.get('maxrss')
1346+
peak = maxrss or watchdog.peak
1347+
if peak:
1348+
print(f" ... peak memory use: "
1349+
f"{peak / (1024 ** 3):.1f} GiB"
1350+
f"{'' if maxrss else ' or more'}", flush=True)
1351+
majflt = payload and payload.get('majflt')
1352+
if majflt:
1353+
# The test did not fit in memory, so its timing means
1354+
# little.
1355+
print(f" ... {majflt} major page faults: the test "
1356+
f"waited for the disk", flush=True)
1357+
isolation._replay_test(self, payload, output, returncode)
13371358
return
13381359

13391360
return f(self, maxsize)

Lib/test/support/subprocess_runner.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,45 @@ def _outcome(kind, test, detail):
7272
for t, tb in result.expectedFailures]
7373
outcomes += [_outcome('skipped', t, reason) for t, reason in result.skipped]
7474

75-
payload = {'outcomes': outcomes, 'durations': result.id_durations}
75+
def _usage():
76+
"""What this process used: peak resident set size in bytes, and the
77+
number of major page faults it took, either of which can be None.
78+
79+
A major page fault is served from disk, so a non-zero count means swapping.
80+
81+
The modules are imported here, after the test has run, so that the test
82+
does not see them.
83+
"""
84+
try:
85+
import resource
86+
except ImportError:
87+
pass
88+
else:
89+
usage = resource.getrusage(resource.RUSAGE_SELF)
90+
# Solaris and illumos leave these fields at 0, which no live process
91+
# has, so treat it as "not supported".
92+
if not usage.ru_maxrss:
93+
return {'maxrss': None, 'majflt': None}
94+
# ru_maxrss is in bytes on macOS, in kilobytes on Linux and the BSDs.
95+
maxrss = usage.ru_maxrss
96+
return {'maxrss': maxrss if sys.platform == 'darwin' else maxrss * 1024,
97+
'majflt': usage.ru_majflt}
98+
try:
99+
import os
100+
import _winapi
101+
handle = _winapi.OpenProcess(
102+
_winapi.PROCESS_QUERY_LIMITED_INFORMATION, False, os.getpid())
103+
except (ImportError, OSError):
104+
return {'maxrss': None, 'majflt': None}
105+
try:
106+
info = _winapi.GetProcessMemoryInfo(handle)
107+
finally:
108+
_winapi.CloseHandle(handle)
109+
# PageFaultCount counts all faults, not only the ones served from disk.
110+
return {'maxrss': info['PeakWorkingSetSize'], 'majflt': None}
111+
112+
113+
payload = {'outcomes': outcomes, 'durations': result.id_durations, **_usage()}
76114
with open(outfile, 'wb') as f:
77115
marshal.dump(payload, f)
78116

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

0 commit comments

Comments
 (0)