Source code for panel.widgets.debugger

"""
The Debugger Widget is an uneditable Card that gives you feedback on errors
thrown by your Panel callbacks.
"""
from __future__ import annotations

import logging

from typing import (
    ClassVar, Dict, List, Mapping,
)

import param

from ..io.resources import CDN_DIST
from ..io.state import state
from ..layout import Card, HSpacer, Row
from ..reactive import ReactiveHTML
from .terminal import Terminal


[docs]class TermFormatter(logging.Formatter): def __init__(self, *args, only_last=True, **kwargs): """ Standard logging.Formatter with the default option of prompting only the last stack. Does not cache exc_text. Parameters ---------- only_last : BOOLEAN, optional Whether the full stack trace or only the last file should be shown. The default is True. """ super().__init__(*args, **kwargs) self.only_last = only_last
[docs] def format(self, record): record.message = record.getMessage() if self.usesTime(): record.asctime = self.formatTime(record, self.datefmt) s = self.formatMessage(record) exc_text = None if record.exc_info: exc_text = super().formatException(record.exc_info) last = exc_text.rfind('File') if last >0 and self.only_last: exc_text = exc_text[last:] if exc_text: if s[-1:] != "\n": s = s + "\n" s = s + exc_text if record.stack_info: if s[-1:] != "\n": s = s + "\n" s = s + self.formatStack(record.stack_info) return s
[docs]class CheckFilter(logging.Filter):
[docs] def add_debugger(self, debugger): """ Add a debugger to this logging filter. Parameters ---------- widg : panel.widgets.Debugger The widget displaying the logs. Returns ------- None. """ self.debugger = debugger
def _update_debugger(self, record): if not hasattr(self, 'debugger'): return if record.levelno >= 40: self.debugger._number_of_errors += 1 elif 40 > record.levelno >= 30: self.debugger._number_of_warnings += 1 elif record.levelno < 30: self.debugger._number_of_infos += 1
[docs] def filter(self,record): """ Will filter out messages coming from a different bokeh document than the document where the debugger is embedded in server mode. Returns True if no debugger was added. """ if not hasattr(self, 'debugger'): return True if state.curdoc and state.curdoc.session_context: session_id = state.curdoc.session_context.id widget_session_ids = set(m.document.session_context.id for m in sum(self.debugger._models.values(), tuple()) if m.document.session_context) if session_id not in widget_session_ids: return False self._update_debugger(record) return True
[docs]class DebuggerButtons(ReactiveHTML): terminal_output = param.String() debug_name = param.String() clears = param.Integer(default=0) _template: ClassVar[str] = """ <div style="display: flex;"> <button class="special_btn clear_btn" id="clear_btn" onclick="${script('click_clear')}" style="width: ${model.width}px;"> <span class="shown">☐</span> <span class="tooltiptext">Acknowledge logs and clear</span> </button> <button class="special_btn" id="save_btn" onclick="${script('click')}" style="width: ${model.width}px;">💾 <span class="tooltiptext">Save logs</span> </button> </div> """ js_cb: ClassVar[str] = """ var filename = data.debug_name+'.txt' console.log('saving debugger terminal output to '+filename) var blob = new Blob([data.terminal_output], { type: "text/plain;charset=utf-8" }); if (navigator.msSaveBlob) { navigator.msSaveBlob(blob, filename); } else { var link = document.createElement('a'); var url = URL.createObjectURL(blob); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); setTimeout(function() { document.body.removeChild(link); window.URL.revokeObjectURL(url); }, 0); } """ _scripts: ClassVar[Dict[str, str | List[str]]] = { 'click': js_cb, 'click_clear': "data.clears += 1" } _dom_events: ClassVar[Dict[str, List[str]]] = {'clear_btn': ['click']}
[docs]class Debugger(Card): """ A uneditable Card layout holding a terminal printing out logs from your callbacks. By default, it will only print exceptions. If you want to add your own log, use the `panel.callbacks` logger within your callbacks: `logger = logging.getLogger('panel.callbacks')` """ _number_of_errors = param.Integer(bounds=(0, None), precedence=-1, doc=""" Number of logged errors since last acknowledged.""") _number_of_warnings = param.Integer(bounds=(0, None), precedence=-1, doc=""" Number of logged warnings since last acknowledged.""") _number_of_infos = param.Integer(bounds=(0, None), precedence=-1, doc=""" Number of logged information since last acknowledged.""") only_last = param.Boolean(default=True, doc=""" Whether only the last stack is printed or the full.""") level = param.Integer(default=logging.ERROR, doc=""" Logging level to print in the debugger terminal.""") formatter_args = param.Dict( default={'fmt': "%(asctime)s [%(name)s - %(levelname)s]: %(message)s"}, precedence=-1, doc=""" Arguments to pass to the logging formatter. See the standard python logging libraries.""") logger_names = param.List(default=['panel'], item_type=str, bounds=(1, None), precedence=-1, doc=""" Loggers which will be prompted in the debugger terminal.""") _rename: ClassVar[Mapping[str, str | None]] = dict( Card._rename, **{ '_number_of_errors': None, '_number_of_warnings': None, '_number_of_infos': None, 'only_last': None, 'level': None, 'formatter_args': None, 'logger_names': None, }) _stylesheets: ClassVar[List[str]] = [f'{CDN_DIST}css/debugger.css'] def __init__(self, **params): super().__init__(**params) #change default css self.button_css_classes = ['debugger-card-button'] self.css_classes = ['debugger-card'] self.header_css_classes = ['debugger-card-header'] self.title_css_classes = ['debugger-card-title'] smode = 'stretch_width' if self.height else 'stretch_both' height = self.height or self.min_height terminal = Terminal( min_height=200, sizing_mode=smode, name=self.name, margin=0, height=(height-70) if height else None ) stream_handler = logging.StreamHandler(terminal) stream_handler.terminator = " \n" formatter = TermFormatter( **self.formatter_args, only_last=self.only_last ) stream_handler.setFormatter(formatter) stream_handler.setLevel(self.level) curr_filter = CheckFilter() curr_filter.add_debugger(self) stream_handler.addFilter(curr_filter) for logger_name in self.logger_names: logger = logging.getLogger(logger_name) logger.addHandler(stream_handler) self.terminal = terminal self.stream_handler = stream_handler #callbacks for header self.param.watch(self.update_log_counts,'_number_of_errors') self.param.watch(self.update_log_counts,'_number_of_warnings') self.param.watch(self.update_log_counts,'_number_of_infos') # Buttons self.btns = DebuggerButtons(stylesheets=self._stylesheets) inc = """ target.data.terminal_output += source.output """ clr = """ target.data.terminal_output = '' """ self.terminal.jslink(self.btns, code={'_output': inc}) self.terminal.jslink(self.btns, code={'_clears': clr}) self.btns.jslink(self.terminal, clears='_clears') self.terminal.param.watch(self.acknowledge_errors, ['_clears']) self.jslink(self.btns, name='debug_name') #set header self.title = '' #body self.append( Row( f'### {self.name}', HSpacer(), self.btns, sizing_mode='stretch_width', align=('end','start') ) ) self.append(terminal) #make it an uneditable card self.param['objects'].constant = True #by default it should be collapsed and small. self.collapsed = True def update_log_counts(self, event): title = [] if self._number_of_errors: title.append(f'<span style="color:rgb(190,0,0);">errors: </span>{self._number_of_errors}') if self._number_of_warnings: title.append(f'<span style="color:rgb(190,160,20);">w: </span>{self._number_of_warnings}') if self._number_of_infos: title.append(f'i: {self._number_of_infos}') self.title = ', '.join(title) def acknowledge_errors(self, event): self._number_of_errors = 0 self._number_of_warnings = 0 self._number_of_infos = 0 @param.depends("level", watch=True) def _update_level(self): self.stream_handler.setLevel(self.level)