diff --git a/jupyter_core/application.py b/jupyter_core/application.py index f2fe221..cdc1de0 100644 --- a/jupyter_core/application.py +++ b/jupyter_core/application.py @@ -29,7 +29,7 @@ jupyter_path, jupyter_runtime_dir, ) -from .utils import ensure_dir_exists +from .utils import ensure_dir_exists, get_event_loop # mypy: disable-error-code="no-untyped-call" @@ -259,6 +259,11 @@ def initialize(self, argv: t.Any = None) -> None: self.update_config(cl_config) if allow_insecure_writes: issue_insecure_write_warning() + return + + async def initialize_async(self) -> None: + """Perform async initialization of the application, will be called + after synchronous initialize.""" def start(self) -> None: """Start the whole thing""" @@ -274,14 +279,33 @@ def start(self) -> None: self.write_default_config() raise NoStart() + return + + async def start_async(self) -> None: + """Perform async start of the app, will be called after sync start.""" + @classmethod - def launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None: - """Launch an instance of a Jupyter Application""" + async def _async_launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None: + """Launch the instance from inside an event loop.""" try: - super().launch_instance(argv=argv, **kwargs) + app = cls.instance(**kwargs) + app.initialize(argv) + await app.initialize_async() + app.start() + await app.start_async() except NoStart: return + @classmethod + def launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None: + """Launch a global instance of this Application + + If a global instance already exists, this reinitializes and starts it + """ + loop = get_event_loop() + coro = cls._async_launch_instance(argv, **kwargs) + loop.run_until_complete(coro) + if __name__ == "__main__": JupyterApp.launch_instance() diff --git a/jupyter_core/utils/__init__.py b/jupyter_core/utils/__init__.py index e8e1158..a82cac1 100644 --- a/jupyter_core/utils/__init__.py +++ b/jupyter_core/utils/__init__.py @@ -158,18 +158,8 @@ def wrapped(*args: Any, **kwargs: Any) -> Any: except RuntimeError: pass - # Run the loop for this thread. - # In Python 3.12, a deprecation warning is raised, which - # may later turn into a RuntimeError. We handle both - # cases. - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - try: - loop = asyncio.get_event_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - return loop.run_until_complete(inner) + loop = get_event_loop() + return loop.run_until_complete(inner) wrapped.__doc__ = coro.__doc__ return wrapped @@ -194,3 +184,21 @@ async def ensure_async(obj: Awaitable[T] | T) -> T: return result # obj doesn't need to be awaited return cast(T, obj) + + +def get_event_loop() -> asyncio.AbstractEventLoop: + # Get the loop for this thread. + # In Python 3.12, a deprecation warning is raised, which + # may later turn into a RuntimeError. We handle both + # cases. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + try: + loop = asyncio.get_event_loop() + except RuntimeError: + if sys.platform == "win32": + loop = asyncio.WindowsSelectorEventLoopPolicy().new_event_loop() + else: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop diff --git a/tests/test_application.py b/tests/test_application.py index 6bc2d89..6935c8c 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -125,3 +125,17 @@ def test_runtime_dir_changed(): app.runtime_dir = td assert os.path.isdir(td) shutil.rmtree(td) + + +class AsyncApp(JupyterApp): + async def initialize_async(self): + self.value = 10 + + async def start_async(self): + assert self.value == 10 + + +def test_async_app(): + AsyncApp.launch_instance([]) + app = AsyncApp.instance() + assert app.value == 10