Skip to content

Commit d67d6fd

Browse files
committed
Merge branch 'main' into typewatch
* main: pythongh-68686: Retire eptag ptag scripts (python#98064) pythongh-97922: Run the GC only on eval breaker (python#97920) GitHub Workflows security hardening (python#96492) Add `@ezio-melotti` as codeowner for `.github/`. (python#98079) pythongh-97913 Docs: Add walrus operator to the index (python#97921) [doc] Fix broken links to C extensions accelerating stdlib modules (python#96914) pythongh-97822: Fix http.server documentation reference to test() function (python#98027) pythongh-91052: Add PyDict_Unwatch for unwatching a dictionary (python#98055) pythonGH-98023: Change default child watcher to PidfdChildWatcher on supported systems (python#98024) pythonGH-94182: Run the PidfdChildWatcher on the running loop (python#94184)
2 parents 27a4980 + 4ed00be commit d67d6fd

27 files changed

+296
-203
lines changed

.github/CODEOWNERS

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
# It uses the same pattern rule for gitignore file
55
# https://git-scm.com/docs/gitignore#_pattern_format
66

7+
# GitHub
8+
.github/** @ezio-melotti
9+
710
# asyncio
811
**/*asyncio* @1st1 @asvetlov @gvanrossum
912

.github/workflows/project-updater.yml

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ on:
66
- opened
77
- labeled
88

9+
permissions:
10+
contents: read
11+
912
jobs:
1013
add-to-project:
1114
name: Add issues to projects

Doc/c-api/dict.rst

+20-1
Original file line numberDiff line numberDiff line change
@@ -246,24 +246,41 @@ Dictionary Objects
246246
of error (e.g. no more watcher IDs available), return ``-1`` and set an
247247
exception.
248248
249+
.. versionadded:: 3.12
250+
249251
.. c:function:: int PyDict_ClearWatcher(int watcher_id)
250252
251253
Clear watcher identified by *watcher_id* previously returned from
252254
:c:func:`PyDict_AddWatcher`. Return ``0`` on success, ``-1`` on error (e.g.
253255
if the given *watcher_id* was never registered.)
254256
257+
.. versionadded:: 3.12
258+
255259
.. c:function:: int PyDict_Watch(int watcher_id, PyObject *dict)
256260
257261
Mark dictionary *dict* as watched. The callback granted *watcher_id* by
258262
:c:func:`PyDict_AddWatcher` will be called when *dict* is modified or
259-
deallocated.
263+
deallocated. Return ``0`` on success or ``-1`` on error.
264+
265+
.. versionadded:: 3.12
266+
267+
.. c:function:: int PyDict_Unwatch(int watcher_id, PyObject *dict)
268+
269+
Mark dictionary *dict* as no longer watched. The callback granted
270+
*watcher_id* by :c:func:`PyDict_AddWatcher` will no longer be called when
271+
*dict* is modified or deallocated. The dict must previously have been
272+
watched by this watcher. Return ``0`` on success or ``-1`` on error.
273+
274+
.. versionadded:: 3.12
260275
261276
.. c:type:: PyDict_WatchEvent
262277
263278
Enumeration of possible dictionary watcher events: ``PyDict_EVENT_ADDED``,
264279
``PyDict_EVENT_MODIFIED``, ``PyDict_EVENT_DELETED``, ``PyDict_EVENT_CLONED``,
265280
``PyDict_EVENT_CLEARED``, or ``PyDict_EVENT_DEALLOCATED``.
266281
282+
.. versionadded:: 3.12
283+
267284
.. c:type:: int (*PyDict_WatchCallback)(PyDict_WatchEvent event, PyObject *dict, PyObject *key, PyObject *new_value)
268285
269286
Type of a dict watcher callback function.
@@ -289,3 +306,5 @@ Dictionary Objects
289306
If the callback returns with an exception set, it must return ``-1``; this
290307
exception will be printed as an unraisable exception using
291308
:c:func:`PyErr_WriteUnraisable`. Otherwise it should return ``0``.
309+
310+
.. versionadded:: 3.12

Doc/library/http.server.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,8 @@ provides three different variants:
392392
contents of the file are output. If the file's MIME type starts with
393393
``text/`` the file is opened in text mode; otherwise binary mode is used.
394394

395-
For example usage, see the implementation of the :func:`test` function
396-
invocation in the :mod:`http.server` module.
395+
For example usage, see the implementation of the ``test`` function
396+
in :source:`Lib/http/server.py`.
397397

398398
.. versionchanged:: 3.7
399399
Support of the ``'If-Modified-Since'`` header.

Doc/license.rst

+6-3
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ for third-party software incorporated in the Python distribution.
302302
Mersenne Twister
303303
----------------
304304

305-
The :mod:`_random` module includes code based on a download from
305+
The :mod:`!_random` C extension underlying the :mod:`random` module
306+
includes code based on a download from
306307
http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/MT2002/emt19937ar.html. The following are
307308
the verbatim comments from the original code::
308309

@@ -819,7 +820,8 @@ sources unless the build is configured ``--with-system-expat``::
819820
libffi
820821
------
821822

822-
The :mod:`_ctypes` extension is built using an included copy of the libffi
823+
The :mod:`!_ctypes` C extension underlying the :mod:`ctypes` module
824+
is built using an included copy of the libffi
823825
sources unless the build is configured ``--with-system-libffi``::
824826

825827
Copyright (c) 1996-2008 Red Hat, Inc and others.
@@ -920,7 +922,8 @@ on the cfuhash project::
920922
libmpdec
921923
--------
922924

923-
The :mod:`_decimal` module is built using an included copy of the libmpdec
925+
The :mod:`!_decimal` C extension underlying the :mod:`decimal` module
926+
is built using an included copy of the libmpdec
924927
library unless the build is configured ``--with-system-libmpdec``::
925928

926929
Copyright (c) 2008-2020 Stefan Krah. All rights reserved.

Doc/reference/expressions.rst

+6
Original file line numberDiff line numberDiff line change
@@ -1741,6 +1741,12 @@ returns a boolean value regardless of the type of its argument
17411741
(for example, ``not 'foo'`` produces ``False`` rather than ``''``.)
17421742

17431743

1744+
.. index::
1745+
single: := (colon equals)
1746+
single: assignment expression
1747+
single: walrus operator
1748+
single: named expression
1749+
17441750
Assignment expressions
17451751
======================
17461752

Doc/whatsnew/3.12.rst

+12
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ Other Language Changes
9393
when parsing source code containing null bytes. (Contributed by Pablo Galindo
9494
in :gh:`96670`.)
9595

96+
* The Garbage Collector now runs only on the eval breaker mechanism of the
97+
Python bytecode evaluation loop instead on object allocations. The GC can
98+
also run when :c:func:`PyErr_CheckSignals` is called so C extensions that
99+
need to run for a long time without executing any Python code also have a
100+
chance to execute the GC periodically. (Contributed by Pablo Galindo in
101+
:gh:`97922`.)
102+
96103
New Modules
97104
===========
98105

@@ -546,6 +553,11 @@ New Features
546553
which sets the vectorcall field of a given :c:type:`PyFunctionObject`.
547554
(Contributed by Andrew Frost in :gh:`92257`.)
548555

556+
* The C API now permits registering callbacks via :c:func:`PyDict_AddWatcher`,
557+
:c:func:`PyDict_AddWatch` and related APIs to be called whenever a dictionary
558+
is modified. This is intended for use by optimizing interpreters, JIT
559+
compilers, or debuggers.
560+
549561
Porting to Python 3.12
550562
----------------------
551563

Include/cpython/dictobject.h

+1
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,4 @@ PyAPI_FUNC(int) PyDict_ClearWatcher(int watcher_id);
106106

107107
// Mark given dictionary as "watched" (callback will be called if it is modified)
108108
PyAPI_FUNC(int) PyDict_Watch(int watcher_id, PyObject* dict);
109+
PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject* dict);

