Skip to content
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

GH-120804: Remove get_child_watcher and set_child_watcher from asyncio #120818

Merged
merged 3 commits into from
Jun 23, 2024
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
23 changes: 0 additions & 23 deletions Lib/asyncio/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
'Handle', 'TimerHandle',
'get_event_loop_policy', 'set_event_loop_policy',
'get_event_loop', 'set_event_loop', 'new_event_loop',
'get_child_watcher', 'set_child_watcher',
'_set_running_loop', 'get_running_loop',
'_get_running_loop',
)
Expand Down Expand Up @@ -652,17 +651,6 @@ def new_event_loop(self):
the current context, set_event_loop must be called explicitly."""
raise NotImplementedError

# Child processes handling (Unix only).

def get_child_watcher(self):
"Get the watcher for child processes."
raise NotImplementedError

def set_child_watcher(self, watcher):
"""Set the watcher for child processes."""
raise NotImplementedError


class BaseDefaultEventLoopPolicy(AbstractEventLoopPolicy):
"""Default policy implementation for accessing the event loop.

Expand Down Expand Up @@ -837,17 +825,6 @@ def new_event_loop():
return get_event_loop_policy().new_event_loop()


def get_child_watcher():
"""Equivalent to calling get_event_loop_policy().get_child_watcher()."""
return get_event_loop_policy().get_child_watcher()


def set_child_watcher(watcher):
"""Equivalent to calling
get_event_loop_policy().set_child_watcher(watcher)."""
return get_event_loop_policy().set_child_watcher(watcher)


# Alias pure-Python implementations for testing purposes.
_py__get_running_loop = _get_running_loop
_py__set_running_loop = _set_running_loop
Expand Down
95 changes: 5 additions & 90 deletions Lib/asyncio/unix_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ async def _make_subprocess_transport(self, protocol, args, shell,
extra=None, **kwargs):
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
watcher = events.get_child_watcher()
watcher = events.get_event_loop_policy()._watcher

with watcher:
if not watcher.is_active():
Expand Down Expand Up @@ -1009,59 +1009,6 @@ def remove_child_handler(self, pid):
return True


class BaseChildWatcher(AbstractChildWatcher):

def __init__(self):
self._loop = None
self._callbacks = {}

def close(self):
self.attach_loop(None)

def is_active(self):
return self._loop is not None and self._loop.is_running()

def _do_waitpid(self, expected_pid):
raise NotImplementedError()

def _do_waitpid_all(self):
raise NotImplementedError()

def attach_loop(self, loop):
assert loop is None or isinstance(loop, events.AbstractEventLoop)

if self._loop is not None and loop is None and self._callbacks:
warnings.warn(
'A loop is being detached '
'from a child watcher with pending handlers',
RuntimeWarning)

if self._loop is not None:
self._loop.remove_signal_handler(signal.SIGCHLD)

self._loop = loop
if loop is not None:
loop.add_signal_handler(signal.SIGCHLD, self._sig_chld)

# Prevent a race condition in case a child terminated
# during the switch.
self._do_waitpid_all()

def _sig_chld(self):
try:
self._do_waitpid_all()
except (SystemExit, KeyboardInterrupt):
raise
except BaseException as exc:
# self._loop should always be available here
# as '_sig_chld' is added as a signal handler
# in 'attach_loop'
self._loop.call_exception_handler({
'message': 'Unknown exception in SIGCHLD handler',
'exception': exc,
})


class ThreadedChildWatcher(AbstractChildWatcher):
"""Threaded child watcher implementation.

Expand Down Expand Up @@ -1161,15 +1108,10 @@ class _UnixDefaultEventLoopPolicy(events.BaseDefaultEventLoopPolicy):

def __init__(self):
super().__init__()
self._watcher = None

def _init_watcher(self):
with events._lock:
if self._watcher is None: # pragma: no branch
if can_use_pidfd():
self._watcher = PidfdChildWatcher()
else:
self._watcher = ThreadedChildWatcher()
if can_use_pidfd():
self._watcher = PidfdChildWatcher()
else:
self._watcher = ThreadedChildWatcher()

def set_event_loop(self, loop):
"""Set the event loop.
Expand All @@ -1185,33 +1127,6 @@ def set_event_loop(self, loop):
threading.current_thread() is threading.main_thread()):
self._watcher.attach_loop(loop)

