diff --git a/CHANGELOG.md b/CHANGELOG.md index 072f92175e..f3a334e35c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed `VERTICAL_BREAKPOINTS` doesn't work https://github.com/Textualize/textual/pull/5785 - Fixed `Button` allowing text selection https://github.com/Textualize/textual/pull/5770 +- Fixed running `App.run` after `asyncio.run` https://github.com/Textualize/textual/pull/5799 +- Fixed triggering a deprecation warning in py >= 3.10 https://github.com/Textualize/textual/pull/5799 ## [3.2.0] - 2025-05-02 diff --git a/src/textual/app.py b/src/textual/app.py index 9ae50e7421..332e9ff13f 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -150,6 +150,9 @@ if constants.DEBUG: warnings.simplefilter("always", ResourceWarning) +# `asyncio.get_event_loop()` is deprecated since Python 3.10: +_ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED = sys.version_info >= (3, 10, 0) + ComposeResult = Iterable[Widget] RenderResult: TypeAlias = "RenderableType | Visual | SupportsVisual" """Result of Widget.render()""" @@ -2140,9 +2143,9 @@ def run( App return value. """ - async def run_app() -> None: + async def run_app() -> ReturnType | None: """Run the app.""" - await self.run_async( + return await self.run_async( headless=headless, inline=inline, inline_no_clear=inline_no_clear, @@ -2151,9 +2154,24 @@ async def run_app() -> None: auto_pilot=auto_pilot, ) - event_loop = asyncio.get_event_loop() if loop is None else loop - event_loop.run_until_complete(run_app()) - return self.return_value + if loop is None: + if _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED: + # N.B. This does work with Python<3.10, but global Locks, Events, etc + # eagerly bind the event loop, and result in Future bound to wrong + # loop errors. + return asyncio.run(run_app()) + try: + global_loop = asyncio.get_event_loop() + except RuntimeError: + # the global event loop may have been destroyed by someone running + # asyncio.run(), or asyncio.set_event_loop(None), in which case + # we need to use asyncio.run() also. (We run this outside the + # context of an exception handler) + pass + else: + return global_loop.run_until_complete(run_app()) + return asyncio.run(run_app()) + return loop.run_until_complete(run_app()) async def _on_css_change(self) -> None: """Callback for the file monitor, called when CSS files change.""" diff --git a/tests/test_app.py b/tests/test_app.py index 782b99e671..3c6d54aed8 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -367,3 +367,20 @@ def on_mount(self) -> None: app = MyApp() result = await app.run_async() assert result == 42 + + +def test_app_loop_run_after_asyncio_run() -> None: + """Test that App.run runs after asyncio.run has run.""" + + class MyApp(App[int]): + def on_mount(self) -> None: + self.exit(42) + + async def amain(): + pass + + asyncio.run(amain()) + + app = MyApp() + result = app.run() + assert result == 42