-
-
Notifications
You must be signed in to change notification settings - Fork 571
Description
Current status
In its current status, interactive Panel objects raising errors will either create the traceback in the jupyter cell (depending on panel.config.console_output) or inside the console in a server context.
This is inconvenient in a production context as a raised error may just look like an unresponsive application.
Short description
In a production context as well as during analytical work, quickly knowing why an error is raised is very helpful.
I would like to suggest a decorator to add to any function that is used to render a dynamic pane, e.g. using pn.depends, param.depends, or pn.interact.
This solution could be either included in the standard library, or if it is too niche inside a template or in the gallery.
Proposed solution
add the following decorator in the standard library
import sys
import traceback
import functools
import panel as pn
info_layout = pn.Tabs(closable=True)
def status_decorator(f: callable =None, behavior: str='raise', info_layout: pn.layout.base.ListPanel=info_layout) -> callable:
"""
Parameters
----------
f : callable, optional
The function to decorate.
behavior : str, optional
If 'replace', the object which should have been rendered will be replaced by the error message and the error will not be raised.
If 'raise', the error will be raised, meaning the target object will look frozen.
If 'erase', the function will return None and nothing will be rendered in place of the target object.
The default is 'raise'.
In all cases, the error message will be appended to the list-like panel object 'info_layout'
info_layout : pn.layout.base.ListPanel, optional
A list-like panel object where each element is an error status. The default is info_layout.
Returns
-------
callable
The decorated function.
"""
def mydec(f):
@functools.wraps(f)
def wrapper(*args,**kwargs):
try:
#info_layout.clear()
return f(*args,**kwargs)
except Exception as e:
v, e, tb = sys.exc_info()
stack = traceback.extract_tb(tb)
(filename, line, procname, text) = stack[-1]
info = pn.pane.Alert(object=repr(stack[-1]), height=100)
info_layout.append((procname,info))
if behavior == 'replace':
return info
elif behavior == 'raise':
raise e
elif behavior == 'erase':
return
return wrapper
if f is None:
return mydec
else:
return mydec(f)The ListPanel object I am using is Tabs, so that the user can close it once they acknowledge they read it. Whenever #1555 is closed, any type of list type layout would work well.
Could be also associated with a logging, see #1978 .
Additionally, it could become a class decorator so that several functions can be overwritten. For example a user could redefine what happens within the "try" close, like activating a loading spinner or so.
in context
pn.interact
Here order is important! The status_decorator must come after the interact decorator.
pn.extension()
@pn.interact(x=True, y=1.0)
@status_decorator(replace=True)
def g(x, y):
if y>2:
raise ValueError('y too big!')
return (x, y)
ginteract_dec.mp4
pn.depends with a template
import holoviews as hv
pn.extension('echarts')
bootstrap = pn.template.BootstrapTemplate(title='Bootstrap Template')
pn.config.sizing_mode = 'stretch_width'
xs = np.linspace(0, np.pi)
freq = pn.widgets.FloatSlider(name="Frequency", start=0, end=10, value=2)
phase = pn.widgets.FloatSlider(name="Phase", start=0, end=np.pi)
@status_decorator
@pn.depends(freq=freq, phase=phase)
def sine(freq, phase):
return hv.Curve((xs, np.sin(xs*freq+phase))).opts(
responsive=True, min_height=400)
@pn.depends(freq=freq)
@status_decorator(behavior='replace')
def throwerr(freq):
if freq>8:
raise ValueError("I don't like your freq")
return pn.indicators.Gauge(name='Frequency', value=freq, bounds=(0, 8))
@pn.depends(freq=freq)
@status_decorator(behavior='raise')
def throwerr2(freq):
if freq>8:
raise ValueError("I don't like your freq")
return pn.indicators.Number(name='Frequency', value=freq)
bootstrap.sidebar.append(freq)
bootstrap.sidebar.append(phase)
bootstrap.sidebar.append(info_layout)
bootstrap.main.append(
pn.Row(
pn.Card(hv.DynamicMap(sine), title='Sine'),
throwerr,
throwerr2
)
)
bootstrap.modal.append(pn.pane.Markdown('## HELLO!'))
bootstrap.servable();template_depends_dec.mp4
param.depends
class A(param.Parameterized):
with_throttled_enabled = param.Range(
default=(100, 250),
bounds=(0, 250),
)
def __init__(self, **params):
super().__init__(**params)
widgets = {
"with_throttled_enabled": {
"type": pn.widgets.IntRangeSlider,
"throttled": False,
},
}
self.controls = pn.Param(self, widgets=widgets)
@param.depends("controls")
@status_decorator(behavior='erase')
def calculation(self):
if self.with_throttled_enabled[0] > 130:
raise ValueError('too low')
return pn.Pane((self.with_throttled_enabled), min_width=200)
@param.depends("controls")
@status_decorator(behavior='erase')
def buggy_calculation(self):
return pn.Pane(self.with_throttled_enabled[1]/self.with_throttled_enabled[0], min_width=200)
a = A()
pn.Column(a.controls, pn.pane.Markdown('### will bug above 130'), a.calculation, pn.pane.Markdown('### division by 0'), a.buggy_calculation, info_layout)param_depends.mp4
Additional context
After some feedback from the main dev I can submit a PR.