Include/internal/pycore_gc.h

+2
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ extern void _PyList_ClearFreeList(PyInterpreterState *interp);
202202
extern void _PyDict_ClearFreeList(PyInterpreterState *interp);
203203
extern void _PyAsyncGen_ClearFreeLists(PyInterpreterState *interp);
204204
extern void _PyContext_ClearFreeList(PyInterpreterState *interp);
205+
extern void _Py_ScheduleGC(PyInterpreterState *interp);
206+
extern void _Py_RunGC(PyThreadState *tstate);
205207

206208
#ifdef __cplusplus
207209
}

Include/internal/pycore_interp.h

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ struct _ceval_state {
4949
_Py_atomic_int eval_breaker;
5050
/* Request for dropping the GIL */
5151
_Py_atomic_int gil_drop_request;
52+
/* The GC is ready to be executed */
53+
_Py_atomic_int gc_scheduled;
5254
struct _pending_calls pending;
5355
};
5456

Lib/asyncio/unix_events.py

+27-33
Original file line numberDiff line numberDiff line change
@@ -912,46 +912,29 @@ class PidfdChildWatcher(AbstractChildWatcher):
912912
recent (5.3+) kernels.
913913
"""
914914

915-
def __init__(self):
916-
self._loop = None
917-
self._callbacks = {}
918-
919915
def __enter__(self):
920916
return self
921917

922918
def __exit__(self, exc_type, exc_value, exc_traceback):
923919
pass
924920

925921
def is_active(self):
926-
return self._loop is not None and self._loop.is_running()
922+
return True
927923

928924
def close(self):
929-
self.attach_loop(None)
925+
pass
930926

931927
def attach_loop(self, loop):
932-
if self._loop is not None and loop is None and self._callbacks:
933-
warnings.warn(
934-
'A loop is being detached '
935-
'from a child watcher with pending handlers',
936-
RuntimeWarning)
937-
for pidfd, _, _ in self._callbacks.values():
938-
self._loop._remove_reader(pidfd)
939-
os.close(pidfd)
940-
self._callbacks.clear()
941-
self._loop = loop
928+
pass
942929

943930
def add_child_handler(self, pid, callback, *args):
944-
existing = self._callbacks.get(pid)
945-
if existing is not None:
946-
self._callbacks[pid] = existing[0], callback, args
947-
else:
948-
pidfd = os.pidfd_open(pid)
949-
self._loop._add_reader(pidfd, self._do_wait, pid)
950-
self._callbacks[pid] = pidfd, callback, args
931+
loop = events.get_running_loop()
932+
pidfd = os.pidfd_open(pid)
933+
loop._add_reader(pidfd, self._do_wait, pid, pidfd, callback, args)
951934

952-
def _do_wait(self, pid):
953-
pidfd, callback, args = self._callbacks.pop(pid)
954-
self._loop._remove_reader(pidfd)
935+
def _do_wait(self, pid, pidfd, callback, args):
936+
loop = events.get_running_loop()
937+
loop._remove_reader(pidfd)
955938
try:
956939
_, status = os.waitpid(pid, 0)
957940
except ChildProcessError:
@@ -969,12 +952,9 @@ def _do_wait(self, pid):
969952
callback(pid, returncode, *args)
970953

971954
def remove_child_handler(self, pid):
972-
try:
973-
pidfd, _, _ = self._callbacks.pop(pid)
974-
except KeyError:
975-
return False
976-
self._loop._remove_reader(pidfd)
977-
os.close(pidfd)
955+
# asyncio never calls remove_child_handler() !!!
956+
# The method is no-op but is implemented because
957+
# abstract base classes require it.
978958
return True
979959

980960

@@ -1423,6 +1403,17 @@ def _do_waitpid(self, loop, expected_pid, callback, args):
14231403

14241404
self._threads.pop(expected_pid)
14251405

1406+
def can_use_pidfd():
1407+
if not hasattr(os, 'pidfd_open'):
1408+
return False
1409+
try:
1410+
pid = os.getpid()
1411+
os.close(os.pidfd_open(pid, 0))
1412+
except OSError:
1413+
# blocked by security policy like SECCOMP
1414+
return False
1415+
return True
1416+
14261417

14271418
class _UnixDefaultEventLoopPolicy(events.BaseDefaultEventLoopPolicy):
14281419
"""UNIX event loop policy with a watcher for child processes."""
@@ -1435,7 +1426,10 @@ def __init__(self):
14351426
def _init_watcher(self):
14361427
with events._lock:
14371428
if self._watcher is None: # pragma: no branch
1438-
self._watcher = ThreadedChildWatcher()
1429+
if can_use_pidfd():
1430+
self._watcher = PidfdChildWatcher()
1431+
else:
1432+
self._watcher = ThreadedChildWatcher()
14391433
if threading.current_thread() is threading.main_thread():
14401434
self._watcher.attach_loop(self._local._loop)
14411435

Lib/test/test_asyncio/test_subprocess.py

+44-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
import unittest
66
import warnings
7+
import functools
78
from unittest import mock
89

910
import asyncio
@@ -30,6 +31,19 @@
3031
'sys.stdout.buffer.write(data)'))]
3132

3233

34+
@functools.cache
35+
def _has_pidfd_support():
36+
if not hasattr(os, 'pidfd_open'):
37+
return False
38+
39+
try:
40+
os.close(os.pidfd_open(os.getpid()))
41+
except OSError:
42+
return False
43+
44+
return True
45+
46+
3347
def tearDownModule():
3448
asyncio.set_event_loop_policy(None)
3549

@@ -708,17 +722,8 @@ class SubprocessFastWatcherTests(SubprocessWatcherMixin,
708722

709723
Watcher = unix_events.FastChildWatcher
710724

711-
def has_pidfd_support():
712-
if not hasattr(os, 'pidfd_open'):
713-
return False
714-
try:
715-
os.close(os.pidfd_open(os.getpid()))
716-
except OSError:
717-
return False
718-
return True
719-
720725
@unittest.skipUnless(
721-
has_pidfd_support(),
726+
_has_pidfd_support(),
722727
"operating system does not support pidfds",
723728
)
724729
class SubprocessPidfdWatcherTests(SubprocessWatcherMixin,
@@ -751,6 +756,35 @@ async def execute():
751756
mock.call.__exit__(RuntimeError, mock.ANY, mock.ANY),
752757
])
753758

759+
760+
@unittest.skipUnless(
761+
_has_pidfd_support(),
762+
"operating system does not support pidfds",
763+
)
764+
def test_create_subprocess_with_pidfd(self):
765+
async def in_thread():
766+
proc = await asyncio.create_subprocess_exec(
767+
*PROGRAM_CAT,
768+
stdin=subprocess.PIPE,
769+
stdout=subprocess.PIPE,
770+
)
771+
stdout, stderr = await proc.communicate(b"some data")
772+
return proc.returncode, stdout
773+
774+
async def main():
775+
# asyncio.Runner did not call asyncio.set_event_loop()
776+
with self.assertRaises(RuntimeError):
777+
asyncio.get_event_loop_policy().get_event_loop()
778+
return await asyncio.to_thread(asyncio.run, in_thread())
779+
780+
asyncio.set_child_watcher(asyncio.PidfdChildWatcher())
781+
try:
782+
with asyncio.Runner(loop_factory=asyncio.new_event_loop) as runner:
783+
returncode, stdout = runner.run(main())
784+
self.assertEqual(returncode, 0)
785+
self.assertEqual(stdout, b'some data')
786+
finally:
787+
asyncio.set_child_watcher(None)
754788
else:
755789
# Windows
756790
class SubprocessProactorTests(SubprocessMixin, test_utils.TestCase):

Lib/test/test_asyncio/test_unix_events.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -1702,14 +1702,26 @@ def create_policy(self):
17021702
def test_get_default_child_watcher(self):
17031703
policy = self.create_policy()
17041704
self.assertIsNone(policy._watcher)
1705-
1705+
unix_events.can_use_pidfd = mock.Mock()
1706+
unix_events.can_use_pidfd.return_value = False
17061707
watcher = policy.get_child_watcher()
17071708
self.assertIsInstance(watcher, asyncio.ThreadedChildWatcher)
17081709

17091710
self.assertIs(policy._watcher, watcher)
17101711

17111712
self.assertIs(watcher, policy.get_child_watcher())
17121713

1714+
policy = self.create_policy()
1715+
self.assertIsNone(policy._watcher)
1716+
unix_events.can_use_pidfd = mock.Mock()
1717+
unix_events.can_use_pidfd.return_value = True
1718+
watcher = policy.get_child_watcher()
1719+
self.assertIsInstance(watcher, asyncio.PidfdChildWatcher)
1720+
1721+
self.assertIs(policy._watcher, watcher)
1722+
1723+
self.assertIs(watcher, policy.get_child_watcher())
1724+
17131725
def test_get_child_watcher_after_set(self):
17141726
policy = self.create_policy()
17151727
watcher = asyncio.FastChildWatcher()

0 commit comments

Comments
 (0)