Source code for ClearMap.gui.gui_logging

# -*- coding: utf-8 -*-
"""
gui_logging
===========

Defines the Printer class used to log to file and to the GUI widgets from simple prints
"""
import re
import sys
import traceback
from datetime import datetime
from io import UnsupportedOperation

import lxml.html
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QWidget
from matplotlib.colors import cnames


__author__ = 'Charly Rousseau <charly.rousseau@icm-institute.org>'
__license__ = 'GPLv3 - GNU General Public License v3 (see LICENSE.txt)'
__copyright__ = 'Copyright © 2022 by Charly Rousseau'
__webpage__ = 'https://idisco.info'
__download__ = 'https://github.com/ClearAnatomics/ClearMap'

import pygments
from pygments.formatters.html import HtmlFormatter
from pygments.lexers.python import PythonTracebackLexer  # noqa

from qdarkstyle import DarkPalette

from ClearMap.gui.style import WARNING_YELLOW
_ANSI_RE = re.compile(
    r'(?:'
    r'\x1b\[[0-9;]*[A-Za-z]'           # CSI sequence  ESC [ ... X
    r'|\x1b\][^\x07\x1b\x9c]*'         # OSC open      ESC ] ...
      r'(?:\x07|\x1b\\|\x9c)'          #   ... terminated by BEL, ST, or 0x9C
    r'|\x1b[PX^_][^\x1b\x9c]*'         # DCS / SOS / PM / APC
      r'(?:\x1b\\|\x9c)'               #   ... terminated
    r'|\x1b[@-Z\\-_]'                  # Fe sequences  ESC + one byte
    r'|[\x80-\x9f]'                    # C1 control bytes (includes 0x9C)
    r')'
)


[docs] class Printer(QWidget): #: Triggered on ``write`` text_updated = QtCore.pyqtSignal(str) original_std_out = sys.stdout original_std_err = sys.stderr def __init__(self, log_path=None, color=None, logger_type='info', app=None, open_mode='a', redirects=None, parent=None): super().__init__(parent) self.file = None self.n_lines = 0 self.color = color self.type = logger_type # if logger_type == 'error': # self.setup_except_hook() print(f'Logger initialized with type: {logger_type}, redirects: {redirects}, log path: {log_path}') self.redirects = redirects self._original_stream = None # stored before redirect self._line_buf = '' self.set_file(log_path, open_mode) if app is None: self.app = QApplication.instance() else: self.app = app def __del__(self): self.close_file() @staticmethod def _strip_ansi(text: str) -> str: return _ANSI_RE.sub('', text)
[docs] def close_file(self): # Flush any buffered partial line if self._line_buf: if self.file is not None: self.file.write(self._line_buf + '\n') self.file.flush() self.text_updated.emit(self.colourise(self._line_buf)) self._line_buf = '' try: self.file.close() except AttributeError: pass self.__unset_redirects()
[docs] def set_file(self, log_path, open_mode='a'): self.close_file() # flushes _line_buf via close_file self._line_buf = '' # reset for new file if log_path: self.file = open(log_path, open_mode) self.n_lines = 0 self.__set_redirects()
[docs] def setup_except_hook(self): def except_hook(exc_type, exc_value, exc_tb): lexer = PythonTracebackLexer() default_style = 'native' style = 'nord-darker' if 'nord-darker' in pygments.styles.get_all_styles() else default_style formatter = HtmlFormatter(full=True, style=style, linenos='table', wrapcode=True, noclasses=True) formatter.style.background_color = DarkPalette.COLOR_BACKGROUND_1 raw_traceback = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) formatted_traceback = pygments.highlight(raw_traceback, lexer, formatter) self.write(formatted_traceback) if isinstance(exc_type(), Warning): self.warning_disclaimer() sys.excepthook = except_hook
def __set_redirects(self): if self.redirects == 'stdout': self._original_stream = self.original_std_out self.encoding = sys.stdout.encoding sys.stdout = self elif self.redirects == 'stderr': self._original_stream = self.original_std_err self.encoding = sys.stderr.encoding sys.stderr = self def __unset_redirects(self): if self.redirects == 'stdout': sys.stdout = self._original_stream or self.original_std_out elif self.redirects == 'stderr': sys.stderr = self._original_stream or self.original_std_err self._original_stream = None
[docs] def write(self, msg: str): # Console: pass through verbatim if self.redirects and self._original_stream is not None: try: self._original_stream.write(msg) self._original_stream.flush() except (ValueError, OSError): pass # stream closed # Now strip before the rest clean = self._strip_ansi(msg) # Buffer until we have complete lines self._line_buf += clean if '\n' not in self._line_buf: return # wait for more # Split into complete lines + leftover *lines, self._line_buf = self._line_buf.split('\n') for line in lines: if not line and not lines: # skip empty from trailing \n continue # File if self.file is not None: if self.type in ('error', 'progress'): self.file.write(f'{datetime.now().strftime("%y-%m-%d %H:%M:%S")}: ') self.file.write(line + '\n') self.file.flush() # GUI self.text_updated.emit(self.colourise(line))
[docs] def flush(self): try: self.file.flush() except AttributeError: pass
[docs] def warning_disclaimer(self): self.write(f'<strong><p style="color:{WARNING_YELLOW}">' f'THIS IS A WARNING AND CAN NORMALLY BE SAFELY IGNORED</p></strong>')
[docs] def fileno(self): if self.file is not None: return self.file.fileno() else: raise UnsupportedOperation('Cannot access fileno without a file object')
[docs] def colourise(self, msg, force=False): """ Convert msg to the self.color colour in html Parameters ---------- msg str: The message to colourise force bool: By default, do not colorise html code (to avoid messing up existing colours). This forces the colorise nonetheless. Returns ------- """ if self.color is not None: try: html = lxml.html.fromstring(msg) is_html = html.find('.//*') is not None except lxml.etree.ParserError: is_html = False if force or not is_html: if len(msg.split(':')) > 2 and msg.split(':')[2].endswith('Warning'): html_col = cnames['yellow'] else: html_col = cnames[self.color] colour_msg = f'<p style="color:{html_col}">{msg}</p>' else: colour_msg = msg else: colour_msg = msg return colour_msg