def get_child_watcher(self):
"""Get the watcher for child processes.

If not yet set, a ThreadedChildWatcher object is automatically created.
"""
if self._watcher is None:
self._init_watcher()

warnings._deprecated("get_child_watcher",
"{name!r} is deprecated as of Python 3.12 and will be "
"removed in Python {remove}.", remove=(3, 14))
return self._watcher

def set_child_watcher(self, watcher):
"""Set the watcher for child processes."""

assert watcher is None or isinstance(watcher, AbstractChildWatcher)

if self._watcher is not None:
self._watcher.close()

self._watcher = watcher
warnings._deprecated("set_child_watcher",
"{name!r} is deprecated as of Python 3.12 and will be "
"removed in Python {remove}.", remove=(3, 14))


SelectorEventLoop = _UnixSelectorEventLoop
DefaultEventLoopPolicy = _UnixDefaultEventLoopPolicy
EventLoop = SelectorEventLoop
22 changes: 8 additions & 14 deletions Lib/test/test_asyncio/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -2212,16 +2212,14 @@ def test_remove_fds_after_closing(self):
class UnixEventLoopTestsMixin(EventLoopTestsMixin):
def setUp(self):
super().setUp()
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
watcher = asyncio.ThreadedChildWatcher()
watcher.attach_loop(self.loop)
asyncio.set_child_watcher(watcher)
watcher = asyncio.ThreadedChildWatcher()
watcher.attach_loop(self.loop)
policy = asyncio.get_event_loop_policy()
policy._watcher = watcher

def tearDown(self):
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
asyncio.set_child_watcher(None)
policy = asyncio.get_event_loop_policy()
policy._watcher = None
super().tearDown()


Expand Down Expand Up @@ -2716,9 +2714,6 @@ def test_event_loop_policy(self):
self.assertRaises(NotImplementedError, policy.get_event_loop)
self.assertRaises(NotImplementedError, policy.set_event_loop, object())
self.assertRaises(NotImplementedError, policy.new_event_loop)
self.assertRaises(NotImplementedError, policy.get_child_watcher)
self.assertRaises(NotImplementedError, policy.set_child_watcher,
object())

def test_get_event_loop(self):
policy = asyncio.DefaultEventLoopPolicy()
Expand Down Expand Up @@ -2836,9 +2831,8 @@ def setUp(self):
def tearDown(self):
try:
if sys.platform != 'win32':
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
asyncio.set_child_watcher(None)
policy = asyncio.get_event_loop_policy()
policy._watcher = None

super().tearDown()
finally:
Expand Down
70 changes: 3 additions & 67 deletions Lib/test/test_asyncio/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -879,17 +879,13 @@ def setUp(self):

watcher = self._get_watcher()
watcher.attach_loop(self.loop)
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
policy.set_child_watcher(watcher)
policy._watcher = watcher

def tearDown(self):
super().tearDown()
policy = asyncio.get_event_loop_policy()
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
watcher = policy.get_child_watcher()
policy.set_child_watcher(None)
watcher = policy._watcher
policy._watcher = None
watcher.attach_loop(None)
watcher.close()

Expand All @@ -910,66 +906,6 @@ def _get_watcher(self):
return unix_events.PidfdChildWatcher()


class GenericWatcherTests(test_utils.TestCase):

def test_create_subprocess_fails_with_inactive_watcher(self):
watcher = mock.create_autospec(asyncio.AbstractChildWatcher)
watcher.is_active.return_value = False

