""" The Debugger Widget is an uneditable Card that gives you feedback on errors thrown by your Panel callbacks. """ import param import logging from ..layout.card import Card from ..reactive import ReactiveHTML from ..io.state import state from ..layout import Row, HSpacer from .terminal import Terminal 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. Returns ------- None. """ super().__init__(*args, **kwargs) self.only_last = only_last 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 class CheckFilter(logging.Filter): 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 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) print('>>>', session_id, widget_session_ids) if session_id not in widget_session_ids: return False self._update_debugger(record) return True class DebuggerButtons(ReactiveHTML): terminal_output = param.String() debug_name = param.String() clears = param.Integer(default=0) _template = """
""" js_cb = """ 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 = { 'click': js_cb, 'click_clear': "data.clears += 1" } _dom_events = {'clear_btn': ['click']} 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 informations 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 = Card._rename.copy() _rename.update({ '_number_of_errors': None, '_number_of_warnings': None, '_number_of_infos': None, 'only_last': None, 'level': None, 'formatter_args': None, 'logger_names': None, }) 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'] if self.width and self.height: smode = 'fixed' elif self.width: smode = 'stretch_height' elif self.height: smode = 'stretch_width' else: smode = 'stretch_both' height = self.height or self.min_height width = self.width or self.min_width terminal = Terminal( min_height=200, sizing_mode=smode, name=self.name, margin=5, width=(width-10) if width else None, 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 #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() 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( 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'errors: {self._number_of_errors}') if self._number_of_warnings: title.append(f'w: {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