Skip to content

Commit 772a3f0

Browse files
authored
Merge pull request #5863 from Textualize/log-in-threads
Log in threads
2 parents 3483d81 + e640279 commit 772a3f0

File tree

4 files changed

+89
-22
lines changed

4 files changed

+89
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1212
- Fixed issues with initial flicker in `TextArea` rendering https://github.com/Textualize/textual/issues/5841vcomm
1313
- Fixed issue with workers that have large parameter lists breaking dev tools https://github.com/Textualize/textual/pull/5850
1414
- Fixed post_message failing on 3.8 https://github.com/Textualize/textual/pull/5848
15+
- Fixed log not working from threads https://github.com/Textualize/textual/pull/5863
1516

1617
### Added
1718

src/textual/__init__.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import annotations
99

1010
import inspect
11+
import weakref
1112
from typing import TYPE_CHECKING, Callable
1213

1314
import rich.repr
@@ -35,6 +36,8 @@
3536
if TYPE_CHECKING:
3637
from importlib.metadata import version
3738

39+
from textual.app import App
40+
3841
__version__ = version("textual")
3942
"""The version of Textual."""
4043

@@ -62,10 +65,17 @@ def __init__(
6265
log_callable: LogCallable | None,
6366
group: LogGroup = LogGroup.INFO,
6467
verbosity: LogVerbosity = LogVerbosity.NORMAL,
68+
app: App | None = None,
6569
) -> None:
6670
self._log = log_callable
6771
self._group = group
6872
self._verbosity = verbosity
73+
self._app = None if app is None else weakref.ref(app)
74+
75+
@property
76+
def app(self) -> App | None:
77+
"""The associated application, or `None` if there isn't one."""
78+
return None if self._app is None else self._app()
6979

7080
def __rich_repr__(self) -> rich.repr.Result:
7181
yield self._group, LogGroup.INFO
@@ -82,17 +92,20 @@ def __call__(self, *args: object, **kwargs) -> None:
8292

8393
with open(constants.LOG_FILE, "a") as log_file:
8494
print(output, file=log_file)
85-
try:
86-
app = active_app.get()
87-
except LookupError:
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)
94-
return
95-
if app.devtools is None or not app.devtools.is_connected:
95+
96+
app = self.app
97+
if app is None:
98+
try:
99+
app = active_app.get()
100+
except LookupError:
101+
if constants.DEBUG:
102+
print_args = (
103+
*args,
104+
*[f"{key}={value!r}" for key, value in kwargs.items()],
105+
)
106+
print(*print_args)
107+
return
108+
if not app._is_devtools_connected:
96109
return
97110

98111
current_frame = inspect.currentframe()
@@ -129,52 +142,52 @@ def verbosity(self, verbose: bool) -> Logger:
129142
New logger.
130143
"""
131144
verbosity = LogVerbosity.HIGH if verbose else LogVerbosity.NORMAL
132-
return Logger(self._log, self._group, verbosity)
145+
return Logger(self._log, self._group, verbosity, app=self.app)
133146

134147
@property
135148
def verbose(self) -> Logger:
136149
"""A verbose logger."""
137-
return Logger(self._log, self._group, LogVerbosity.HIGH)
150+
return Logger(self._log, self._group, LogVerbosity.HIGH, app=self.app)
138151

139152
@property
140153
def event(self) -> Logger:
141154
"""Logs events."""
142-
return Logger(self._log, LogGroup.EVENT)
155+
return Logger(self._log, LogGroup.EVENT, app=self.app)
143156

144157
@property
145158
def debug(self) -> Logger:
146159
"""Logs debug messages."""
147-
return Logger(self._log, LogGroup.DEBUG)
160+
return Logger(self._log, LogGroup.DEBUG, app=self.app)
148161

149162
@property
150163
def info(self) -> Logger:
151164
"""Logs information."""
152-
return Logger(self._log, LogGroup.INFO)
165+
return Logger(self._log, LogGroup.INFO, app=self.app)
153166

154167
@property
155168
def warning(self) -> Logger:
156169
"""Logs warnings."""
157-
return Logger(self._log, LogGroup.WARNING)
170+
return Logger(self._log, LogGroup.WARNING, app=self.app)
158171

159172
@property
160173
def error(self) -> Logger:
161174
"""Logs errors."""
162-
return Logger(self._log, LogGroup.ERROR)
175+
return Logger(self._log, LogGroup.ERROR, app=self.app)
163176

164177
@property
165178
def system(self) -> Logger:
166179
"""Logs system information."""
167-
return Logger(self._log, LogGroup.SYSTEM)
180+
return Logger(self._log, LogGroup.SYSTEM, app=self.app)
168181

169182
@property
170183
def logging(self) -> Logger:
171184
"""Logs from stdlib logging module."""
172-
return Logger(self._log, LogGroup.LOGGING)
185+
return Logger(self._log, LogGroup.LOGGING, app=self.app)
173186

174187
@property
175188
def worker(self) -> Logger:
176189
"""Logs worker information."""
177-
return Logger(self._log, LogGroup.WORKER)
190+
return Logger(self._log, LogGroup.WORKER, app=self.app)
178191

179192

180193
log = Logger(None)
@@ -185,4 +198,10 @@ def worker(self) -> Logger:
185198
from textual import log
186199
log(locals())
187200
```
201+
202+
!!! note
203+
This logger will only work if there is an active app in the current thread.
204+
Use `app.log` to write logs from a thread without an active app.
205+
206+
188207
"""

src/textual/app.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,7 @@ def __init__(
682682
will be ignored.
683683
"""
684684

685-
self._logger = Logger(self._log)
685+
self._logger = Logger(self._log, app=self)
686686

687687
self._css_has_errors = False
688688

@@ -842,6 +842,11 @@ def __init__(
842842
)
843843
)
844844

845+
@property
846+
def _is_devtools_connected(self) -> bool:
847+
"""Is the app connected to the devtools?"""
848+
return self.devtools is not None and self.devtools.is_connected
849+
845850
@cached_property
846851
def _exception_event(self) -> asyncio.Event:
847852
"""An event that will be set when the first exception is encountered."""

tests/test_logger.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import inspect
2+
from typing import Any
3+
4+
from textual import work
5+
from textual._log import LogGroup, LogVerbosity
6+
from textual.app import App
7+
8+
9+
async def test_log_from_worker() -> None:
10+
"""Check that log calls from threaded workers call app._log"""
11+
12+
log_messages: list[tuple] = []
13+
14+
class LogApp(App):
15+
16+
def _log(
17+
self,
18+
group: LogGroup,
19+
verbosity: LogVerbosity,
20+
_textual_calling_frame: inspect.Traceback,
21+
*objects: Any,
22+
**kwargs,
23+
) -> None:
24+
log_messages.append(objects)
25+
26+
@property
27+
def _is_devtools_connected(self):
28+
"""Fake connected devtools."""
29+
return True
30+
31+
def on_mount(self) -> None:
32+
self.do_work()
33+
self.call_after_refresh(self.exit)
34+
35+
@work(thread=True)
36+
def do_work(self) -> None:
37+
self.log("HELLO from do_work")
38+
39+
app = LogApp()
40+
async with app.run_test() as pilot:
41+
await pilot.pause()
42+
assert ("HELLO from do_work",) in log_messages

0 commit comments

Comments
 (0)