textual icon indicating copy to clipboard operation
textual copied to clipboard

Is it possible to run Textual apps inside of a Jupyter Notebook?

Open SlappedWithSilence opened this issue 1 year ago • 3 comments

This seems like it should work, but I'm running into AsyncIO issues when I try.

RuntimeError                              Traceback (most recent call last)
Input In [12], in <cell line: 9>()
     11 logger.add("debug.log", level="ERROR")
     12 if load_config(CONFIG_PATH):
---> 13     UI.run(log="textual.log")

File ~\.conda\envs\basic\lib\site-packages\textual\app.py:206, in App.run(cls, console, screen, driver, **kwargs)
    203     app = cls(screen=screen, driver_class=driver, **kwargs)
    204     await app.process_messages()
--> 206 asyncio.run(run_app())

File ~\.conda\envs\basic\lib\asyncio\runners.py:33, in run(main, debug)
      9 """Execute the coroutine and return the result.
     10 
     11 This function runs the passed coroutine, taking care of
   (...)
     30     asyncio.run(main())
     31 """
     32 if events._get_running_loop() is not None:
---> 33     raise RuntimeError(
     34         "asyncio.run() cannot be called from a running event loop")
     36 if not coroutines.iscoroutine(main):
     37     raise ValueError("a coroutine was expected, got {!r}".format(main))

RuntimeError: asyncio.run() cannot be called from a running event loop

SlappedWithSilence avatar Jul 18 '22 15:07 SlappedWithSilence

I guess there's a workaround for this. In the newer versions of the Jupyter notebook, you can use top level await keyword. This is done by having a running event loop when you start the jupyter.

The conflict with Textualize here is because of [this line]:(https://github.com/Textualize/textual/blob/main/src/textual/app.py)

async def run_app() -> None:
  app = cls(screen=screen, driver_class=driver, **kwargs)
  await app.process_messages()

asyncio.run(run_app())

So either Texualize can reuse the active event loop here, or you can disable this in the Jupyter:

%autoawait False

@willmcgugan Do you have any preference on this? I would be happy to help with a patch or write documentation to clarify this.

Glyphack avatar Jul 19 '22 11:07 Glyphack

This is basically what I thought was happening. I am not a Python expert, but I am trying to improve.

I tried to patch the run method to allow it to re-use the existing event loop:

@classmethod
    async def run_notebook(
        cls,
        console: Console = None,
        screen: bool = True,
        driver: Type[Driver] = None,
        **kwargs,
    ):
        """Run the app.
        Args:
            console (Console, optional): Console object. Defaults to None.
            screen (bool, optional): Enable application mode. Defaults to True.
            driver (Type[Driver], optional): Driver class or None for default. Defaults to None.
        """

        async def run_app() -> None:
            app = cls(screen=screen, driver_class=driver, **kwargs)
            await app.process_messages()

        await run_app()

Now, this runs, but it results in deeper errors that I have no clue how to approach:

UnsupportedOperation                      Traceback (most recent call last)
Input In [16], in <cell line: 10>()
      8 logger.add("debug.log", level="ERROR")
     10 if load_config(CONFIG_PATH):
---> 11    await UI.run_notebook(log="textual.log")

Input In [14], in UI.run_notebook(cls, console, screen, driver, **kwargs)
    359     app = cls(screen=screen, driver_class=driver, **kwargs)
    360     await app.process_messages()
--> 362 await run_app()

Input In [14], in UI.run_notebook.<locals>.run_app()
    358 async def run_app() -> None:
    359     app = cls(screen=screen, driver_class=driver, **kwargs)
--> 360     await app.process_messages()

File ~\.conda\envs\basic\lib\site-packages\textual\app.py:298, in App.process_messages(self)
    295 # Wait for the load event to be processed, so we don't go in to application mode beforehand
    296 await load_event.wait()
--> 298 driver = self._driver = self.driver_class(self.console, self)
    299 try:
    300     driver.start_application_mode()

File ~\.conda\envs\basic\lib\site-packages\textual\drivers\windows_driver.py:23, in WindowsDriver.__init__(self, console, target)
     21 super().__init__(console, target)
     22 self.in_fileno = sys.stdin.fileno()
---> 23 self.out_fileno = sys.stdout.fileno()
     25 self.exit_event = Event()
     26 self._event_thread: Thread | None = None

File ~\.conda\envs\basic\lib\site-packages\ipykernel\iostream.py:310, in OutStream.fileno(self)
    308     return self._original_stdstream_copy
    309 else:
--> 310     raise io.UnsupportedOperation("fileno")

UnsupportedOperation: fileno

Now, to my uneducated eyes, this looks like Textual, since it is running on a Windows installation of Python, expects the Jupyter Notebook output to support some kind of functionality that it actually does not support.

As to where I go from here, I have no clue. Would I have to manually select a different driver?

SlappedWithSilence avatar Jul 19 '22 16:07 SlappedWithSilence

TBH I would be very surprised if you get this working, even after fixing this hurdle. Pretty sure Jupyter Notebook doesn't implement full terminal capabilities.

We do plan on a neat Jupyter integration, but you might have to wait for that.

willmcgugan avatar Jul 19 '22 17:07 willmcgugan

Did we solve your problem?

Glad we could help!

github-actions[bot] avatar Oct 25 '22 09:10 github-actions[bot]