"""
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