From a9e27a22208fd33ca6cbc82b8d089fe9baa9a5a3 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Thu, 29 Jan 2026 14:03:28 +0000 Subject: [PATCH 1/2] bugfix: logger works with each module and handles stdout/stderr --- BlocksScreen/BlocksScreen.py | 21 +- BlocksScreen/lib/moonrakerComm.py | 2 +- BlocksScreen/lib/network.py | 2 +- BlocksScreen/lib/panels/mainWindow.py | 28 +- BlocksScreen/lib/panels/networkWindow.py | 2 +- BlocksScreen/lib/panels/printTab.py | 2 +- BlocksScreen/lib/panels/widgets/filesPage.py | 5 +- .../lib/panels/widgets/jobStatusPage.py | 2 +- BlocksScreen/lib/printer.py | 2 +- BlocksScreen/logger.py | 517 ++++++++++++++++-- 10 files changed, 495 insertions(+), 88 deletions(-) diff --git a/BlocksScreen/BlocksScreen.py b/BlocksScreen/BlocksScreen.py index a7a2098e..3ccceb74 100644 --- a/BlocksScreen/BlocksScreen.py +++ b/BlocksScreen/BlocksScreen.py @@ -2,11 +2,10 @@ import sys import typing -import logger from lib.panels.mainWindow import MainWindow +from logger import setup_logging from PyQt6 import QtCore, QtGui, QtWidgets -_logger = logging.getLogger(name="logs/BlocksScreen.log") QtGui.QGuiApplication.setAttribute( QtCore.Qt.ApplicationAttribute.AA_SynthesizeMouseForUnhandledTouchEvents, True, @@ -22,13 +21,6 @@ RESET = "\033[0m" -def setup_app_loggers(): - """Setup logger""" - _ = logger.create_logger(name="logs/BlocksScreen.log", level=logging.DEBUG) - _logger = logging.getLogger(name="logs/BlocksScreen.log") - _logger.info("============ BlocksScreen Initializing ============") - - def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): """Show splash screen on app initialization""" logo = QtGui.QPixmap("BlocksScreen/BlocksScreen/lib/ui/resources/logoblocks.png") @@ -39,7 +31,16 @@ def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): if __name__ == "__main__": - setup_app_loggers() + setup_logging( + filename="logs/BlocksScreen.log", + level=logging.DEBUG, # File gets DEBUG+ + console_output=True, # Print to terminal + console_level=logging.DEBUG, # Console gets DEBUG+ + capture_stderr=True, # Capture X11 errors + capture_stdout=False, # Don't capture print() + ) + _logger = logging.getLogger(__name__) + _logger.info("============ BlocksScreen Initializing ============") BlocksScreen = QtWidgets.QApplication([]) BlocksScreen.setApplicationName("BlocksScreen") BlocksScreen.setApplicationDisplayName("BlocksScreen") diff --git a/BlocksScreen/lib/moonrakerComm.py b/BlocksScreen/lib/moonrakerComm.py index ba298ba7..bf3a74cc 100644 --- a/BlocksScreen/lib/moonrakerComm.py +++ b/BlocksScreen/lib/moonrakerComm.py @@ -14,7 +14,7 @@ from lib.utils.RepeatedTimer import RepeatedTimer from PyQt6 import QtCore, QtWidgets -_logger = logging.getLogger(name="logs/BlocksScreen.log") +_logger = logging.getLogger(__name__) class OneShotTokenError(Exception): diff --git a/BlocksScreen/lib/network.py b/BlocksScreen/lib/network.py index 51b87074..9b61a462 100644 --- a/BlocksScreen/lib/network.py +++ b/BlocksScreen/lib/network.py @@ -9,7 +9,7 @@ from PyQt6 import QtCore from sdbus_async import networkmanager as dbusNm -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class NetworkManagerRescanError(Exception): diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 32355803..ec1f27f1 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -9,16 +9,15 @@ from lib.moonrakerComm import MoonWebSocket from lib.panels.controlTab import ControlTab from lib.panels.filamentTab import FilamentTab -from lib.panels.networkWindow import NetworkControlWindow from lib.panels.printTab import PrintTab from lib.panels.utilitiesTab import UtilitiesTab +from lib.panels.widgets.basePopup import BasePopup from lib.panels.widgets.connectionPage import ConnectionPage +from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.popupDialogWidget import Popup +from lib.panels.widgets.updatePage import UpdatePage from lib.printer import Printer from lib.ui.mainWindow_ui import Ui_MainWindow # With header -from lib.panels.widgets.updatePage import UpdatePage -from lib.panels.widgets.basePopup import BasePopup -from lib.panels.widgets.loadWidget import LoadingOverlayWidget # from lib.ui.mainWindow_v2_ui import Ui_MainWindow # No header from lib.ui.resources.background_resources_rc import * @@ -28,10 +27,11 @@ from lib.ui.resources.main_menu_resources_rc import * from lib.ui.resources.system_resources_rc import * from lib.ui.resources.top_bar_resources_rc import * +from logger import LogManager from PyQt6 import QtCore, QtGui, QtWidgets from screensaver import ScreenSaver -_logger = logging.getLogger(name="logs/BlocksScreen.log") +_logger = logging.getLogger(__name__) def api_handler(func): @@ -93,7 +93,7 @@ def __init__(self): self.filamentPanel = FilamentTab(self.ui.filamentTab, self.printer, self.ws) self.controlPanel = ControlTab(self.ui.controlTab, self.ws, self.printer) self.utilitiesPanel = UtilitiesTab(self.ui.utilitiesTab, self.ws, self.printer) - self.networkPanel = NetworkControlWindow(self) + # self.networkPanel = NetworkControlWindow(self) self.bo_ws_startup.connect(slot=self.bo_start_websocket_connection) self.ws.connecting_signal.connect(self.conn_window.on_websocket_connecting) self.ws.connected_signal.connect( @@ -153,7 +153,7 @@ def __init__(self): self.printer.extruder_update.connect(self.on_extruder_update) self.printer.heater_bed_update.connect(self.on_heater_bed_update) self.ui.main_content_widget.currentChanged.connect(slot=self.reset_tab_indexes) - self.call_network_panel.connect(self.networkPanel.show_network_panel) + # self.call_network_panel.connect(self.networkPanel.show_network_panel) self.conn_window.wifi_button_clicked.connect(self.call_network_panel.emit) self.ui.wifi_button.clicked.connect(self.call_network_panel.emit) self.handle_error_response.connect( @@ -352,7 +352,7 @@ def reset_tab_indexes(self): self.filamentPanel.setCurrentIndex(0) self.controlPanel.setCurrentIndex(0) self.utilitiesPanel.setCurrentIndex(0) - self.networkPanel.setCurrentIndex(0) + # self.networkPanel.setCurrentIndex(0) def current_panel_index(self) -> int: """Helper function to get the index of the current page in the current tab @@ -687,14 +687,10 @@ def set_header_nozzle_diameter(self, diam: str): def closeEvent(self, a0: typing.Optional[QtGui.QCloseEvent]) -> None: """Handles GUI closing""" - _loggers = [ - logging.getLogger(name) for name in logging.root.manager.loggerDict - ] # Get available logger handlers - for logger in _loggers: # noqa: F402 - if hasattr(logger, "cancel"): - _callback = getattr(logger, "cancel") - if callable(_callback): - _callback() + + # Shutdown logger (closes files, stops threads, restores streams) + LogManager.shutdown() + self.ws.wb_disconnect() self.close() if a0 is None: diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index f9636a82..508bc7fe 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -10,7 +10,7 @@ from lib.utils.list_button import ListCustomButton from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class BuildNetworkList(QtCore.QThread): diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index 7431938e..74799823 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -20,7 +20,7 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger(name="logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class PrintTab(QtWidgets.QStackedWidget): diff --git a/BlocksScreen/lib/panels/widgets/filesPage.py b/BlocksScreen/lib/panels/widgets/filesPage.py index f8fa490f..77959e83 100644 --- a/BlocksScreen/lib/panels/widgets/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/filesPage.py @@ -5,11 +5,10 @@ import helper_methods from lib.utils.blocks_Scrollbar import CustomScrollBar from lib.utils.icon_button import IconButton -from PyQt6 import QtCore, QtGui, QtWidgets - from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem +from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class FilesPage(QtWidgets.QWidget): diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index bd5bbb8c..796af2e6 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -10,7 +10,7 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class JobStatusWidget(QtWidgets.QWidget): diff --git a/BlocksScreen/lib/printer.py b/BlocksScreen/lib/printer.py index 5889c19d..fd33f172 100644 --- a/BlocksScreen/lib/printer.py +++ b/BlocksScreen/lib/printer.py @@ -7,7 +7,7 @@ from lib.moonrakerComm import MoonWebSocket from PyQt6 import QtCore, QtWidgets -logger = logging.getLogger(name="logs/BlocksScreen.logs") +logger = logging.getLogger(__name__) class Printer(QtCore.QObject): diff --git a/BlocksScreen/logger.py b/BlocksScreen/logger.py index f63631e5..a5e187c3 100644 --- a/BlocksScreen/logger.py +++ b/BlocksScreen/logger.py @@ -1,95 +1,506 @@ +from __future__ import annotations + +import atexit import copy import logging -import logging.config import logging.handlers import pathlib import queue +import sys import threading +from typing import ClassVar, TextIO + +DEFAULT_FORMAT = ( + "[%(levelname)s] | %(asctime)s | %(name)s | " + "%(relativeCreated)6d | %(threadName)s : %(message)s" +) + + +class StreamToLogger(TextIO): + """ + Redirects a stream (stdout/stderr) to a logger. + + Useful for capturing output from subprocesses, X11, or print statements. + """ + + def __init__( + self, + logger: logging.Logger, + level: int = logging.INFO, + original_stream: TextIO | None = None, + ) -> None: + self._logger = logger + self._level = level + self._original = original_stream + self._buffer = "" + + def write(self, message: str) -> int: + """Write message to logger.""" + if message: + if self._original: + try: + self._original.write(message) + self._original.flush() + except Exception: + pass + + self._buffer += message + + while "\n" in self._buffer: + line, self._buffer = self._buffer.split("\n", 1) + if line.strip(): + self._logger.log(self._level, line.rstrip()) + + return len(message) + + def flush(self) -> None: + """Flush remaining buffer.""" + if self._buffer.strip(): + self._logger.log(self._level, self._buffer.rstrip()) + self._buffer = "" + + if self._original: + try: + self._original.flush() + except Exception: + pass + + def fileno(self) -> int: + """Return file descriptor for compatibility.""" + if self._original: + return self._original.fileno() + raise OSError("No file descriptor available") + + def isatty(self) -> bool: + """Check if stream is a TTY.""" + if self._original: + return self._original.isatty() + return False + + # Required for TextIO interface + def read(self, n: int = -1) -> str: + return "" + + def readline(self, limit: int = -1) -> str: + return "" + + def readlines(self, hint: int = -1) -> list[str]: + return [] + + def seek(self, offset: int, whence: int = 0) -> int: + return 0 + + def tell(self) -> int: + return 0 + + def truncate(self, size: int | None = None) -> int: + return 0 + + def writable(self) -> bool: + return True + + def readable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def close(self) -> None: + self.flush() + + @property + def closed(self) -> bool: + return False + + def __enter__(self) -> "StreamToLogger": + return self + + def __exit__(self, *args) -> None: + self.close() class QueueHandler(logging.Handler): - """Handler that sends events to a queue""" + """ + Logging handler that sends records to a queue. + + Records are formatted before being placed on the queue, + then consumed by a QueueListener in a background thread. + """ def __init__( self, - queue: queue.Queue, - format: str = "'[%(levelname)s] | %(asctime)s | %(name)s | %(relativeCreated)6d | %(threadName)s : %(message)s", - level=logging.DEBUG, - ): - super(QueueHandler, self).__init__() - self.log_queue = queue - self.setFormatter(logging.Formatter(format, validate=True)) - self.setLevel(level) - - def emit(self, record): - """Emit logging record""" + log_queue: queue.Queue, + fmt: str = DEFAULT_FORMAT, + level: int = logging.DEBUG, + ) -> None: + super().__init__(level) + self._queue = log_queue + self.setFormatter(logging.Formatter(fmt)) + + def emit(self, record: logging.LogRecord) -> None: + """Format and queue the log record.""" try: + # Format the message msg = self.format(record) + + # Copy record and update message record = copy.copy(record) - record.message = msg - record.name = record.name record.msg = msg - self.log_queue.put_nowait(record) + record.args = None # Already formatted + record.message = msg + + self._queue.put_nowait(record) except Exception: self.handleError(record) - def setFormatter(self, fmt: logging.Formatter | None) -> None: - """Set logging formatter""" - return super().setFormatter(fmt) +class AsyncFileHandler(logging.handlers.TimedRotatingFileHandler): + """ + Async file handler using a background thread. + + Wraps TimedRotatingFileHandler with a queue and worker thread + for non-blocking log writes. Automatically recreates log file + if deleted during runtime. + """ -class QueueListener(logging.handlers.TimedRotatingFileHandler): - """Threaded listener watching for log records on the queue handler queue, passes them for processing""" + def __init__( + self, + filename: str, + when: str = "midnight", + backup_count: int = 10, + encoding: str = "utf-8", + ) -> None: + self._log_path = pathlib.Path(filename) + + # Create log directory if needed + if self._log_path.parent != pathlib.Path("."): + self._log_path.parent.mkdir(parents=True, exist_ok=True) - def __init__(self, filename, encoding="utf-8"): - log_path = pathlib.Path(filename) - if log_path.parent != pathlib.Path("."): - log_path.parent.mkdir(parents=True, exist_ok=True) - super(QueueListener, self).__init__( + super().__init__( filename=filename, - when="MIDNIGHT", - backupCount=10, + when=when, + backupCount=backup_count, encoding=encoding, delay=True, ) - self.queue = queue.Queue() + + self._queue: queue.Queue[logging.LogRecord | None] = queue.Queue() + self._stop_event = threading.Event() + self._lock = threading.Lock() self._thread = threading.Thread( - name=f"log.{filename}", target=self._run, daemon=True + name=f"logger-{self._log_path.stem}", + target=self._worker, + daemon=True, ) self._thread.start() - def _run(self): - while True: + def _ensure_file_exists(self) -> None: + """Ensure log file and directory exist, recreate if deleted.""" + try: + # Check if directory exists + if not self._log_path.parent.exists(): + self._log_path.parent.mkdir(parents=True, exist_ok=True) + + # Check if file was deleted (stream is open but file gone) + if self.stream is not None and not self._log_path.exists(): + # Close old stream + try: + self.stream.close() + except Exception: + pass + self.stream = None + + # Reopen stream if needed + if self.stream is None: + self.stream = self._open() + + except Exception: + pass + + def emit(self, record: logging.LogRecord) -> None: + """Emit a record with file existence check.""" + with self._lock: + self._ensure_file_exists() + super().emit(record) + + def _worker(self) -> None: + """Background worker that processes queued log records.""" + while not self._stop_event.is_set(): try: - record = self.queue.get(True) + record = self._queue.get(timeout=0.5) if record is None: break self.handle(record) except queue.Empty: - break + continue + except Exception: + # Don't crash the worker thread + pass + + @property + def queue(self) -> queue.Queue: + """Get the log queue for QueueHandler.""" + return self._queue - def close(self): - """Close logger listener""" - if self._thread is None: + def close(self) -> None: + """Stop worker thread and close file handler.""" + if self._thread is None or not self._thread.is_alive(): + super().close() return - self.queue.put_nowait(None) - self._thread.join() + + # Signal worker to stop + self._stop_event.set() + self._queue.put_nowait(None) + + # Wait for worker to finish + self._thread.join(timeout=2.0) self._thread = None + # Close the file handler + super().close() + + +class _ExcludeStreamLoggers(logging.Filter): + """Filter to exclude stdout/stderr loggers from console output.""" + + def filter(self, record: logging.LogRecord) -> bool: + # Exclude to avoid double printing (already goes to console via StreamToLogger) + return record.name not in ("stdout", "stderr") + + +class LogManager: + """ + Manages application logging. + + Creates async file loggers with queue-based handlers. + Ensures proper cleanup on application exit. + """ + + _handlers: ClassVar[dict[str, AsyncFileHandler]] = {} + _initialized: ClassVar[bool] = False + _original_stdout: ClassVar[TextIO | None] = None + _original_stderr: ClassVar[TextIO | None] = None + + @classmethod + def _ensure_initialized(cls) -> None: + """Register cleanup handler on first use.""" + if not cls._initialized: + atexit.register(cls.shutdown) + cls._initialized = True + + @classmethod + def setup( + cls, + filename: str = "logs/app.log", + level: int = logging.DEBUG, + fmt: str = DEFAULT_FORMAT, + capture_stdout: bool = False, + capture_stderr: bool = True, + console_output: bool = True, + console_level: int | None = None, + ) -> None: + """ + Setup root logger for entire application. + + Call once at startup. After this, all modules can use: + logger = logging.getLogger(__name__) + + Args: + filename: Log file path + level: Logging level for all loggers + fmt: Log format string + capture_stdout: Redirect stdout to logger + capture_stderr: Redirect stderr to logger + console_output: Also print logs to console + console_level: Console log level (defaults to same as level) + """ + cls._ensure_initialized() + + # Store original streams before any redirection + if cls._original_stdout is None: + cls._original_stdout = sys.stdout + if cls._original_stderr is None: + cls._original_stderr = sys.stderr + + # Get root logger + root = logging.getLogger() + + # Don't add duplicate handlers + if root.handlers: + return + + root.setLevel(level) + + # Create async file handler + file_handler = AsyncFileHandler(filename) + cls._handlers["root"] = file_handler + + # Create queue handler that feeds the file handler + queue_handler = QueueHandler(file_handler.queue, fmt, level) + root.addHandler(queue_handler) + + # Add console handler + if console_output: + cls._add_console_handler(root, console_level or level, fmt) + + # Capture stdout/stderr (after console handler is set up) + if capture_stdout: + cls.redirect_stdout() + if capture_stderr: + cls.redirect_stderr() + + @classmethod + def _add_console_handler(cls, logger: logging.Logger, level: int, fmt: str) -> None: + """Add a console handler that prints to original stdout.""" + # Use original stdout to avoid recursion if stdout is redirected + stream = cls._original_stdout or sys.stdout + + console_handler = logging.StreamHandler(stream) + console_handler.setLevel(level) + console_handler.setFormatter(logging.Formatter(fmt)) + + # Filter out stderr logger to avoid double printing + console_handler.addFilter(_ExcludeStreamLoggers()) + + logger.addHandler(console_handler) + + @classmethod + def get_logger( + cls, + name: str, + filename: str | None = None, + level: int = logging.INFO, + fmt: str = DEFAULT_FORMAT, + ) -> logging.Logger: + """ + Get or create a named logger with its own file output. + + Args: + name: Logger name + filename: Log file path (defaults to "logs/{name}.log") + level: Logging level + fmt: Log format string + + Returns: + Configured Logger instance + """ + cls._ensure_initialized() + + logger = logging.getLogger(name) + + # Don't add duplicate handlers + if logger.handlers: + return logger + + logger.setLevel(level) + + # Create async file handler + if filename is None: + filename = f"logs/{name}.log" + + file_handler = AsyncFileHandler(filename) + cls._handlers[name] = file_handler + + # Create queue handler that feeds the file handler + queue_handler = QueueHandler(file_handler.queue, fmt, level) + logger.addHandler(queue_handler) + + # Don't propagate to root (has its own file) + logger.propagate = False + + return logger + + @classmethod + def redirect_stdout(cls, logger_name: str = "stdout") -> None: + """ + Redirect stdout to logger. + + Captures print() statements and subprocess output. + """ + logger = logging.getLogger(logger_name) + sys.stdout = StreamToLogger(logger, logging.INFO, cls._original_stdout) + + @classmethod + def redirect_stderr(cls, logger_name: str = "stderr") -> None: + """ + Redirect stderr to logger. + + Captures X11 errors, warnings, and subprocess errors. + """ + logger = logging.getLogger(logger_name) + sys.stderr = StreamToLogger(logger, logging.WARNING, cls._original_stderr) + + @classmethod + def restore_streams(cls) -> None: + """Restore original stdout/stderr.""" + if cls._original_stdout: + sys.stdout = cls._original_stdout + if cls._original_stderr: + sys.stderr = cls._original_stderr + + @classmethod + def shutdown(cls) -> None: + """Close all handlers. Called automatically on exit.""" + # Restore original streams + cls.restore_streams() + + # Close handlers + for handler in cls._handlers.values(): + handler.close() + cls._handlers.clear() + + +def setup_logging( + filename: str = "logs/app.log", + level: int = logging.DEBUG, + fmt: str = DEFAULT_FORMAT, + capture_stdout: bool = False, + capture_stderr: bool = True, + console_output: bool = True, + console_level: int | None = None, +) -> None: + """ + Setup logging for entire application. + + Call once at startup. After this, all modules can use: + import logging + logger = logging.getLogger(__name__) + + Args: + filename: Log file path + level: Logging level + fmt: Log format string + capture_stdout: Redirect stdout (print statements) to logger + capture_stderr: Redirect stderr (X11 errors, warnings) to logger + console_output: Also print logs to console/terminal + console_level: Console log level (defaults to same as level) + """ + LogManager.setup( + filename, + level, + fmt, + capture_stdout, + capture_stderr, + console_output, + console_level, + ) -global MainLoggingHandler +def get_logger( + name: str, + filename: str | None = None, + level: int = logging.INFO, + fmt: str = DEFAULT_FORMAT, +) -> logging.Logger: + """ + Get or create a logger with its own file output. + Args: + name: Logger name + filename: Log file path (defaults to "logs/{name}.log") + level: Logging level + fmt: Log format string -def create_logger( - name: str = "log", - level=logging.INFO, - format: str = "'[%(levelname)s] | %(asctime)s | %(name)s | %(relativeCreated)6d | %(threadName)s : %(message)s", -): - """Create amd return logger""" - global MainLoggingHandler - logger = logging.getLogger(name) - logger.setLevel(level) - ql = QueueListener(filename=name) - MainLoggingHandler = QueueHandler(ql.queue, format, level) - logger.addHandler(MainLoggingHandler) - return ql + Returns: + Configured Logger instance + """ + return LogManager.get_logger(name, filename, level, fmt) From 39fc282b780b725d52fa013ace1fe9525a52dc44 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 30 Jan 2026 14:21:05 +0000 Subject: [PATCH 2/2] fix code formatation --- BlocksScreen/logger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/BlocksScreen/logger.py b/BlocksScreen/logger.py index a5e187c3..c6bf03af 100644 --- a/BlocksScreen/logger.py +++ b/BlocksScreen/logger.py @@ -485,6 +485,7 @@ def setup_logging( console_level, ) + def get_logger( name: str, filename: str | None = None,