From 3348f6a06714e1a9b06a5918e96dfa3e8f1bf578 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 22 Dec 2023 16:02:12 -0600 Subject: [PATCH 1/4] Enable async app --- jupyter_core/application.py | 40 +++++++++++++++++++++++++++++----- jupyter_core/utils/__init__.py | 32 +++++++++++++++++---------- tests/test_application.py | 12 ++++++++++ 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/jupyter_core/application.py b/jupyter_core/application.py index f2fe221..9bb1c16 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_async, ensure_dir_exists, get_event_loop # mypy: disable-error-code="no-untyped-call" @@ -237,8 +237,16 @@ def _dispatching(self) -> bool: subcommand = Unicode() - @catch_config_error + @t.overload + async def initialize(self, argv: t.Any = None) -> None: + ... + + @t.overload def initialize(self, argv: t.Any = None) -> None: + ... + + @catch_config_error + def initialize(self, argv: t.Any = None) -> t.Optional[t.Awaitable[None]]: """Initialize the application.""" # don't hook up crash handler before parsing command-line if argv is None: @@ -260,7 +268,15 @@ def initialize(self, argv: t.Any = None) -> None: if allow_insecure_writes: issue_insecure_write_warning() + @t.overload + async def start(self) -> None: + ... + + @t.overload def start(self) -> None: + ... + + def start(self) -> t.Optional[t.Awaitable[None]]: """Start the whole thing""" if self.subcommand: os.execv(self.subcommand, [self.subcommand] + self.argv[1:]) # noqa: S606 @@ -275,13 +291,27 @@ def start(self) -> None: raise NoStart() @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) + # Allow there to be a synchronous or asynchronous init method. + await ensure_async(app.initialize(argv)) + # Allow there to be a synchronous or asynchronous start method. + await ensure_async(app.start()) 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..1238a93 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -125,3 +125,15 @@ def test_runtime_dir_changed(): app.runtime_dir = td assert os.path.isdir(td) shutil.rmtree(td) + + +class AsyncApp(JupyterApp): + async def initialize(self, argv): + self.value = 10 + + async def start(self): + assert self.value == 10 + + +def test_async_app(): + AsyncApp.launch_instance() From d808f80dac695378f560b1f00031dbb8c7b4a673 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 22 Dec 2023 16:04:22 -0600 Subject: [PATCH 2/4] improve the test --- tests/test_application.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_application.py b/tests/test_application.py index 1238a93..58a4776 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -137,3 +137,5 @@ async def start(self): def test_async_app(): AsyncApp.launch_instance() + app = AsyncApp.instance() + assert app.value == 10 From 2754a86de3862bb67d66a160d3593f22179ee2c7 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 22 Dec 2023 16:18:51 -0600 Subject: [PATCH 3/4] fix async handling --- jupyter_core/application.py | 36 +++++++++++++++--------------------- jupyter_core/paths.py | 2 +- tests/test_application.py | 6 +++--- 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/jupyter_core/application.py b/jupyter_core/application.py index 9bb1c16..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_async, ensure_dir_exists, get_event_loop +from .utils import ensure_dir_exists, get_event_loop # mypy: disable-error-code="no-untyped-call" @@ -237,16 +237,8 @@ def _dispatching(self) -> bool: subcommand = Unicode() - @t.overload - async def initialize(self, argv: t.Any = None) -> None: - ... - - @t.overload - def initialize(self, argv: t.Any = None) -> None: - ... - @catch_config_error - def initialize(self, argv: t.Any = None) -> t.Optional[t.Awaitable[None]]: + def initialize(self, argv: t.Any = None) -> None: """Initialize the application.""" # don't hook up crash handler before parsing command-line if argv is None: @@ -267,16 +259,13 @@ def initialize(self, argv: t.Any = None) -> t.Optional[t.Awaitable[None]]: self.update_config(cl_config) if allow_insecure_writes: issue_insecure_write_warning() + return - @t.overload - async def start(self) -> None: - ... + async def initialize_async(self) -> None: + """Perform async initialization of the application, will be called + after synchronous initialize.""" - @t.overload def start(self) -> None: - ... - - def start(self) -> t.Optional[t.Awaitable[None]]: """Start the whole thing""" if self.subcommand: os.execv(self.subcommand, [self.subcommand] + self.argv[1:]) # noqa: S606 @@ -290,15 +279,20 @@ def start(self) -> t.Optional[t.Awaitable[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 async def _async_launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None: """Launch the instance from inside an event loop.""" try: app = cls.instance(**kwargs) - # Allow there to be a synchronous or asynchronous init method. - await ensure_async(app.initialize(argv)) - # Allow there to be a synchronous or asynchronous start method. - await ensure_async(app.start()) + app.initialize(argv) + await app.initialize_async() + app.start() + await app.start_async() except NoStart: return diff --git a/jupyter_core/paths.py b/jupyter_core/paths.py index 943f6a4..a4ffd72 100644 --- a/jupyter_core/paths.py +++ b/jupyter_core/paths.py @@ -174,7 +174,7 @@ def jupyter_data_dir() -> str: if sys.platform == "darwin": return str(Path(home, "Library", "Jupyter")) - if sys.platform == "win32": + if sys.platform == "win32": # type:ignore[unreachable] appdata = os.environ.get("APPDATA", None) if appdata: return str(Path(appdata, "jupyter").resolve()) diff --git a/tests/test_application.py b/tests/test_application.py index 58a4776..6935c8c 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -128,14 +128,14 @@ def test_runtime_dir_changed(): class AsyncApp(JupyterApp): - async def initialize(self, argv): + async def initialize_async(self): self.value = 10 - async def start(self): + async def start_async(self): assert self.value == 10 def test_async_app(): - AsyncApp.launch_instance() + AsyncApp.launch_instance([]) app = AsyncApp.instance() assert app.value == 10 From ca0df3efd7a26643f2a1698dee04f2abe0d0ef51 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 22 Dec 2023 16:20:34 -0600 Subject: [PATCH 4/4] fix typing --- jupyter_core/paths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_core/paths.py b/jupyter_core/paths.py index a4ffd72..943f6a4 100644 --- a/jupyter_core/paths.py +++ b/jupyter_core/paths.py @@ -174,7 +174,7 @@ def jupyter_data_dir() -> str: if sys.platform == "darwin": return str(Path(home, "Library", "Jupyter")) - if sys.platform == "win32": # type:ignore[unreachable] + if sys.platform == "win32": appdata = os.environ.get("APPDATA", None) if appdata: return str(Path(appdata, "jupyter").resolve())