Skip to content

Commit ed00287

Browse files
authored
Merge pull request #5782 from Textualize/log-message
Fix log messages in stdout, add loop argument to run_async
2 parents c3b5d4f + 4e4e2a3 commit ed00287

File tree

4 files changed

+71
-50
lines changed

4 files changed

+71
-50
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1010
### Fixed
1111

1212
- Fixed `OptionList` causing excessive redrawing https://github.com/Textualize/textual/pull/5766
13+
- Log messages could be written to stdout when there was no app, which could happen when using run_async or threads. Now they will be suppressed, unless the env var `TEXTUAL_DEBUG` is set https://github.com/Textualize/textual/pull/5782
1314

1415
### Added
1516

src/textual/__init__.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,12 @@ def __call__(self, *args: object, **kwargs) -> None:
8585
try:
8686
app = active_app.get()
8787
except LookupError:
88-
print_args = (*args, *[f"{key}={value!r}" for key, value in kwargs.items()])
89-
print(*print_args)
88+
if constants.DEBUG:
89+
print_args = (
90+
*args,
91+
*[f"{key}={value!r}" for key, value in kwargs.items()],
92+
)
93+
print(*print_args)
9094
return
9195
if app.devtools is None or not app.devtools.is_connected:
9296
return
@@ -108,8 +112,12 @@ def __call__(self, *args: object, **kwargs) -> None:
108112
)
109113
except LoggerError:
110114
# If there is not active app, try printing
111-
print_args = (*args, *[f"{key}={value!r}" for key, value in kwargs.items()])
112-
print(*print_args)
115+
if constants.DEBUG:
116+
print_args = (
117+
*args,
118+
*[f"{key}={value!r}" for key, value in kwargs.items()],
119+
)
120+
print(*print_args)
113121

114122
def verbosity(self, verbose: bool) -> Logger:
115123
"""Get a new logger with selective verbosity.

src/textual/app.py

Lines changed: 33 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import threading
1919
import uuid
2020
import warnings
21-
from asyncio import Task, create_task
21+
from asyncio import AbstractEventLoop, Task, create_task
2222
from concurrent.futures import Future
2323
from contextlib import (
2424
asynccontextmanager,
@@ -150,9 +150,6 @@
150150
if constants.DEBUG:
151151
warnings.simplefilter("always", ResourceWarning)
152152

153-
# `asyncio.get_event_loop()` is deprecated since Python 3.10:
154-
_ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED = sys.version_info >= (3, 10, 0)
155-
156153
ComposeResult = Iterable[Widget]
157154
RenderResult: TypeAlias = "RenderableType | Visual | SupportsVisual"
158155
"""Result of Widget.render()"""
@@ -2059,7 +2056,6 @@ async def run_async(
20592056
from textual.pilot import Pilot
20602057

20612058
app = self
2062-
20632059
auto_pilot_task: Task | None = None
20642060

20652061
if auto_pilot is None and constants.PRESS:
@@ -2092,27 +2088,29 @@ async def run_auto_pilot(
20922088
run_auto_pilot(auto_pilot, pilot), name=repr(pilot)
20932089
)
20942090

2095-
try:
2096-
app._loop = asyncio.get_running_loop()
2097-
app._thread_id = threading.get_ident()
2098-
2099-
await app._process_messages(
2100-
ready_callback=None if auto_pilot is None else app_ready,
2101-
headless=headless,
2102-
inline=inline,
2103-
inline_no_clear=inline_no_clear,
2104-
mouse=mouse,
2105-
terminal_size=size,
2106-
)
2107-
finally:
2091+
app._loop = asyncio.get_running_loop()
2092+
app._thread_id = threading.get_ident()
2093+
with app._context():
21082094
try:
2109-
if auto_pilot_task is not None:
2110-
await auto_pilot_task
2095+
await app._process_messages(
2096+
ready_callback=None if auto_pilot is None else app_ready,
2097+
headless=headless,
2098+
inline=inline,
2099+
inline_no_clear=inline_no_clear,
2100+
mouse=mouse,
2101+
terminal_size=size,
2102+
)
21112103
finally:
21122104
try:
2113-
await asyncio.shield(app._shutdown())
2114-
except asyncio.CancelledError:
2115-
pass
2105+
if auto_pilot_task is not None:
2106+
await auto_pilot_task
2107+
finally:
2108+
try:
2109+
await asyncio.shield(app._shutdown())
2110+
except asyncio.CancelledError:
2111+
pass
2112+
app._loop = None
2113+
app._thread_id = 0
21162114

21172115
return app.return_value
21182116

@@ -2125,6 +2123,7 @@ def run(
21252123
mouse: bool = True,
21262124
size: tuple[int, int] | None = None,
21272125
auto_pilot: AutopilotCallbackType | None = None,
2126+
loop: AbstractEventLoop | None = None,
21282127
) -> ReturnType | None:
21292128
"""Run the app.
21302129
@@ -2136,36 +2135,24 @@ def run(
21362135
size: Force terminal size to `(WIDTH, HEIGHT)`,
21372136
or None to auto-detect.
21382137
auto_pilot: An auto pilot coroutine.
2139-
2138+
loop: Asyncio loop instance, or `None` to use default.
21402139
Returns:
21412140
App return value.
21422141
"""
21432142

21442143
async def run_app() -> None:
21452144
"""Run the app."""
2146-
self._loop = asyncio.get_running_loop()
2147-
self._thread_id = threading.get_ident()
2148-
with self._context():
2149-
try:
2150-
await self.run_async(
2151-
headless=headless,
2152-
inline=inline,
2153-
inline_no_clear=inline_no_clear,
2154-
mouse=mouse,
2155-
size=size,
2156-
auto_pilot=auto_pilot,
2157-
)
2158-
finally:
2159-
self._loop = None
2160-
self._thread_id = 0
2145+
await self.run_async(
2146+
headless=headless,
2147+
inline=inline,
2148+
inline_no_clear=inline_no_clear,
2149+
mouse=mouse,
2150+
size=size,
2151+
auto_pilot=auto_pilot,
2152+
)
21612153

2162-
if _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED:
2163-
# N.B. This doesn't work with Python<3.10, as we end up with 2 event loops:
2164-
asyncio.run(run_app())
2165-
else:
2166-
# However, this works with Python<3.10:
2167-
event_loop = asyncio.get_event_loop()
2168-
event_loop.run_until_complete(run_app())
2154+
event_loop = asyncio.get_event_loop() if loop is None else loop
2155+
event_loop.run_until_complete(run_app())
21692156
return self.return_value
21702157

21712158
async def _on_css_change(self) -> None:

tests/test_app.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import contextlib
23

34
import pytest
@@ -340,3 +341,27 @@ def on_click(self, event: events.Click) -> None:
340341
# Each click is outwith the time threshold, so a click chain is never created.
341342
await raw_click(pilot, "#one")
342343
assert click_count == i
344+
345+
346+
def test_app_loop() -> None:
347+
"""Test that App.run accepts a loop argument."""
348+
349+
class MyApp(App[int]):
350+
def on_mount(self) -> None:
351+
self.exit(42)
352+
353+
app = MyApp()
354+
result = app.run(loop=asyncio.new_event_loop())
355+
assert result == 42
356+
357+
358+
async def test_app_run_async() -> None:
359+
"""Check run_async runs without issues."""
360+
361+
class MyApp(App[int]):
362+
def on_mount(self) -> None:
363+
self.exit(42)
364+
365+
app = MyApp()
366+
result = await app.run_async()
367+
assert result == 42

0 commit comments

Comments
 (0)