diff --git a/Lib/asyncio/events.py b/Lib/asyncio/events.py index ce44942186b2721..f2bafb275993b0a 100644 --- a/Lib/asyncio/events.py +++ b/Lib/asyncio/events.py @@ -81,7 +81,13 @@ def cancelled(self): def _run(self): try: - self._context.run(self._callback, *self._args) + try: + self._context.run(self._callback, *self._args) + except RuntimeError as e: + if 'cannot enter context' not in str(e): + raise + # XXX: HACK! + self._callback(*self._args) except (SystemExit, KeyboardInterrupt): raise except BaseException as exc: diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index bd2a471156065bd..a930352e20f05d6 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -6,23 +6,13 @@ from .case import TestCase +__unittest = True + + class IsolatedAsyncioTestCase(TestCase): # Names intentionally have a long prefix # to reduce a chance of clashing with user-defined attributes # from inherited test case - # - # The class doesn't call loop.run_until_complete(self.setUp()) and family - # but uses a different approach: - # 1. create a long-running task that reads self.setUp() - # awaitable from queue along with a future - # 2. await the awaitable object passing in and set the result - # into the future object - # 3. Outer code puts the awaitable and the future object into a queue - # with waiting for the future - # The trick is necessary because every run_until_complete() call - # creates a new task with embedded ContextVar context. - # To share contextvars between setUp(), test and tearDown() we need to execute - # them inside the same task. # Note: the test case modifies event loop policy if the policy was not instantiated # yet. @@ -83,7 +73,7 @@ def _callSetUp(self): # so that setUp functions can use get_event_loop() and get the # correct loop instance. self._asyncioRunner.get_loop() - self._asyncioTestContext.run(self.setUp) + self.setUp() self._callAsync(self.asyncSetUp) def _callTestMethod(self, method): @@ -93,28 +83,30 @@ def _callTestMethod(self, method): def _callTearDown(self): self._callAsync(self.asyncTearDown) - self._asyncioTestContext.run(self.tearDown) + self.tearDown() def _callCleanup(self, function, *args, **kwargs): self._callMaybeAsync(function, *args, **kwargs) def _callAsync(self, func, /, *args, **kwargs): assert self._asyncioRunner is not None, 'asyncio runner is not initialized' - assert inspect.iscoroutinefunction(func), f'{func!r} is not an async function' + result = func(*args, **kwargs) + assert inspect.isawaitable(result), f'{func!r} does not return an awaitable' return self._asyncioRunner.run( - func(*args, **kwargs), + result, context=self._asyncioTestContext ) def _callMaybeAsync(self, func, /, *args, **kwargs): assert self._asyncioRunner is not None, 'asyncio runner is not initialized' - if inspect.iscoroutinefunction(func): + result = func(*args, **kwargs) + if inspect.isawaitable(result): return self._asyncioRunner.run( - func(*args, **kwargs), + result, context=self._asyncioTestContext, ) else: - return self._asyncioTestContext.run(func, *args, **kwargs) + return result def _setupAsyncioRunner(self): assert self._asyncioRunner is None, 'asyncio runner is already initialized' @@ -128,13 +120,13 @@ def _tearDownAsyncioRunner(self): def run(self, result=None): self._setupAsyncioRunner() try: - return super().run(result) + return self._asyncioTestContext.run(super().run, result) finally: self._tearDownAsyncioRunner() def debug(self): self._setupAsyncioRunner() - super().debug() + self._asyncioTestContext.run(super().debug) self._tearDownAsyncioRunner() def __del__(self):