Skip to content

don't call get_event_loop() if it's deprecated, handle RuntimeError from get_event_loop after asyncio.run #5799

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 23 additions & 5 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()"""
Expand Down Expand Up @@ -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,
Expand All @@ -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."""
Expand Down
17 changes: 17 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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