""" Jupyter notebook interface for Param (https://github.com/ioam/param). Given a Parameterized object, displays a box with an ipywidget for each Parameter, allowing users to view and and manipulate Parameter values from within a Jupyter/IPython notebook. """ from __future__ import absolute_import import os import ast import uuid import itertools import json import functools from collections import OrderedDict import param import ipywidgets from IPython.display import display, Javascript, HTML, clear_output from . import widgets from .widgets import wtype, apply_error_style, literal_params, Output from .util import named_objs, get_method_owner from .view import View, HTML as HTMLView from param.version import Version __version__ = str(param.Version(fpath=__file__,archive_commit="bdf1856",reponame="paramnb")) del Version def run_next_cells(n): if n=='all': n = 'NaN' elif n<1: return js_code = """ var num = {0}; var run = false; var current = $(this)[0]; $.each(IPython.notebook.get_cells(), function (idx, cell) {{ if ((cell.output_area === current) && !run) {{ run = true; }} else if ((cell.cell_type == 'code') && !(num < 1) && run) {{ cell.execute(); num = num - 1; }} }}); """.format(n) display(Javascript(js_code)) def estimate_label_width(labels): """ Given a list of labels, estimate the width in pixels and return in a format accepted by CSS. Necessarily an approximation, since the font is unknown and is usually proportionally spaced. """ max_length = max([len(l) for l in labels]) return "{0}px".format(max(60,int(max_length*7.5))) class Widgets(param.ParameterizedFunction): callback = param.Callable(default=None, doc=""" Custom callable to execute on button press (if `button`) else whenever a widget is changed, Should accept a Parameterized object argument.""") view_position = param.ObjectSelector(default='below', objects=['below', 'right', 'left', 'above'], doc=""" Layout position of any View parameter widgets.""") next_n = param.Parameter(default=0, doc=""" When executing cells, integer number to execute (or 'all'). A value of zero means not to control cell execution.""") on_init = param.Boolean(default=False, doc=""" Whether to do the action normally taken (executing cells and/or calling a callable) when first instantiating this object.""") close_button = param.Boolean(default=False, doc=""" Whether to show a button allowing the Widgets to be closed.""") button = param.Boolean(default=False, doc=""" Whether to show a button to control cell execution. If false, will execute `next` cells on any widget value change.""") label_width = param.Parameter(default=estimate_label_width, doc=""" Width of the description for parameters in the list, using any string specification accepted by CSS (e.g. "100px" or "50%"). If set to a callable, will call that function using the list of all labels to get the value.""") tooltips = param.Boolean(default=True, doc=""" Whether to add tooltips to the parameter names to show their docstrings.""") show_labels = param.Boolean(default=True) display_threshold = param.Number(default=0,precedence=-10,doc=""" Parameters with precedence below this value are not displayed.""") default_precedence = param.Number(default=1e-8,precedence=-10,doc=""" Precedence value to use for parameters with no declared precedence. By default, zero predecence is available for forcing some parameters to the top of the list, and other values above the default_precedence values can be used to sort or group parameters arbitrarily.""") initializer = param.Callable(default=None, doc=""" User-supplied function that will be called on initialization, usually to update the default Parameter values of the underlying parameterized object.""") layout = param.ObjectSelector(default='column', objects=['row','column'],doc=""" Whether to lay out the buttons as a row or a column.""") continuous_update = param.Boolean(default=False, doc=""" If true, will continuously update the next_n and/or callback, if any, as a slider widget is dragged.""") def __call__(self, parameterized, plots=[], **params): self.p = param.ParamOverrides(self, params) if self.p.initializer: self.p.initializer(parameterized) self._id = uuid.uuid4().hex self._widgets = {} self.parameterized = parameterized widgets, views = self.widgets() layout = ipywidgets.Layout(display='flex', flex_flow=self.p.layout) if self.p.close_button: layout.border = 'solid 1px' widget_box = ipywidgets.VBox(children=widgets, layout=layout) plot_outputs = tuple(Output() for p in plots) if views or plots: outputs = tuple(views.values()) + plot_outputs view_box = ipywidgets.VBox(children=outputs, layout=layout) layout = self.p.view_position if layout in ['below', 'right']: children = [widget_box, view_box] else: children = [view_box, widget_box] box = ipywidgets.VBox if layout in ['below', 'above'] else ipywidgets.HBox widget_box = box(children=children) display(widget_box) self._widget_box = widget_box self._display_handles = {} # Render defined View parameters for pname, view in views.items(): p_obj = self.parameterized.params(pname) value = getattr(self.parameterized, pname) if value is None: continue handle = self._update_trait(pname, p_obj.renderer(value)) if handle: self._display_handles[pname] = handle # Render supplied plots for p, o in zip(plots, plot_outputs): with o: display(p) # Keeps track of changes between button presses self._changed = {} if self.p.on_init: self.execute() def _update_trait(self, p_name, p_value, widget=None): p_obj = self.parameterized.params(p_name) widget = self._widgets[p_name] if widget is None else widget if isinstance(p_value, tuple): p_value, size = p_value if isinstance(size, tuple) and len(size) == 2: if isinstance(widget, ipywidgets.Image): widget.width = size[0] widget.height = size[1] else: widget.layout.min_width = '%dpx' % size[0] widget.layout.min_height = '%dpx' % size[1] if isinstance(widget, Output): if isinstance(p_obj, HTMLView) and p_value: p_value = HTML(p_value) with widget: # clear_output required for JLab support # in future handle.update(p_value) should be sufficient handle = self._display_handles.get(p_name) if handle: clear_output(wait=True) handle.display(p_value) else: handle = display(p_value, display_id=p_name+self._id) self._display_handles[p_name] = handle else: widget.value = p_value def _make_widget(self, p_name): p_obj = self.parameterized.params(p_name) widget_class = wtype(p_obj) value = getattr(self.parameterized, p_name) # For ObjectSelector, pick first from objects if no default; # see https://github.com/ioam/param/issues/164 if hasattr(p_obj,'objects') and len(p_obj.objects)>0 and value is None: value = p_obj.objects[0] if isinstance(p_obj,param.ListSelector): value = [value] setattr(self.parameterized, p_name, value) kw = dict(value=value) if p_obj.doc: kw['tooltip'] = p_obj.doc if isinstance(p_obj, param.Action): def action_cb(button): getattr(self.parameterized, p_name)(self.parameterized) kw['value'] = action_cb kw['name'] = p_name kw['continuous_update']=self.p.continuous_update if hasattr(p_obj, 'callbacks'): kw.pop('value', None) if hasattr(p_obj, 'get_range'): kw['options'] = named_objs(p_obj.get_range().items()) if hasattr(p_obj, 'get_soft_bounds'): kw['min'], kw['max'] = p_obj.get_soft_bounds() if hasattr(p_obj,'is_instance') and p_obj.is_instance: kw['options'][kw['value'].__class__.__name__]=kw['value'] w = widget_class(**kw) if hasattr(p_obj, 'callbacks') and value is not None: self._update_trait(p_name, p_obj.renderer(value), w) def change_event(event): new_values = event['new'] error = False # Apply literal evaluation to values if (isinstance(w, ipywidgets.Text) and isinstance(p_obj, literal_params)): try: new_values = ast.literal_eval(new_values) except: error = 'eval' elif hasattr(p_obj,'is_instance') and p_obj.is_instance and isinstance(new_values,type): # results in new instance each time non-default option # is selected; could consider caching. try: # awkward: support ParameterizedFunction new_values = new_values.instance() if hasattr(new_values,'instance') else new_values() except: error = 'instantiate' # If no error during evaluation try to set parameter if not error: try: setattr(self.parameterized, p_name, new_values) except ValueError: error = 'validation' # Style widget to denote error state apply_error_style(w, error) if not error and not self.p.button: self.execute({p_name: new_values}) else: self._changed[p_name] = new_values if hasattr(p_obj, 'callbacks'): p_obj.callbacks[id(self.parameterized)] = functools.partial(self._update_trait, p_name) else: w.observe(change_event, 'value') # Hack ; should be part of Widget classes if hasattr(p_obj,"path"): def path_change_event(event): new_values = event['new'] p_obj = self.parameterized.params(p_name) p_obj.path = new_values p_obj.update() # Update default value in widget, ensuring it's always a legal option selector = self._widgets[p_name].children[1] defaults = p_obj.default if not issubclass(type(defaults),list): defaults = [defaults] selector.options.update(named_objs(zip(defaults,defaults))) selector.value=p_obj.default selector.options=named_objs(p_obj.get_range().items()) if p_obj.objects and not self.p.button: self.execute({p_name:selector.value}) path_w = ipywidgets.Text(value=p_obj.path) path_w.observe(path_change_event, 'value') w = ipywidgets.VBox(children=[path_w,w], layout=ipywidgets.Layout(margin='0')) return w def widget(self, param_name): """Get widget for param_name""" if param_name not in self._widgets: self._widgets[param_name] = self._make_widget(param_name) return self._widgets[param_name] def execute(self, changed={}): run_next_cells(self.p.next_n) if self.p.callback is not None: if get_method_owner(self.p.callback) is self.parameterized: self.p.callback(**changed) else: self.p.callback(self.parameterized, **changed) # Define some settings :) preamble = """ <style> .widget-dropdown .dropdown-menu { width: 100% } .widget-select-multiple select { min-height: 100px; min-width: 300px;} </style> """ label_format = """<div title="{2}" style="padding: 5px; width: {0}; text-align: right;">{1}</div>""" def helptip(self,obj): """Return HTML code formatting a tooltip if help is available""" helptext = obj.__doc__ return "" if (not self.p.tooltips or not helptext) else helptext def widgets(self): """Return name,widget boxes for all parameters (i.e., a property sheet)""" params = self.parameterized.params().items() key_fn = lambda x: x[1].precedence if x[1].precedence is not None else self.p.default_precedence sorted_precedence = sorted(params, key=key_fn) outputs = [k for k, p in sorted_precedence if isinstance(p, View)] filtered = [(k,p) for (k,p) in sorted_precedence if ((p.precedence is None) or (p.precedence >= self.p.display_threshold)) and k not in outputs] groups = itertools.groupby(filtered, key=key_fn) sorted_groups = [sorted(grp) for (k,grp) in groups] ordered_params = [el[0] for group in sorted_groups for el in group] # Format name specially widgets = [ipywidgets.HTML(self.preamble + '<div class="ttip"><b>{0}</b>'.format(self.parameterized.name)+"</div>")] label_width=self.p.label_width if callable(label_width): label_width = label_width(self.parameterized.params().keys()) def format_name(pname): p = self.parameterized.params(pname) # omit name for buttons, which already show the name on the button name = "" if issubclass(type(p),param.Action) else pname return ipywidgets.HTML(self.label_format.format(label_width, name, self.helptip(p))) if self.p.show_labels: widgets += [ipywidgets.HBox(children=[format_name(pname),self.widget(pname)]) for pname in ordered_params] else: widgets += [self.widget(pname) for pname in ordered_params] if self.p.close_button: close_button = ipywidgets.Button(description="Close") # TODO: what other cleanup should be done? close_button.on_click(lambda _: self._widget_box.close()) widgets.append(close_button) if self.p.button and not (self.p.callback is None and self.p.next_n==0): label = 'Run %s' % self.p.next_n if self.p.next_n != 'all' else "Run" display_button = ipywidgets.Button(description=label) def click_cb(button): # Execute and clear changes since last button press try: self.execute(self._changed) except Exception as e: self._changed.clear() raise e self._changed.clear() display_button.on_click(click_cb) widgets.append(display_button) outputs = OrderedDict([(pname, self.widget(pname)) for pname in outputs]) return widgets, outputs # TODO: this is awkward. An alternative would be to import Widgets in # widgets.py only at the point(s) where Widgets is needed rather than # at the top level (to avoid circular imports). Probably some # reorganization would be better, though. widgets.editor = functools.partial(Widgets,close_button=True) class JSONInit(param.Parameterized): """ Callable that can be passed to Widgets.initializer to set Parameter values using JSON. There are three approaches that may be used: 1. If the json_file argument is specified, this takes precedence. 2. The JSON file path can be specified via an environment variable. 3. The JSON can be read directly from an environment variable. Here is an easy example of setting such an environment variable on the commandline: PARAMNB_INIT='{"p1":5}' jupyter notebook This addresses any JSONInit instances that are inspecting the default environment variable called PARAMNB_INIT, instructing it to set the 'p1' parameter to 5. """ varname = param.String(default='PARAMNB_INIT', doc=""" The name of the environment variable containing the JSON specification.""") target = param.String(default=None, doc=""" Optional key in the JSON specification dictionary containing the desired parameter values.""") json_file = param.String(default=None, doc=""" Optional path to a JSON file containing the parameter settings.""") def __call__(self, parameterized): warnobj = param.main if isinstance(parameterized, type) else parameterized param_class = (parameterized if isinstance(parameterized, type) else parameterized.__class__) target = self.target if self.target is not None else param_class.__name__ env_var = os.environ.get(self.varname, None) if env_var is None and self.json_file is None: return if self.json_file or env_var.endswith('.json'): try: fname = self.json_file if self.json_file else env_var spec = json.load(open(os.path.abspath(fname), 'r')) except: warnobj.warning('Could not load JSON file %r' % spec) else: spec = json.loads(env_var) if not isinstance(spec, dict): warnobj.warning('JSON parameter specification must be a dictionary.') return if target in spec: params = spec[target] else: params = spec for name, value in params.items(): try: parameterized.set_param(**{name:value}) except ValueError as e: warnobj.warning(str(e)) ## # make pyct's example/data commands available if possible from functools import partial try: from pyct.cmd import copy_examples as _copy, fetch_data as _fetch, examples as _examples copy_examples = partial(_copy, 'paramnb') fetch_data = partial(_fetch, 'paramnb') examples = partial(_examples, 'paramnb') except ImportError: def _missing_cmd(*args,**kw): return("install pyct to enable this command (e.g. `conda install pyct` or `pip install pyct[cmd]`)") _copy = _fetch = _examples = _missing_cmd def _err(): raise ValueError(_missing_cmd()) fetch_data = copy_examples = examples = _err del partial, _examples, _copy, _fetch ##