From 73f065bbbd5d02d2536e6a20421bc8a685fb5103 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 28 Mar 2023 11:50:28 +0100 Subject: [PATCH] Logging handler (#2151) * logging handler * changelog * remove logging experiment * handler * fix * docs for logging handler --- CHANGELOG.md | 3 ++- docs/api/logging.md | 1 + docs/guide/devtools.md | 34 +++++++++++++++++++++++++++++++++- mkdocs-nav.yml | 1 + src/textual/__init__.py | 5 +++++ src/textual/_log.py | 1 + src/textual/logging.py | 32 ++++++++++++++++++++++++++++++++ src/textual/message_pump.py | 13 +++++++++++-- 8 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 docs/api/logging.md create mode 100644 src/textual/logging.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba83ff7c..5e72bbe99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `Tree`: `clear`, `reset` - Screens with alpha in their background color will now blend with the background. https://github.com/Textualize/textual/pull/2139 - Added "thick" border style. https://github.com/Textualize/textual/pull/2139 +- message_pump.app will now set the active app if it is not already set. ### Added @@ -36,7 +37,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added scroll_end switch to TextLog.write https://github.com/Textualize/textual/pull/2127 - Added Screen.ModalScreen which prevents App from handling bindings. https://github.com/Textualize/textual/pull/2139 - Added TEXTUAL_LOG env var which should be a path that Textual will write verbose logs to (textual devtools is generally preferred) https://github.com/Textualize/textual/pull/2148 - +- Added textual.logging.TextualHandler logging handler ## [0.16.0] - 2023-03-22 diff --git a/docs/api/logging.md b/docs/api/logging.md new file mode 100644 index 000000000..5fbf833e1 --- /dev/null +++ b/docs/api/logging.md @@ -0,0 +1 @@ +::: textual.logging diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index fccbce004..087ebf4ce 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -120,5 +120,37 @@ class LogApp(App): if __name__ == "__main__": LogApp().run() - +``` + +### Logging handler + +Textual has a [logging handler][textual.logging.TextualHandler] which will write anything logged via the builtin logging library to the devtools. +This may be useful if you have a third-party library that uses the logging module, and you want to see those logs with Textual logs. + +!!! note + + The logging library works with strings only, so you won't be able to log Rich renderables such as `self.tree` with the logging handler. + +Here's an example of configuring logging to use the `TextualHandler`. + +```python +import logging +from textual.app import App +from textual.logging import TextualHandler + +logging.basicConfig( + level="NOTSET", + handlers=[TextualHandler()], +) + + +class LogApp(App): + """Using logging with Textual.""" + + def on_mount(self) -> None: + logging.debug("Logged via TextualHandler") + + +if __name__ == "__main__": + LogApp().run() ``` diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index c508c3619..e785346eb 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -169,6 +169,7 @@ nav: - "api/list_item.md" - "api/list_view.md" - "api/loading_indicator.md" + - "api/logging.md" - "api/markdown_viewer.md" - "api/markdown.md" - "api/message_pump.md" diff --git a/src/textual/__init__.py b/src/textual/__init__.py index d2e59f0bf..935f4da97 100644 --- a/src/textual/__init__.py +++ b/src/textual/__init__.py @@ -142,6 +142,11 @@ class Logger: """Logs system information.""" return Logger(self._log, LogGroup.SYSTEM) + @property + def logging(self) -> Logger: + """Logs from stdlib logging module.""" + return Logger(self._log, LogGroup.LOGGING) + log = Logger(None) diff --git a/src/textual/_log.py b/src/textual/_log.py index e9adcef42..26125052d 100644 --- a/src/textual/_log.py +++ b/src/textual/_log.py @@ -12,6 +12,7 @@ class LogGroup(Enum): ERROR = 5 PRINT = 6 SYSTEM = 7 + LOGGING = 8 class LogVerbosity(Enum): diff --git a/src/textual/logging.py b/src/textual/logging.py new file mode 100644 index 000000000..1ceb1bd2a --- /dev/null +++ b/src/textual/logging.py @@ -0,0 +1,32 @@ +import sys +from logging import Handler, LogRecord + +from ._context import active_app + + +class TextualHandler(Handler): + """A Logging handler for Textual apps.""" + + def __init__(self, stderr: bool = True, stdout: bool = False) -> None: + """Initialize a Textual logging handler. + + Args: + stderr: Log to stderr when there is no active app. + stdout: Log to stdout when there is not active app. + """ + super().__init__() + self._stderr = stderr + self._stdout = stdout + + def emit(self, record: LogRecord) -> None: + """Invoked by logging.""" + message = self.format(record) + try: + app = active_app.get() + except LookupError: + if self._stderr: + print(message, file=sys.stderr) + elif self._stdout: + print(message, file=sys.stdout) + else: + app.log.logging(message) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 9163811d5..6599f7191 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -156,17 +156,26 @@ class MessagePump(metaclass=MessagePumpMeta): try: return active_app.get() except LookupError: - raise NoActiveAppError() + from .app import App + + node: MessagePump | None = self + while not isinstance(node, App): + if node is None: + raise NoActiveAppError() + node = node._parent + active_app.set(node) + return node @property def is_parent_active(self) -> bool: + """Is the parent active?""" return bool( self._parent and not self._parent._closed and not self._parent._closing ) @property def is_running(self) -> bool: - """bool: Is the message pump running (potentially processing messages).""" + """Is the message pump running (potentially processing messages).""" return self._running @property