From 1132e56df5037deb3972bd999d6cda64d1c9942e Mon Sep 17 00:00:00 2001 From: "C. E. Ball" Date: Mon, 19 Jun 2017 13:58:20 +0100 Subject: [PATCH 1/2] Added registry of 'watchers', allowing functions to be executed when specific widget values change. --- doc/WidgetLinking.ipynb | 129 ++++++++++++++++++++++++++++++++++++++++ paramnb/__init__.py | 36 +++++++++-- 2 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 doc/WidgetLinking.ipynb diff --git a/doc/WidgetLinking.ipynb b/doc/WidgetLinking.ipynb new file mode 100644 index 0000000..73ac58c --- /dev/null +++ b/doc/WidgetLinking.ipynb @@ -0,0 +1,129 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import param\n", + "import paramnb" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "class SomeManagement(param.Parameterized):\n", + " employee = param.ObjectSelector(default='Person Z',objects=['Person X','Person Y','Person Z'])\n", + " project = param.ObjectSelector(default='A',objects=['A','B','C','D','E','F','G','H'])\n", + " days = param.Integer(default=1,softbounds=(0,10),precedence=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "m = SomeManagement()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "capabilities = { \n", + " 'Person X' : ('A','B','C','D'),\n", + " 'Person Y' : ('A','B','E','F'),\n", + " 'Person Z' : ('G','H'),\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "speed = {\n", + " 'Person X' : 10,\n", + " 'Person Y' : 20,\n", + " 'Person Z' : 30,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def update_days(widgets,**params):\n", + " widgets['days'].max = speed[params['employee']]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def update_projects(widgets,**params):\n", + " widgets['project'].options = capabilities[params['employee']]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "paramnb.Widgets(m, watchers={'employee':(update_projects,update_days)})" + ] + } + ], + "metadata": { + "anaconda-cloud": {}, + "hide_input": false, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.0" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/paramnb/__init__.py b/paramnb/__init__.py index b0842b1..75155a2 100644 --- a/paramnb/__init__.py +++ b/paramnb/__init__.py @@ -128,6 +128,16 @@ class Widgets(param.ParameterizedFunction): If true, will continuously update the next_n and/or callback, if any, as a slider widget is dragged.""") + watchers = param.Dict(default={}, doc=""" + Registry of functions to call whenever a parameter's widget value + is changed. + + Functions will be passed the widgets dictionary and a dictionary + of parameter values. + + Dictionary of lists :( + """) + def __call__(self, parameterized, **params): self.p = param.ParamOverrides(self, params) if self.p.initializer: @@ -163,6 +173,7 @@ def __call__(self, parameterized, **params): if self.p.on_init: self.execute() + self._execute_watchers() def _update_trait(self, p_name, p_value, widget=None): @@ -232,10 +243,13 @@ def change_event(event): # 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 not error: + self._execute_watchers({p_name: new_values}) + if 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) @@ -277,6 +291,20 @@ def widget(self, param_name): return self._widgets[param_name] + def _execute_watchers(self, changed=None): + param_values = dict(self.parameterized.get_param_values()) + del param_values['name'] + + if changed is None: + changed = param_values # assume all values have changed + else: + param_values.update(changed) + + for param in changed: + for fn in self.p.watchers.get(param,[]): + fn(self._widgets,**param_values) + + def execute(self, changed={}): run_next_cells(self.p.next_n) if self.p.callback is not None: From 9ac6d245a8ec75d2f6401afa79b6c0857e8b8d51 Mon Sep 17 00:00:00 2001 From: "C. E. Ball" Date: Tue, 20 Jun 2017 01:59:05 +0100 Subject: [PATCH 2/2] Watcher functions now only need to deal with the parameterized object. --- doc/WidgetLinking.ipynb | 36 +++++++++++++++++++++++++++++------- paramnb/__init__.py | 20 +++++++++++++++++--- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/doc/WidgetLinking.ipynb b/doc/WidgetLinking.ipynb index 73ac58c..0be550c 100644 --- a/doc/WidgetLinking.ipynb +++ b/doc/WidgetLinking.ipynb @@ -19,10 +19,32 @@ "collapsed": true }, "outputs": [], + "source": [ + "# we'd add get_soft_range() to relevant parameters in param\n", + "class ObjectSelector2(param.ObjectSelector):\n", + " __slots__ = ['softbounds']\n", + " def __init__(self,default=None,objects=None,instantiate=False,\n", + " compute_default_fn=None,check_on_set=None,allow_None=None,softbounds=None,**params): \n", + " self.softbounds=softbounds if softbounds is not None else objects \n", + " super(ObjectSelector2,self).__init__(\n", + " default=default,objects=objects,instantiate=instantiate,\n", + " compute_default_fn=compute_default_fn,check_on_set=check_on_set,allow_None=allow_None,**params)\n", + " \n", + " def get_soft_range(self):\n", + " return param.named_objs(self.softbounds)\n", + "\n", + "param.ObjectSelector2 = ObjectSelector2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "class SomeManagement(param.Parameterized):\n", - " employee = param.ObjectSelector(default='Person Z',objects=['Person X','Person Y','Person Z'])\n", - " project = param.ObjectSelector(default='A',objects=['A','B','C','D','E','F','G','H'])\n", + " employee = param.ObjectSelector2(default='Person Z',objects=['Person X','Person Y','Person Z'])\n", + " project = param.ObjectSelector2(default='A',objects=['A','B','C','D','E','F','G','H'])\n", " days = param.Integer(default=1,softbounds=(0,10),precedence=1)" ] }, @@ -75,8 +97,8 @@ }, "outputs": [], "source": [ - "def update_days(widgets,**params):\n", - " widgets['days'].max = speed[params['employee']]" + "def update_days(parameterized):\n", + " parameterized.params('days').softbounds = (0,speed[parameterized.employee])" ] }, { @@ -87,15 +109,15 @@ }, "outputs": [], "source": [ - "def update_projects(widgets,**params):\n", - " widgets['project'].options = capabilities[params['employee']]" + "def update_projects(parameterized):\n", + " parameterized.params('project').softbounds = capabilities[parameterized.employee]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": true + "scrolled": false }, "outputs": [], "source": [ diff --git a/paramnb/__init__.py b/paramnb/__init__.py index 75155a2..38eb76c 100644 --- a/paramnb/__init__.py +++ b/paramnb/__init__.py @@ -132,8 +132,7 @@ class Widgets(param.ParameterizedFunction): Registry of functions to call whenever a parameter's widget value is changed. - Functions will be passed the widgets dictionary and a dictionary - of parameter values. + Functions will be passed the parameterized object. Dictionary of lists :( """) @@ -302,7 +301,22 @@ def _execute_watchers(self, changed=None): for param in changed: for fn in self.p.watchers.get(param,[]): - fn(self._widgets,**param_values) + fn(self.parameterized) + + for param in param_values: + p_obj = self.parameterized.params(param) + + # TODO: as suggested by Philipp, should factor out + # converting parameter attributes to ipywidget attributes + # e.g. have a registry of functions ;) or more likely some + # kind of update method that's used during creation and + # here. + if hasattr(p_obj, 'get_soft_range'): + self._widgets[param].options = p_obj.get_soft_range() + elif hasattr(p_obj, 'get_soft_bounds'): + min,max=p_obj.get_soft_bounds() + self._widgets[param].min = min + self._widgets[param].max = max def execute(self, changed={}):