SXXXXXXX_ControlPanel/controlpanel/gui/logger_integration.py
2026-01-19 12:56:30 +01:00

160 lines
5.5 KiB
Python

"""Logger integration wrapper for ControlPanel GUI.
Tries to load the external `python-tkinter-logger` module from
`externals/python-tkinter-logger`. If available, uses its
`TkinterLogger` implementation. Otherwise falls back to a minimal
thread-safe Tkinter text handler so the UI still receives logs.
API:
- init_logger(root, **opts) -> object
- add_widget(text_widget, level_colors=None, max_lines=1000) -> bool
- shutdown() -> None
- get_logger(name) -> logging.Logger
"""
from __future__ import annotations
import logging
import importlib
import os
import sys
from typing import Optional, Dict
import tkinter as tk
_logger_system = None
_simple_handler = None
def _try_import_external() -> Optional[object]:
"""Attempts to import the external TkinterLogger module.
Returns the imported module or None if import fails.
"""
try:
# externals path (repo_root/externals/python-tkinter-logger)
base = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
ext_path = os.path.join(base, "externals", "python-tkinter-logger")
if os.path.isdir(ext_path) and ext_path not in sys.path:
sys.path.insert(0, ext_path)
module = importlib.import_module("tkinter_logger")
return module
except Exception:
return None
def init_logger(root: Optional[tk.Tk] = None, **opts) -> Optional[object]:
"""Initializes the logger system.
If the external `TkinterLogger` is present it will be used; otherwise a
minimal fallback is configured that writes to a `tk.Text` widget when
`add_widget` is called.
"""
global _logger_system
if _logger_system is not None:
return _logger_system
ext = _try_import_external()
if ext is not None and hasattr(ext, "TkinterLogger"):
Tk = getattr(ext, "TkinterLogger")
try:
instance = Tk(root)
instance.setup(**{k: v for k, v in opts.items()})
_logger_system = instance
return _logger_system
except Exception:
# Fall through to fallback
pass
# Fallback: basic configuration (console + handlers will be added when widget added)
logging.basicConfig(level=opts.get("root_level", logging.INFO))
_logger_system = None
return None
def add_widget(text_widget: tk.Text, level_colors: Optional[Dict[int, str]] = None, max_lines: int = 1000) -> bool:
"""Adds a tkinter text widget as a log sink.
If the external logger was loaded, delegates to its `add_tkinter_handler`.
Otherwise attaches a simple handler to the root logger that writes into
`text_widget` using `after` to remain thread-safe.
"""
global _logger_system, _simple_handler
if _logger_system is not None:
try:
_logger_system.add_tkinter_handler(text_widget, level_colors=level_colors, max_lines=max_lines)
return True
except Exception:
pass
# Minimal fallback handler
class _SimpleTkHandler(logging.Handler):
def __init__(self, widget: tk.Text, max_lines: int = 1000):
super().__init__()
self.widget = widget
self.max_lines = max_lines
def emit(self, record: logging.LogRecord) -> None:
try:
msg = self.format(record)
def _append():
try:
if not self.widget.winfo_exists():
return
self.widget.config(state=tk.NORMAL)
self.widget.insert(tk.END, msg + "\n")
# Trim lines
total_lines = int(self.widget.index("end-1c").split(".")[0])
if total_lines > self.max_lines:
delete_to = total_lines - self.max_lines
self.widget.delete("1.0", f"{delete_to}.0")
self.widget.see(tk.END)
self.widget.config(state=tk.DISABLED)
except Exception:
pass
# Schedule append on the Tk event loop
try:
self.widget.after(0, _append)
except Exception:
# If scheduling fails, try direct append (best effort)
_append()
except Exception:
self.handleError(record)
# Attach handler
try:
handler = _SimpleTkHandler(text_widget, max_lines=max_lines)
formatter = logging.Formatter(fmt=opts.get("log_format", "%(asctime)s [%(levelname)s] %(name)s: %(message)s"), datefmt=opts.get("date_format", "%Y-%m-%d %H:%M:%S"))
handler.setFormatter(formatter)
root_logger = logging.getLogger()
root_logger.addHandler(handler)
_simple_handler = handler
return True
except Exception:
return False
def shutdown() -> None:
"""Shuts down the logger system (if external) and removes fallback handler."""
global _logger_system, _simple_handler
try:
if _logger_system is not None:
try:
_logger_system.shutdown()
except Exception:
pass
_logger_system = None
if _simple_handler is not None:
try:
logging.getLogger().removeHandler(_simple_handler)
except Exception:
pass
_simple_handler = None
finally:
return
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)