Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed issues with initial flicker in `TextArea` rendering https://github.com/Textualize/textual/issues/5841vcomm
- Fixed issue with workers that have large parameter lists breaking dev tools https://github.com/Textualize/textual/pull/5850
- Fixed post_message failing on 3.8 https://github.com/Textualize/textual/pull/5848
- Fixed log not working from threads https://github.com/Textualize/textual/pull/5863

### Added

Expand Down
61 changes: 40 additions & 21 deletions src/textual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from __future__ import annotations

import inspect
import weakref
from typing import TYPE_CHECKING, Callable

import rich.repr
Expand Down Expand Up @@ -35,6 +36,8 @@
if TYPE_CHECKING:
from importlib.metadata import version

from textual.app import App

__version__ = version("textual")
"""The version of Textual."""

Expand Down Expand Up @@ -62,10 +65,17 @@ def __init__(
log_callable: LogCallable | None,
group: LogGroup = LogGroup.INFO,
verbosity: LogVerbosity = LogVerbosity.NORMAL,
app: App | None = None,
) -> None:
self._log = log_callable
self._group = group
self._verbosity = verbosity
self._app = None if app is None else weakref.ref(app)

@property
def app(self) -> App | None:
"""The associated application, or `None` if there isn't one."""
return None if self._app is None else self._app()

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

with open(constants.LOG_FILE, "a") as log_file:
print(output, file=log_file)
try:
app = active_app.get()
except LookupError:
if constants.DEBUG:
print_args = (
*args,
*[f"{key}={value!r}" for key, value in kwargs.items()],
)
print(*print_args)
return
if app.devtools is None or not app.devtools.is_connected:

app = self.app
if app is None:
try:
app = active_app.get()
except LookupError:
if constants.DEBUG:
print_args = (
*args,
*[f"{key}={value!r}" for key, value in kwargs.items()],
)
print(*print_args)
return
if not app._is_devtools_connected:
return

current_frame = inspect.currentframe()
Expand Down Expand Up @@ -129,52 +142,52 @@ def verbosity(self, verbose: bool) -> Logger:
New logger.
"""
verbosity = LogVerbosity.HIGH if verbose else LogVerbosity.NORMAL
return Logger(self._log, self._group, verbosity)
return Logger(self._log, self._group, verbosity, app=self.app)

@property
def verbose(self) -> Logger:
"""A verbose logger."""
return Logger(self._log, self._group, LogVerbosity.HIGH)
return Logger(self._log, self._group, LogVerbosity.HIGH, app=self.app)

@property
def event(self) -> Logger:
"""Logs events."""
return Logger(self._log, LogGroup.EVENT)
return Logger(self._log, LogGroup.EVENT, app=self.app)

@property
def debug(self) -> Logger:
"""Logs debug messages."""
return Logger(self._log, LogGroup.DEBUG)
return Logger(self._log, LogGroup.DEBUG, app=self.app)

@property
def info(self) -> Logger:
"""Logs information."""
return Logger(self._log, LogGroup.INFO)
return Logger(self._log, LogGroup.INFO, app=self.app)

@property
def warning(self) -> Logger:
"""Logs warnings."""
return Logger(self._log, LogGroup.WARNING)
return Logger(self._log, LogGroup.WARNING, app=self.app)

@property
def error(self) -> Logger:
"""Logs errors."""
return Logger(self._log, LogGroup.ERROR)
return Logger(self._log, LogGroup.ERROR, app=self.app)

@property
def system(self) -> Logger:
"""Logs system information."""
return Logger(self._log, LogGroup.SYSTEM)
return Logger(self._log, LogGroup.SYSTEM, app=self.app)

@property
def logging(self) -> Logger:
"""Logs from stdlib logging module."""
return Logger(self._log, LogGroup.LOGGING)
return Logger(self._log, LogGroup.LOGGING, app=self.app)

@property
def worker(self) -> Logger:
"""Logs worker information."""
return Logger(self._log, LogGroup.WORKER)
return Logger(self._log, LogGroup.WORKER, app=self.app)


log = Logger(None)
Expand All @@ -185,4 +198,10 @@ def worker(self) -> Logger:
from textual import log
log(locals())
```

!!! note
This logger will only work if there is an active app in the current thread.
Use `app.log` to write logs from a thread without an active app.


"""
7 changes: 6 additions & 1 deletion src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ def __init__(
will be ignored.
"""

self._logger = Logger(self._log)
self._logger = Logger(self._log, app=self)

self._css_has_errors = False

Expand Down Expand Up @@ -842,6 +842,11 @@ def __init__(
)
)

@property
def _is_devtools_connected(self) -> bool:
"""Is the app connected to the devtools?"""
return self.devtools is not None and self.devtools.is_connected

@cached_property
def _exception_event(self) -> asyncio.Event:
"""An event that will be set when the first exception is encountered."""
Expand Down
42 changes: 42 additions & 0 deletions tests/test_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import inspect
from typing import Any

from textual import work
from textual._log import LogGroup, LogVerbosity
from textual.app import App


async def test_log_from_worker() -> None:
"""Check that log calls from threaded workers call app._log"""

log_messages: list[tuple] = []

class LogApp(App):

def _log(
self,
group: LogGroup,
verbosity: LogVerbosity,
_textual_calling_frame: inspect.Traceback,
*objects: Any,
**kwargs,
) -> None:
log_messages.append(objects)

@property
def _is_devtools_connected(self):
"""Fake connected devtools."""
return True

def on_mount(self) -> None:
self.do_work()
self.call_after_refresh(self.exit)

@work(thread=True)
def do_work(self) -> None:
self.log("HELLO from do_work")

app = LogApp()
async with app.run_test() as pilot:
await pilot.pause()
assert ("HELLO from do_work",) in log_messages
Loading