async def execute():
asyncio.set_child_watcher(watcher)

with self.assertRaises(RuntimeError):
await subprocess.create_subprocess_exec(
os_helper.FakePath(sys.executable), '-c', 'pass')

watcher.add_child_handler.assert_not_called()

with asyncio.Runner(loop_factory=asyncio.new_event_loop) as runner:
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
self.assertIsNone(runner.run(execute()))
self.assertListEqual(watcher.mock_calls, [
mock.call.__enter__(),
mock.call.is_active(),
mock.call.__exit__(RuntimeError, mock.ANY, mock.ANY),
], watcher.mock_calls)


@unittest.skipUnless(
unix_events.can_use_pidfd(),
"operating system does not support pidfds",
)
def test_create_subprocess_with_pidfd(self):
async def in_thread():
proc = await asyncio.create_subprocess_exec(
*PROGRAM_CAT,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
stdout, stderr = await proc.communicate(b"some data")
return proc.returncode, stdout

async def main():
# asyncio.Runner did not call asyncio.set_event_loop()
with warnings.catch_warnings():
warnings.simplefilter('error', DeprecationWarning)
# get_event_loop() raises DeprecationWarning if
# set_event_loop() was never called and RuntimeError if
# it was called at least once.
with self.assertRaises((RuntimeError, DeprecationWarning)):
asyncio.get_event_loop_policy().get_event_loop()
return await asyncio.to_thread(asyncio.run, in_thread())
with self.assertWarns(DeprecationWarning):
asyncio.set_child_watcher(asyncio.PidfdChildWatcher())
try:
with asyncio.Runner(loop_factory=asyncio.new_event_loop) as runner:
returncode, stdout = runner.run(main())
self.assertEqual(returncode, 0)
self.assertEqual(stdout, b'some data')
finally:
with self.assertWarns(DeprecationWarning):
asyncio.set_child_watcher(None)
else:
# Windows
class SubprocessProactorTests(SubprocessMixin, test_utils.TestCase):
Expand Down
29 changes: 0 additions & 29 deletions Lib/test/test_asyncio/test_unix_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1138,35 +1138,6 @@ def test_not_implemented(self):
NotImplementedError, watcher.__exit__, f, f, f)


class PolicyTests(unittest.TestCase):

def create_policy(self):
return asyncio.DefaultEventLoopPolicy()

@mock.patch('asyncio.unix_events.can_use_pidfd')
def test_get_default_child_watcher(self, m_can_use_pidfd):
m_can_use_pidfd.return_value = False
policy = self.create_policy()
self.assertIsNone(policy._watcher)
with self.assertWarns(DeprecationWarning):
watcher = policy.get_child_watcher()
self.assertIsInstance(watcher, asyncio.ThreadedChildWatcher)

self.assertIs(policy._watcher, watcher)
with self.assertWarns(DeprecationWarning):
self.assertIs(watcher, policy.get_child_watcher())

m_can_use_pidfd.return_value = True
policy = self.create_policy()
self.assertIsNone(policy._watcher)
with self.assertWarns(DeprecationWarning):
watcher = policy.get_child_watcher()
self.assertIsInstance(watcher, asyncio.PidfdChildWatcher)

self.assertIs(policy._watcher, watcher)
with self.assertWarns(DeprecationWarning):
self.assertIs(watcher, policy.get_child_watcher())

class TestFunctional(unittest.TestCase):

def setUp(self):
Expand Down
6 changes: 2 additions & 4 deletions Lib/test/test_asyncio/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,10 +550,8 @@ def close_loop(loop):
policy = support.maybe_get_event_loop_policy()
if policy is not None:
try:
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
watcher = policy.get_child_watcher()
except NotImplementedError:
watcher = policy._watcher
except AttributeError:
# watcher is not implemented by EventLoopPolicy, e.g. Windows
pass
else:
Expand Down
Loading