From 00673f98cd73e0bc8e3d4ce7edb30f44afa497e1 Mon Sep 17 00:00:00 2001 From: Zachary Ware Date: Tue, 31 Jan 2023 16:06:13 -0600 Subject: [PATCH 1/4] Add missing __unittest = True marker to async_case.py --- Lib/unittest/async_case.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index bd2a471156065bd..ef05251909a6b8b 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -6,6 +6,9 @@ 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 ab3633fcf1f0fa39e677ba53011c089de06379d4 Mon Sep 17 00:00:00 2001 From: Zachary Ware Date: Tue, 31 Jan 2023 16:09:05 -0600 Subject: [PATCH 2/4] Remove out-of-date implementation comment --- Lib/unittest/async_case.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index ef05251909a6b8b..60a24bf05ccd62c 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -13,19 +13,6 @@ 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. From f3eb654141c32193a9f59a0a139ff4b4b0f31a7f Mon Sep 17 00:00:00 2001 From: Zachary Ware Date: Tue, 31 Jan 2023 21:43:02 -0600 Subject: [PATCH 3/4] Run the entire test in the test context This is a broken commit; the `asyncioRunner.run` call raises a `RuntimeError` because the context is already entered. Not passing the context to `asyncioRunner.run` results in test failures, but the tests do actually run. --- Lib/unittest/async_case.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index 60a24bf05ccd62c..a930352e20f05d6 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -73,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): @@ -83,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' @@ -118,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): From 075971fd38099ae7dd8e5f2b63451dad910d5699 Mon Sep 17 00:00:00 2001 From: Zachary Ware Date: Tue, 31 Jan 2023 21:45:23 -0600 Subject: [PATCH 4/4] DO-NOT-MERGE: Hack around the inability to re-enter a running context --- Lib/asyncio/events.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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: