bpo-31033: Add a msg argument to Future.cancel() and Task.cancel() (GH-19979)
This commit is contained in:
parent
fe1176e882
commit
1ce5841eca
|
@ -170,7 +170,7 @@ Future Object
|
||||||
Returns the number of callbacks removed, which is typically 1,
|
Returns the number of callbacks removed, which is typically 1,
|
||||||
unless a callback was added more than once.
|
unless a callback was added more than once.
|
||||||
|
|
||||||
.. method:: cancel()
|
.. method:: cancel(msg=None)
|
||||||
|
|
||||||
Cancel the Future and schedule callbacks.
|
Cancel the Future and schedule callbacks.
|
||||||
|
|
||||||
|
@ -178,6 +178,9 @@ Future Object
|
||||||
Otherwise, change the Future's state to *cancelled*,
|
Otherwise, change the Future's state to *cancelled*,
|
||||||
schedule the callbacks, and return ``True``.
|
schedule the callbacks, and return ``True``.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.9
|
||||||
|
Added the ``msg`` parameter.
|
||||||
|
|
||||||
.. method:: exception()
|
.. method:: exception()
|
||||||
|
|
||||||
Return the exception that was set on this Future.
|
Return the exception that was set on this Future.
|
||||||
|
@ -255,3 +258,6 @@ the Future has a result::
|
||||||
- asyncio Future is not compatible with the
|
- asyncio Future is not compatible with the
|
||||||
:func:`concurrent.futures.wait` and
|
:func:`concurrent.futures.wait` and
|
||||||
:func:`concurrent.futures.as_completed` functions.
|
:func:`concurrent.futures.as_completed` functions.
|
||||||
|
|
||||||
|
- :meth:`asyncio.Future.cancel` accepts an optional ``msg`` argument,
|
||||||
|
but :func:`concurrent.futures.cancel` does not.
|
||||||
|
|
|
@ -724,7 +724,7 @@ Task Object
|
||||||
.. deprecated-removed:: 3.8 3.10
|
.. deprecated-removed:: 3.8 3.10
|
||||||
The *loop* parameter.
|
The *loop* parameter.
|
||||||
|
|
||||||
.. method:: cancel()
|
.. method:: cancel(msg=None)
|
||||||
|
|
||||||
Request the Task to be cancelled.
|
Request the Task to be cancelled.
|
||||||
|
|
||||||
|
@ -739,6 +739,9 @@ Task Object
|
||||||
suppressing cancellation completely is not common and is actively
|
suppressing cancellation completely is not common and is actively
|
||||||
discouraged.
|
discouraged.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.9
|
||||||
|
Added the ``msg`` parameter.
|
||||||
|
|
||||||
.. _asyncio_example_task_cancel:
|
.. _asyncio_example_task_cancel:
|
||||||
|
|
||||||
The following example illustrates how coroutines can intercept
|
The following example illustrates how coroutines can intercept
|
||||||
|
|
|
@ -51,6 +51,7 @@ class Future:
|
||||||
_exception = None
|
_exception = None
|
||||||
_loop = None
|
_loop = None
|
||||||
_source_traceback = None
|
_source_traceback = None
|
||||||
|
_cancel_message = None
|
||||||
|
|
||||||
# This field is used for a dual purpose:
|
# This field is used for a dual purpose:
|
||||||
# - Its presence is a marker to declare that a class implements
|
# - Its presence is a marker to declare that a class implements
|
||||||
|
@ -123,7 +124,7 @@ class Future:
|
||||||
raise RuntimeError("Future object is not initialized.")
|
raise RuntimeError("Future object is not initialized.")
|
||||||
return loop
|
return loop
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self, msg=None):
|
||||||
"""Cancel the future and schedule callbacks.
|
"""Cancel the future and schedule callbacks.
|
||||||
|
|
||||||
If the future is already done or cancelled, return False. Otherwise,
|
If the future is already done or cancelled, return False. Otherwise,
|
||||||
|
@ -134,6 +135,7 @@ class Future:
|
||||||
if self._state != _PENDING:
|
if self._state != _PENDING:
|
||||||
return False
|
return False
|
||||||
self._state = _CANCELLED
|
self._state = _CANCELLED
|
||||||
|
self._cancel_message = msg
|
||||||
self.__schedule_callbacks()
|
self.__schedule_callbacks()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -173,7 +175,9 @@ class Future:
|
||||||
the future is done and has an exception set, this exception is raised.
|
the future is done and has an exception set, this exception is raised.
|
||||||
"""
|
"""
|
||||||
if self._state == _CANCELLED:
|
if self._state == _CANCELLED:
|
||||||
raise exceptions.CancelledError
|
raise exceptions.CancelledError(
|
||||||
|
'' if self._cancel_message is None else self._cancel_message)
|
||||||
|
|
||||||
if self._state != _FINISHED:
|
if self._state != _FINISHED:
|
||||||
raise exceptions.InvalidStateError('Result is not ready.')
|
raise exceptions.InvalidStateError('Result is not ready.')
|
||||||
self.__log_traceback = False
|
self.__log_traceback = False
|
||||||
|
@ -190,7 +194,8 @@ class Future:
|
||||||
InvalidStateError.
|
InvalidStateError.
|
||||||
"""
|
"""
|
||||||
if self._state == _CANCELLED:
|
if self._state == _CANCELLED:
|
||||||
raise exceptions.CancelledError
|
raise exceptions.CancelledError(
|
||||||
|
'' if self._cancel_message is None else self._cancel_message)
|
||||||
if self._state != _FINISHED:
|
if self._state != _FINISHED:
|
||||||
raise exceptions.InvalidStateError('Exception is not set.')
|
raise exceptions.InvalidStateError('Exception is not set.')
|
||||||
self.__log_traceback = False
|
self.__log_traceback = False
|
||||||
|
|
|
@ -230,7 +230,7 @@ class Task(futures._PyFuture): # Inherit Python Task implementation
|
||||||
"""
|
"""
|
||||||
return base_tasks._task_print_stack(self, limit, file)
|
return base_tasks._task_print_stack(self, limit, file)
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self, msg=None):
|
||||||
"""Request that this task cancel itself.
|
"""Request that this task cancel itself.
|
||||||
|
|
||||||
This arranges for a CancelledError to be thrown into the
|
This arranges for a CancelledError to be thrown into the
|
||||||
|
@ -254,13 +254,14 @@ class Task(futures._PyFuture): # Inherit Python Task implementation
|
||||||
if self.done():
|
if self.done():
|
||||||
return False
|
return False
|
||||||
if self._fut_waiter is not None:
|
if self._fut_waiter is not None:
|
||||||
if self._fut_waiter.cancel():
|
if self._fut_waiter.cancel(msg=msg):
|
||||||
# Leave self._fut_waiter; it may be a Task that
|
# Leave self._fut_waiter; it may be a Task that
|
||||||
# catches and ignores the cancellation so we may have
|
# catches and ignores the cancellation so we may have
|
||||||
# to cancel it again later.
|
# to cancel it again later.
|
||||||
return True
|
return True
|
||||||
# It must be the case that self.__step is already scheduled.
|
# It must be the case that self.__step is already scheduled.
|
||||||
self._must_cancel = True
|
self._must_cancel = True
|
||||||
|
self._cancel_message = msg
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __step(self, exc=None):
|
def __step(self, exc=None):
|
||||||
|
@ -269,7 +270,8 @@ class Task(futures._PyFuture): # Inherit Python Task implementation
|
||||||
f'_step(): already done: {self!r}, {exc!r}')
|
f'_step(): already done: {self!r}, {exc!r}')
|
||||||
if self._must_cancel:
|
if self._must_cancel:
|
||||||
if not isinstance(exc, exceptions.CancelledError):
|
if not isinstance(exc, exceptions.CancelledError):
|
||||||
exc = exceptions.CancelledError()
|
exc = exceptions.CancelledError(''
|
||||||
|
if self._cancel_message is None else self._cancel_message)
|
||||||
self._must_cancel = False
|
self._must_cancel = False
|
||||||
coro = self._coro
|
coro = self._coro
|
||||||
self._fut_waiter = None
|
self._fut_waiter = None
|
||||||
|
@ -287,11 +289,15 @@ class Task(futures._PyFuture): # Inherit Python Task implementation
|
||||||
if self._must_cancel:
|
if self._must_cancel:
|
||||||
# Task is cancelled right before coro stops.
|
# Task is cancelled right before coro stops.
|
||||||
self._must_cancel = False
|
self._must_cancel = False
|
||||||
super().cancel()
|
super().cancel(msg=self._cancel_message)
|
||||||
else:
|
else:
|
||||||
super().set_result(exc.value)
|
super().set_result(exc.value)
|
||||||
except exceptions.CancelledError:
|
except exceptions.CancelledError as exc:
|
||||||
super().cancel() # I.e., Future.cancel(self).
|
if exc.args:
|
||||||
|
cancel_msg = exc.args[0]
|
||||||
|
else:
|
||||||
|
cancel_msg = None
|
||||||
|
super().cancel(msg=cancel_msg) # I.e., Future.cancel(self).
|
||||||
except (KeyboardInterrupt, SystemExit) as exc:
|
except (KeyboardInterrupt, SystemExit) as exc:
|
||||||
super().set_exception(exc)
|
super().set_exception(exc)
|
||||||
raise
|
raise
|
||||||
|
@ -319,7 +325,8 @@ class Task(futures._PyFuture): # Inherit Python Task implementation
|
||||||
self.__wakeup, context=self._context)
|
self.__wakeup, context=self._context)
|
||||||
self._fut_waiter = result
|
self._fut_waiter = result
|
||||||
if self._must_cancel:
|
if self._must_cancel:
|
||||||
if self._fut_waiter.cancel():
|
if self._fut_waiter.cancel(
|
||||||
|
msg=self._cancel_message):
|
||||||
self._must_cancel = False
|
self._must_cancel = False
|
||||||
else:
|
else:
|
||||||
new_exc = RuntimeError(
|
new_exc = RuntimeError(
|
||||||
|
@ -716,12 +723,12 @@ class _GatheringFuture(futures.Future):
|
||||||
self._children = children
|
self._children = children
|
||||||
self._cancel_requested = False
|
self._cancel_requested = False
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self, msg=None):
|
||||||
if self.done():
|
if self.done():
|
||||||
return False
|
return False
|
||||||
ret = False
|
ret = False
|
||||||
for child in self._children:
|
for child in self._children:
|
||||||
if child.cancel():
|
if child.cancel(msg=msg):
|
||||||
ret = True
|
ret = True
|
||||||
if ret:
|
if ret:
|
||||||
# If any child tasks were actually cancelled, we should
|
# If any child tasks were actually cancelled, we should
|
||||||
|
@ -780,7 +787,8 @@ def gather(*coros_or_futures, loop=None, return_exceptions=False):
|
||||||
# Check if 'fut' is cancelled first, as
|
# Check if 'fut' is cancelled first, as
|
||||||
# 'fut.exception()' will *raise* a CancelledError
|
# 'fut.exception()' will *raise* a CancelledError
|
||||||
# instead of returning it.
|
# instead of returning it.
|
||||||
exc = exceptions.CancelledError()
|
exc = exceptions.CancelledError(''
|
||||||
|
if fut._cancel_message is None else fut._cancel_message)
|
||||||
outer.set_exception(exc)
|
outer.set_exception(exc)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
@ -799,7 +807,9 @@ def gather(*coros_or_futures, loop=None, return_exceptions=False):
|
||||||
# Check if 'fut' is cancelled first, as
|
# Check if 'fut' is cancelled first, as
|
||||||
# 'fut.exception()' will *raise* a CancelledError
|
# 'fut.exception()' will *raise* a CancelledError
|
||||||
# instead of returning it.
|
# instead of returning it.
|
||||||
res = exceptions.CancelledError()
|
res = exceptions.CancelledError(
|
||||||
|
'' if fut._cancel_message is None else
|
||||||
|
fut._cancel_message)
|
||||||
else:
|
else:
|
||||||
res = fut.exception()
|
res = fut.exception()
|
||||||
if res is None:
|
if res is None:
|
||||||
|
@ -810,7 +820,9 @@ def gather(*coros_or_futures, loop=None, return_exceptions=False):
|
||||||
# If gather is being cancelled we must propagate the
|
# If gather is being cancelled we must propagate the
|
||||||
# cancellation regardless of *return_exceptions* argument.
|
# cancellation regardless of *return_exceptions* argument.
|
||||||
# See issue 32684.
|
# See issue 32684.
|
||||||
outer.set_exception(exceptions.CancelledError())
|
exc = exceptions.CancelledError(''
|
||||||
|
if fut._cancel_message is None else fut._cancel_message)
|
||||||
|
outer.set_exception(exc)
|
||||||
else:
|
else:
|
||||||
outer.set_result(results)
|
outer.set_result(results)
|
||||||
|
|
||||||
|
|
|
@ -75,9 +75,9 @@ class _OverlappedFuture(futures.Future):
|
||||||
self._loop.call_exception_handler(context)
|
self._loop.call_exception_handler(context)
|
||||||
self._ov = None
|
self._ov = None
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self, msg=None):
|
||||||
self._cancel_overlapped()
|
self._cancel_overlapped()
|
||||||
return super().cancel()
|
return super().cancel(msg=msg)
|
||||||
|
|
||||||
def set_exception(self, exception):
|
def set_exception(self, exception):
|
||||||
super().set_exception(exception)
|
super().set_exception(exception)
|
||||||
|
@ -149,9 +149,9 @@ class _BaseWaitHandleFuture(futures.Future):
|
||||||
|
|
||||||
self._unregister_wait_cb(None)
|
self._unregister_wait_cb(None)
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self, msg=None):
|
||||||
self._unregister_wait()
|
self._unregister_wait()
|
||||||
return super().cancel()
|
return super().cancel(msg=msg)
|
||||||
|
|
||||||
def set_exception(self, exception):
|
def set_exception(self, exception):
|
||||||
self._unregister_wait()
|
self._unregister_wait()
|
||||||
|
|
|
@ -201,6 +201,27 @@ class BaseFutureTests:
|
||||||
self.assertFalse(fut.cancelled())
|
self.assertFalse(fut.cancelled())
|
||||||
self.assertFalse(fut.done())
|
self.assertFalse(fut.done())
|
||||||
|
|
||||||
|
def test_future_cancel_message_getter(self):
|
||||||
|
f = self._new_future(loop=self.loop)
|
||||||
|
self.assertTrue(hasattr(f, '_cancel_message'))
|
||||||
|
self.assertEqual(f._cancel_message, None)
|
||||||
|
|
||||||
|
f.cancel('my message')
|
||||||
|
with self.assertRaises(asyncio.CancelledError):
|
||||||
|
self.loop.run_until_complete(f)
|
||||||
|
self.assertEqual(f._cancel_message, 'my message')
|
||||||
|
|
||||||
|
def test_future_cancel_message_setter(self):
|
||||||
|
f = self._new_future(loop=self.loop)
|
||||||
|
f.cancel('my message')
|
||||||
|
f._cancel_message = 'my new message'
|
||||||
|
self.assertEqual(f._cancel_message, 'my new message')
|
||||||
|
|
||||||
|
# Also check that the value is used for cancel().
|
||||||
|
with self.assertRaises(asyncio.CancelledError):
|
||||||
|
self.loop.run_until_complete(f)
|
||||||
|
self.assertEqual(f._cancel_message, 'my new message')
|
||||||
|
|
||||||
def test_cancel(self):
|
def test_cancel(self):
|
||||||
f = self._new_future(loop=self.loop)
|
f = self._new_future(loop=self.loop)
|
||||||
self.assertTrue(f.cancel())
|
self.assertTrue(f.cancel())
|
||||||
|
|
|
@ -103,6 +103,31 @@ class BaseTaskTests:
|
||||||
self.loop.set_task_factory(self.new_task)
|
self.loop.set_task_factory(self.new_task)
|
||||||
self.loop.create_future = lambda: self.new_future(self.loop)
|
self.loop.create_future = lambda: self.new_future(self.loop)
|
||||||
|
|
||||||
|
def test_task_cancel_message_getter(self):
|
||||||
|
async def coro():
|
||||||
|
pass
|
||||||
|
t = self.new_task(self.loop, coro())
|
||||||
|
self.assertTrue(hasattr(t, '_cancel_message'))
|
||||||
|
self.assertEqual(t._cancel_message, None)
|
||||||
|
|
||||||
|
t.cancel('my message')
|
||||||
|
with self.assertRaises(asyncio.CancelledError):
|
||||||
|
self.loop.run_until_complete(t)
|
||||||
|
self.assertEqual(t._cancel_message, 'my message')
|
||||||
|
|
||||||
|
def test_task_cancel_message_setter(self):
|
||||||
|
async def coro():
|
||||||
|
pass
|
||||||
|
t = self.new_task(self.loop, coro())
|
||||||
|
t.cancel('my message')
|
||||||
|
t._cancel_message = 'my new message'
|
||||||
|
self.assertEqual(t._cancel_message, 'my new message')
|
||||||
|
|
||||||
|
# Also check that the value is used for cancel().
|
||||||
|
with self.assertRaises(asyncio.CancelledError):
|
||||||
|
self.loop.run_until_complete(t)
|
||||||
|
self.assertEqual(t._cancel_message, 'my new message')
|
||||||
|
|
||||||
def test_task_del_collect(self):
|
def test_task_del_collect(self):
|
||||||
class Evil:
|
class Evil:
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
@ -520,6 +545,86 @@ class BaseTaskTests:
|
||||||
self.assertTrue(t.cancelled())
|
self.assertTrue(t.cancelled())
|
||||||
self.assertFalse(t.cancel())
|
self.assertFalse(t.cancel())
|
||||||
|
|
||||||
|
def test_cancel_with_message_then_future_result(self):
|
||||||
|
# Test Future.result() after calling cancel() with a message.
|
||||||
|
cases = [
|
||||||
|
((), ('',)),
|
||||||
|
((None,), ('',)),
|
||||||
|
(('my message',), ('my message',)),
|
||||||
|
# Non-string values should roundtrip.
|
||||||
|
((5,), (5,)),
|
||||||
|
]
|
||||||
|
for cancel_args, expected_args in cases:
|
||||||
|
with self.subTest(cancel_args=cancel_args):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
self.set_event_loop(loop)
|
||||||
|
|
||||||
|
async def sleep():
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
async def coro():
|
||||||
|
task = self.new_task(loop, sleep())
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
task.cancel(*cancel_args)
|
||||||
|
done, pending = await asyncio.wait([task])
|
||||||
|
task.result()
|
||||||
|
|
||||||
|
task = self.new_task(loop, coro())
|
||||||
|
with self.assertRaises(asyncio.CancelledError) as cm:
|
||||||
|
loop.run_until_complete(task)
|
||||||
|
exc = cm.exception
|
||||||
|
self.assertEqual(exc.args, expected_args)
|
||||||
|
|
||||||
|
def test_cancel_with_message_then_future_exception(self):
|
||||||
|
# Test Future.exception() after calling cancel() with a message.
|
||||||
|
cases = [
|
||||||
|
((), ('',)),
|
||||||
|
((None,), ('',)),
|
||||||
|
(('my message',), ('my message',)),
|
||||||
|
# Non-string values should roundtrip.
|
||||||
|
((5,), (5,)),
|
||||||
|
]
|
||||||
|
for cancel_args, expected_args in cases:
|
||||||
|
with self.subTest(cancel_args=cancel_args):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
self.set_event_loop(loop)
|
||||||
|
|
||||||
|
async def sleep():
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
async def coro():
|
||||||
|
task = self.new_task(loop, sleep())
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
task.cancel(*cancel_args)
|
||||||
|
done, pending = await asyncio.wait([task])
|
||||||
|
task.exception()
|
||||||
|
|
||||||
|
task = self.new_task(loop, coro())
|
||||||
|
with self.assertRaises(asyncio.CancelledError) as cm:
|
||||||
|
loop.run_until_complete(task)
|
||||||
|
exc = cm.exception
|
||||||
|
self.assertEqual(exc.args, expected_args)
|
||||||
|
|
||||||
|
def test_cancel_with_message_before_starting_task(self):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
self.set_event_loop(loop)
|
||||||
|
|
||||||
|
async def sleep():
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
async def coro():
|
||||||
|
task = self.new_task(loop, sleep())
|
||||||
|
# We deliberately leave out the sleep here.
|
||||||
|
task.cancel('my message')
|
||||||
|
done, pending = await asyncio.wait([task])
|
||||||
|
task.exception()
|
||||||
|
|
||||||
|
task = self.new_task(loop, coro())
|
||||||
|
with self.assertRaises(asyncio.CancelledError) as cm:
|
||||||
|
loop.run_until_complete(task)
|
||||||
|
exc = cm.exception
|
||||||
|
self.assertEqual(exc.args, ('my message',))
|
||||||
|
|
||||||
def test_cancel_yield(self):
|
def test_cancel_yield(self):
|
||||||
with self.assertWarns(DeprecationWarning):
|
with self.assertWarns(DeprecationWarning):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -2285,31 +2390,42 @@ class BaseTaskTests:
|
||||||
self.assertEqual(gather_task.result(), [42])
|
self.assertEqual(gather_task.result(), [42])
|
||||||
|
|
||||||
def test_cancel_gather_2(self):
|
def test_cancel_gather_2(self):
|
||||||
loop = asyncio.new_event_loop()
|
cases = [
|
||||||
self.addCleanup(loop.close)
|
((), ('',)),
|
||||||
|
((None,), ('',)),
|
||||||
|
(('my message',), ('my message',)),
|
||||||
|
# Non-string values should roundtrip.
|
||||||
|
((5,), (5,)),
|
||||||
|
]
|
||||||
|
for cancel_args, expected_args in cases:
|
||||||
|
with self.subTest(cancel_args=cancel_args):
|
||||||
|
|
||||||
async def test():
|
loop = asyncio.new_event_loop()
|
||||||
time = 0
|
self.addCleanup(loop.close)
|
||||||
while True:
|
|
||||||
time += 0.05
|
|
||||||
await asyncio.gather(asyncio.sleep(0.05),
|
|
||||||
return_exceptions=True,
|
|
||||||
loop=loop)
|
|
||||||
if time > 1:
|
|
||||||
return
|
|
||||||
|
|
||||||
async def main():
|
async def test():
|
||||||
qwe = self.new_task(loop, test())
|
time = 0
|
||||||
await asyncio.sleep(0.2)
|
while True:
|
||||||
qwe.cancel()
|
time += 0.05
|
||||||
try:
|
await asyncio.gather(asyncio.sleep(0.05),
|
||||||
await qwe
|
return_exceptions=True,
|
||||||
except asyncio.CancelledError:
|
loop=loop)
|
||||||
pass
|
if time > 1:
|
||||||
else:
|
return
|
||||||
self.fail('gather did not propagate the cancellation request')
|
|
||||||
|
|
||||||
loop.run_until_complete(main())
|
async def main():
|
||||||
|
qwe = self.new_task(loop, test())
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
qwe.cancel(*cancel_args)
|
||||||
|
try:
|
||||||
|
await qwe
|
||||||
|
except asyncio.CancelledError as exc:
|
||||||
|
self.assertEqual(exc.args, expected_args)
|
||||||
|
else:
|
||||||
|
self.fail('gather did not propagate the cancellation '
|
||||||
|
'request')
|
||||||
|
|
||||||
|
loop.run_until_complete(main())
|
||||||
|
|
||||||
def test_exception_traceback(self):
|
def test_exception_traceback(self):
|
||||||
# See http://bugs.python.org/issue28843
|
# See http://bugs.python.org/issue28843
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Add a ``msg`` argument to :meth:`Future.cancel` and :meth:`Task.cancel`.
|
|
@ -67,6 +67,7 @@ typedef enum {
|
||||||
PyObject *prefix##_exception; \
|
PyObject *prefix##_exception; \
|
||||||
PyObject *prefix##_result; \
|
PyObject *prefix##_result; \
|
||||||
PyObject *prefix##_source_tb; \
|
PyObject *prefix##_source_tb; \
|
||||||
|
PyObject *prefix##_cancel_msg; \
|
||||||
fut_state prefix##_state; \
|
fut_state prefix##_state; \
|
||||||
int prefix##_log_tb; \
|
int prefix##_log_tb; \
|
||||||
int prefix##_blocking; \
|
int prefix##_blocking; \
|
||||||
|
@ -480,6 +481,7 @@ future_init(FutureObj *fut, PyObject *loop)
|
||||||
Py_CLEAR(fut->fut_result);
|
Py_CLEAR(fut->fut_result);
|
||||||
Py_CLEAR(fut->fut_exception);
|
Py_CLEAR(fut->fut_exception);
|
||||||
Py_CLEAR(fut->fut_source_tb);
|
Py_CLEAR(fut->fut_source_tb);
|
||||||
|
Py_CLEAR(fut->fut_cancel_msg);
|
||||||
|
|
||||||
fut->fut_state = STATE_PENDING;
|
fut->fut_state = STATE_PENDING;
|
||||||
fut->fut_log_tb = 0;
|
fut->fut_log_tb = 0;
|
||||||
|
@ -594,11 +596,33 @@ future_set_exception(FutureObj *fut, PyObject *exc)
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
create_cancelled_error(PyObject *msg)
|
||||||
|
{
|
||||||
|
PyObject *exc;
|
||||||
|
if (msg == NULL || msg == Py_None) {
|
||||||
|
msg = PyUnicode_FromString("");
|
||||||
|
exc = PyObject_CallOneArg(asyncio_CancelledError, msg);
|
||||||
|
Py_DECREF(msg);
|
||||||
|
} else {
|
||||||
|
exc = PyObject_CallOneArg(asyncio_CancelledError, msg);
|
||||||
|
}
|
||||||
|
return exc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
set_cancelled_error(PyObject *msg)
|
||||||
|
{
|
||||||
|
PyObject *exc = create_cancelled_error(msg);
|
||||||
|
PyErr_SetObject(asyncio_CancelledError, exc);
|
||||||
|
Py_DECREF(exc);
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
future_get_result(FutureObj *fut, PyObject **result)
|
future_get_result(FutureObj *fut, PyObject **result)
|
||||||
{
|
{
|
||||||
if (fut->fut_state == STATE_CANCELLED) {
|
if (fut->fut_state == STATE_CANCELLED) {
|
||||||
PyErr_SetNone(asyncio_CancelledError);
|
set_cancelled_error(fut->fut_cancel_msg);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -695,7 +719,7 @@ future_add_done_callback(FutureObj *fut, PyObject *arg, PyObject *ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
future_cancel(FutureObj *fut)
|
future_cancel(FutureObj *fut, PyObject *msg)
|
||||||
{
|
{
|
||||||
fut->fut_log_tb = 0;
|
fut->fut_log_tb = 0;
|
||||||
|
|
||||||
|
@ -704,6 +728,9 @@ future_cancel(FutureObj *fut)
|
||||||
}
|
}
|
||||||
fut->fut_state = STATE_CANCELLED;
|
fut->fut_state = STATE_CANCELLED;
|
||||||
|
|
||||||
|
Py_XINCREF(msg);
|
||||||
|
Py_XSETREF(fut->fut_cancel_msg, msg);
|
||||||
|
|
||||||
if (future_schedule_callbacks(fut) == -1) {
|
if (future_schedule_callbacks(fut) == -1) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
@ -749,6 +776,7 @@ FutureObj_clear(FutureObj *fut)
|
||||||
Py_CLEAR(fut->fut_result);
|
Py_CLEAR(fut->fut_result);
|
||||||
Py_CLEAR(fut->fut_exception);
|
Py_CLEAR(fut->fut_exception);
|
||||||
Py_CLEAR(fut->fut_source_tb);
|
Py_CLEAR(fut->fut_source_tb);
|
||||||
|
Py_CLEAR(fut->fut_cancel_msg);
|
||||||
Py_CLEAR(fut->dict);
|
Py_CLEAR(fut->dict);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -763,6 +791,7 @@ FutureObj_traverse(FutureObj *fut, visitproc visit, void *arg)
|
||||||
Py_VISIT(fut->fut_result);
|
Py_VISIT(fut->fut_result);
|
||||||
Py_VISIT(fut->fut_exception);
|
Py_VISIT(fut->fut_exception);
|
||||||
Py_VISIT(fut->fut_source_tb);
|
Py_VISIT(fut->fut_source_tb);
|
||||||
|
Py_VISIT(fut->fut_cancel_msg);
|
||||||
Py_VISIT(fut->dict);
|
Py_VISIT(fut->dict);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -828,7 +857,7 @@ _asyncio_Future_exception_impl(FutureObj *self)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self->fut_state == STATE_CANCELLED) {
|
if (self->fut_state == STATE_CANCELLED) {
|
||||||
PyErr_SetNone(asyncio_CancelledError);
|
set_cancelled_error(self->fut_cancel_msg);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1029,6 +1058,8 @@ fail:
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
_asyncio.Future.cancel
|
_asyncio.Future.cancel
|
||||||
|
|
||||||
|
msg: object = None
|
||||||
|
|
||||||
Cancel the future and schedule callbacks.
|
Cancel the future and schedule callbacks.
|
||||||
|
|
||||||
If the future is already done or cancelled, return False. Otherwise,
|
If the future is already done or cancelled, return False. Otherwise,
|
||||||
|
@ -1037,11 +1068,11 @@ return True.
|
||||||
[clinic start generated code]*/
|
[clinic start generated code]*/
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
_asyncio_Future_cancel_impl(FutureObj *self)
|
_asyncio_Future_cancel_impl(FutureObj *self, PyObject *msg)
|
||||||
/*[clinic end generated code: output=e45b932ba8bd68a1 input=515709a127995109]*/
|
/*[clinic end generated code: output=3edebbc668e5aba3 input=925eb545251f2c5a]*/
|
||||||
{
|
{
|
||||||
ENSURE_FUTURE_ALIVE(self)
|
ENSURE_FUTURE_ALIVE(self)
|
||||||
return future_cancel(self);
|
return future_cancel(self, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
|
@ -1254,6 +1285,29 @@ FutureObj_get_source_traceback(FutureObj *fut, void *Py_UNUSED(ignored))
|
||||||
return fut->fut_source_tb;
|
return fut->fut_source_tb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
FutureObj_get_cancel_message(FutureObj *fut, void *Py_UNUSED(ignored))
|
||||||
|
{
|
||||||
|
if (fut->fut_cancel_msg == NULL) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
Py_INCREF(fut->fut_cancel_msg);
|
||||||
|
return fut->fut_cancel_msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
FutureObj_set_cancel_message(FutureObj *fut, PyObject *msg,
|
||||||
|
void *Py_UNUSED(ignored))
|
||||||
|
{
|
||||||
|
if (msg == NULL) {
|
||||||
|
PyErr_SetString(PyExc_AttributeError, "cannot delete attribute");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
Py_INCREF(msg);
|
||||||
|
Py_XSETREF(fut->fut_cancel_msg, msg);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
FutureObj_get_state(FutureObj *fut, void *Py_UNUSED(ignored))
|
FutureObj_get_state(FutureObj *fut, void *Py_UNUSED(ignored))
|
||||||
{
|
{
|
||||||
|
@ -1422,7 +1476,10 @@ static PyMethodDef FutureType_methods[] = {
|
||||||
{"_exception", (getter)FutureObj_get_exception, NULL, NULL}, \
|
{"_exception", (getter)FutureObj_get_exception, NULL, NULL}, \
|
||||||
{"_log_traceback", (getter)FutureObj_get_log_traceback, \
|
{"_log_traceback", (getter)FutureObj_get_log_traceback, \
|
||||||
(setter)FutureObj_set_log_traceback, NULL}, \
|
(setter)FutureObj_set_log_traceback, NULL}, \
|
||||||
{"_source_traceback", (getter)FutureObj_get_source_traceback, NULL, NULL},
|
{"_source_traceback", (getter)FutureObj_get_source_traceback, \
|
||||||
|
NULL, NULL}, \
|
||||||
|
{"_cancel_message", (getter)FutureObj_get_cancel_message, \
|
||||||
|
(setter)FutureObj_set_cancel_message, NULL},
|
||||||
|
|
||||||
static PyGetSetDef FutureType_getsetlist[] = {
|
static PyGetSetDef FutureType_getsetlist[] = {
|
||||||
FUTURE_COMMON_GETSETLIST
|
FUTURE_COMMON_GETSETLIST
|
||||||
|
@ -2189,6 +2246,8 @@ _asyncio_Task__repr_info_impl(TaskObj *self)
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
_asyncio.Task.cancel
|
_asyncio.Task.cancel
|
||||||
|
|
||||||
|
msg: object = None
|
||||||
|
|
||||||
Request that this task cancel itself.
|
Request that this task cancel itself.
|
||||||
|
|
||||||
This arranges for a CancelledError to be thrown into the
|
This arranges for a CancelledError to be thrown into the
|
||||||
|
@ -2210,8 +2269,8 @@ was not called).
|
||||||
[clinic start generated code]*/
|
[clinic start generated code]*/
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
_asyncio_Task_cancel_impl(TaskObj *self)
|
_asyncio_Task_cancel_impl(TaskObj *self, PyObject *msg)
|
||||||
/*[clinic end generated code: output=6bfc0479da9d5757 input=13f9bf496695cb52]*/
|
/*[clinic end generated code: output=c66b60d41c74f9f1 input=f4ff8e8ffc5f1c00]*/
|
||||||
{
|
{
|
||||||
self->task_log_tb = 0;
|
self->task_log_tb = 0;
|
||||||
|
|
||||||
|
@ -2223,7 +2282,8 @@ _asyncio_Task_cancel_impl(TaskObj *self)
|
||||||
PyObject *res;
|
PyObject *res;
|
||||||
int is_true;
|
int is_true;
|
||||||
|
|
||||||
res = _PyObject_CallMethodIdNoArgs(self->task_fut_waiter, &PyId_cancel);
|
res = _PyObject_CallMethodIdOneArg(self->task_fut_waiter,
|
||||||
|
&PyId_cancel, msg);
|
||||||
if (res == NULL) {
|
if (res == NULL) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
@ -2240,6 +2300,8 @@ _asyncio_Task_cancel_impl(TaskObj *self)
|
||||||
}
|
}
|
||||||
|
|
||||||
self->task_must_cancel = 1;
|
self->task_must_cancel = 1;
|
||||||
|
Py_XINCREF(msg);
|
||||||
|
Py_XSETREF(self->task_cancel_msg, msg);
|
||||||
Py_RETURN_TRUE;
|
Py_RETURN_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2623,7 +2685,8 @@ task_step_impl(TaskObj *task, PyObject *exc)
|
||||||
|
|
||||||
if (!exc) {
|
if (!exc) {
|
||||||
/* exc was not a CancelledError */
|
/* exc was not a CancelledError */
|
||||||
exc = PyObject_CallNoArgs(asyncio_CancelledError);
|
exc = create_cancelled_error(task->task_cancel_msg);
|
||||||
|
|
||||||
if (!exc) {
|
if (!exc) {
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
@ -2672,7 +2735,7 @@ task_step_impl(TaskObj *task, PyObject *exc)
|
||||||
if (task->task_must_cancel) {
|
if (task->task_must_cancel) {
|
||||||
// Task is cancelled right before coro stops.
|
// Task is cancelled right before coro stops.
|
||||||
task->task_must_cancel = 0;
|
task->task_must_cancel = 0;
|
||||||
res = future_cancel((FutureObj*)task);
|
res = future_cancel((FutureObj*)task, task->task_cancel_msg);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
res = future_set_result((FutureObj*)task, o);
|
res = future_set_result((FutureObj*)task, o);
|
||||||
|
@ -2689,8 +2752,26 @@ task_step_impl(TaskObj *task, PyObject *exc)
|
||||||
|
|
||||||
if (PyErr_ExceptionMatches(asyncio_CancelledError)) {
|
if (PyErr_ExceptionMatches(asyncio_CancelledError)) {
|
||||||
/* CancelledError */
|
/* CancelledError */
|
||||||
PyErr_Clear();
|
PyErr_Fetch(&et, &ev, &tb);
|
||||||
return future_cancel((FutureObj*)task);
|
|
||||||
|
PyObject *cancel_msg;
|
||||||
|
if (ev != NULL && PyExceptionInstance_Check(ev)) {
|
||||||
|
PyObject *exc_args = ((PyBaseExceptionObject*)ev)->args;
|
||||||
|
Py_ssize_t size = PyTuple_GET_SIZE(exc_args);
|
||||||
|
if (size > 0) {
|
||||||
|
cancel_msg = PyTuple_GET_ITEM(exc_args, 0);
|
||||||
|
} else {
|
||||||
|
cancel_msg = NULL;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cancel_msg = ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_DECREF(et);
|
||||||
|
Py_XDECREF(tb);
|
||||||
|
Py_XDECREF(ev);
|
||||||
|
|
||||||
|
return future_cancel((FutureObj*)task, cancel_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Some other exception; pop it and call Task.set_exception() */
|
/* Some other exception; pop it and call Task.set_exception() */
|
||||||
|
@ -2770,7 +2851,8 @@ task_step_impl(TaskObj *task, PyObject *exc)
|
||||||
if (task->task_must_cancel) {
|
if (task->task_must_cancel) {
|
||||||
PyObject *r;
|
PyObject *r;
|
||||||
int is_true;
|
int is_true;
|
||||||
r = _PyObject_CallMethodIdNoArgs(result, &PyId_cancel);
|
r = _PyObject_CallMethodIdOneArg(result, &PyId_cancel,
|
||||||
|
task->task_cancel_msg);
|
||||||
if (r == NULL) {
|
if (r == NULL) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
@ -2861,7 +2943,8 @@ task_step_impl(TaskObj *task, PyObject *exc)
|
||||||
if (task->task_must_cancel) {
|
if (task->task_must_cancel) {
|
||||||
PyObject *r;
|
PyObject *r;
|
||||||
int is_true;
|
int is_true;
|
||||||
r = _PyObject_CallMethodIdNoArgs(result, &PyId_cancel);
|
r = _PyObject_CallMethodIdOneArg(result, &PyId_cancel,
|
||||||
|
task->task_cancel_msg);
|
||||||
if (r == NULL) {
|
if (r == NULL) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,7 +174,7 @@ PyDoc_STRVAR(_asyncio_Future_remove_done_callback__doc__,
|
||||||
{"remove_done_callback", (PyCFunction)_asyncio_Future_remove_done_callback, METH_O, _asyncio_Future_remove_done_callback__doc__},
|
{"remove_done_callback", (PyCFunction)_asyncio_Future_remove_done_callback, METH_O, _asyncio_Future_remove_done_callback__doc__},
|
||||||
|
|
||||||
PyDoc_STRVAR(_asyncio_Future_cancel__doc__,
|
PyDoc_STRVAR(_asyncio_Future_cancel__doc__,
|
||||||
"cancel($self, /)\n"
|
"cancel($self, /, msg=None)\n"
|
||||||
"--\n"
|
"--\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Cancel the future and schedule callbacks.\n"
|
"Cancel the future and schedule callbacks.\n"
|
||||||
|
@ -184,15 +184,34 @@ PyDoc_STRVAR(_asyncio_Future_cancel__doc__,
|
||||||
"return True.");
|
"return True.");
|
||||||
|
|
||||||
#define _ASYNCIO_FUTURE_CANCEL_METHODDEF \
|
#define _ASYNCIO_FUTURE_CANCEL_METHODDEF \
|
||||||
{"cancel", (PyCFunction)_asyncio_Future_cancel, METH_NOARGS, _asyncio_Future_cancel__doc__},
|
{"cancel", (PyCFunction)(void(*)(void))_asyncio_Future_cancel, METH_FASTCALL|METH_KEYWORDS, _asyncio_Future_cancel__doc__},
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
_asyncio_Future_cancel_impl(FutureObj *self);
|
_asyncio_Future_cancel_impl(FutureObj *self, PyObject *msg);
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
_asyncio_Future_cancel(FutureObj *self, PyObject *Py_UNUSED(ignored))
|
_asyncio_Future_cancel(FutureObj *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
|
||||||
{
|
{
|
||||||
return _asyncio_Future_cancel_impl(self);
|
PyObject *return_value = NULL;
|
||||||
|
static const char * const _keywords[] = {"msg", NULL};
|
||||||
|
static _PyArg_Parser _parser = {NULL, _keywords, "cancel", 0};
|
||||||
|
PyObject *argsbuf[1];
|
||||||
|
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0;
|
||||||
|
PyObject *msg = Py_None;
|
||||||
|
|
||||||
|
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 0, 1, 0, argsbuf);
|
||||||
|
if (!args) {
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
if (!noptargs) {
|
||||||
|
goto skip_optional_pos;
|
||||||
|
}
|
||||||
|
msg = args[0];
|
||||||
|
skip_optional_pos:
|
||||||
|
return_value = _asyncio_Future_cancel_impl(self, msg);
|
||||||
|
|
||||||
|
exit:
|
||||||
|
return return_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyDoc_STRVAR(_asyncio_Future_cancelled__doc__,
|
PyDoc_STRVAR(_asyncio_Future_cancelled__doc__,
|
||||||
|
@ -413,7 +432,7 @@ _asyncio_Task__repr_info(TaskObj *self, PyObject *Py_UNUSED(ignored))
|
||||||
}
|
}
|
||||||
|
|
||||||
PyDoc_STRVAR(_asyncio_Task_cancel__doc__,
|
PyDoc_STRVAR(_asyncio_Task_cancel__doc__,
|
||||||
"cancel($self, /)\n"
|
"cancel($self, /, msg=None)\n"
|
||||||
"--\n"
|
"--\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Request that this task cancel itself.\n"
|
"Request that this task cancel itself.\n"
|
||||||
|
@ -436,15 +455,34 @@ PyDoc_STRVAR(_asyncio_Task_cancel__doc__,
|
||||||
"was not called).");
|
"was not called).");
|
||||||
|
|
||||||
#define _ASYNCIO_TASK_CANCEL_METHODDEF \
|
#define _ASYNCIO_TASK_CANCEL_METHODDEF \
|
||||||
{"cancel", (PyCFunction)_asyncio_Task_cancel, METH_NOARGS, _asyncio_Task_cancel__doc__},
|
{"cancel", (PyCFunction)(void(*)(void))_asyncio_Task_cancel, METH_FASTCALL|METH_KEYWORDS, _asyncio_Task_cancel__doc__},
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
_asyncio_Task_cancel_impl(TaskObj *self);
|
_asyncio_Task_cancel_impl(TaskObj *self, PyObject *msg);
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
_asyncio_Task_cancel(TaskObj *self, PyObject *Py_UNUSED(ignored))
|
_asyncio_Task_cancel(TaskObj *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
|
||||||
{
|
{
|
||||||
return _asyncio_Task_cancel_impl(self);
|
PyObject *return_value = NULL;
|
||||||
|
static const char * const _keywords[] = {"msg", NULL};
|
||||||
|
static _PyArg_Parser _parser = {NULL, _keywords, "cancel", 0};
|
||||||
|
PyObject *argsbuf[1];
|
||||||
|
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0;
|
||||||
|
PyObject *msg = Py_None;
|
||||||
|
|
||||||
|
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 0, 1, 0, argsbuf);
|
||||||
|
if (!args) {
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
if (!noptargs) {
|
||||||
|
goto skip_optional_pos;
|
||||||
|
}
|
||||||
|
msg = args[0];
|
||||||
|
skip_optional_pos:
|
||||||
|
return_value = _asyncio_Task_cancel_impl(self, msg);
|
||||||
|
|
||||||
|
exit:
|
||||||
|
return return_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyDoc_STRVAR(_asyncio_Task_get_stack__doc__,
|
PyDoc_STRVAR(_asyncio_Task_get_stack__doc__,
|
||||||
|
@ -832,4 +870,4 @@ _asyncio__leave_task(PyObject *module, PyObject *const *args, Py_ssize_t nargs,
|
||||||
exit:
|
exit:
|
||||||
return return_value;
|
return return_value;
|
||||||
}
|
}
|
||||||
/*[clinic end generated code: output=585ba1f8de5b4103 input=a9049054013a1b77]*/
|
/*[clinic end generated code: output=6ed4cfda8fc516ad input=a9049054013a1b77]*/
|
||||||
|
|
Loading…
Reference in New Issue