Skip to content

Commit e33fb74

Browse files
authored
Enable async JupyterApp (#381)
* Enable async app * improve the test * fix async handling * fix typing
1 parent 29a9aaa commit e33fb74

File tree

3 files changed

+62
-16
lines changed

3 files changed

+62
-16
lines changed

jupyter_core/application.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
jupyter_path,
3030
jupyter_runtime_dir,
3131
)
32-
from .utils import ensure_dir_exists
32+
from .utils import ensure_dir_exists, get_event_loop
3333

3434
# mypy: disable-error-code="no-untyped-call"
3535

@@ -259,6 +259,11 @@ def initialize(self, argv: t.Any = None) -> None:
259259
self.update_config(cl_config)
260260
if allow_insecure_writes:
261261
issue_insecure_write_warning()
262+
return
263+
264+
async def initialize_async(self) -> None:
265+
"""Perform async initialization of the application, will be called
266+
after synchronous initialize."""
262267

263268
def start(self) -> None:
264269
"""Start the whole thing"""
@@ -274,14 +279,33 @@ def start(self) -> None:
274279
self.write_default_config()
275280
raise NoStart()
276281

282+
return
283+
284+
async def start_async(self) -> None:
285+
"""Perform async start of the app, will be called after sync start."""
286+
277287
@classmethod
278-
def launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None:
279-
"""Launch an instance of a Jupyter Application"""
288+
async def _async_launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None:
289+
"""Launch the instance from inside an event loop."""
280290
try:
281-
super().launch_instance(argv=argv, **kwargs)
291+
app = cls.instance(**kwargs)
292+
app.initialize(argv)
293+
await app.initialize_async()
294+
app.start()
295+
await app.start_async()
282296
except NoStart:
283297
return
284298

299+
@classmethod
300+
def launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None:
301+
"""Launch a global instance of this Application
302+
303+
If a global instance already exists, this reinitializes and starts it
304+
"""
305+
loop = get_event_loop()
306+
coro = cls._async_launch_instance(argv, **kwargs)
307+
loop.run_until_complete(coro)
308+
285309

286310
if __name__ == "__main__":
287311
JupyterApp.launch_instance()

jupyter_core/utils/__init__.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -158,18 +158,8 @@ def wrapped(*args: Any, **kwargs: Any) -> Any:
158158
except RuntimeError:
159159
pass
160160

161-
# Run the loop for this thread.
162-
# In Python 3.12, a deprecation warning is raised, which
163-
# may later turn into a RuntimeError. We handle both
164-
# cases.
165-
with warnings.catch_warnings():
166-
warnings.simplefilter("ignore", DeprecationWarning)
167-
try:
168-
loop = asyncio.get_event_loop()
169-
except RuntimeError:
170-
loop = asyncio.new_event_loop()
171-
asyncio.set_event_loop(loop)
172-
return loop.run_until_complete(inner)
161+
loop = get_event_loop()
162+
return loop.run_until_complete(inner)
173163

174164
wrapped.__doc__ = coro.__doc__
175165
return wrapped
@@ -194,3 +184,21 @@ async def ensure_async(obj: Awaitable[T] | T) -> T:
194184
return result
195185
# obj doesn't need to be awaited
196186
return cast(T, obj)
187+
188+
189+
def get_event_loop() -> asyncio.AbstractEventLoop:
190+
# Get the loop for this thread.
191+
# In Python 3.12, a deprecation warning is raised, which
192+
# may later turn into a RuntimeError. We handle both
193+
# cases.
194+
with warnings.catch_warnings():
195+
warnings.simplefilter("ignore", DeprecationWarning)
196+
try:
197+
loop = asyncio.get_event_loop()
198+
except RuntimeError:
199+
if sys.platform == "win32":
200+
loop = asyncio.WindowsSelectorEventLoopPolicy().new_event_loop()
201+
else:
202+
loop = asyncio.new_event_loop()
203+
asyncio.set_event_loop(loop)
204+
return loop

tests/test_application.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,17 @@ def test_runtime_dir_changed():
125125
app.runtime_dir = td
126126
assert os.path.isdir(td)
127127
shutil.rmtree(td)
128+
129+
130+
class AsyncApp(JupyterApp):
131+
async def initialize_async(self):
132+
self.value = 10
133+
134+
async def start_async(self):
135+
assert self.value == 10
136+
137+
138+
def test_async_app():
139+
AsyncApp.launch_instance([])
140+
app = AsyncApp.instance()
141+
assert app.value == 10

0 commit comments

Comments
 (0)