From 7b94a2e2cb6d9962b5063fcbfab7654831285b2f 2014-02-04 01:15:06
From: MinRK <benjaminrk@gmail.com>
Date: 2014-02-04 01:15:06
Subject: [PATCH] Make `SelectionWidget.values` a dict

rename 'labels' and '_value' to 'value_names' and 'value_name'.

To specify a mapping of value names and values, use a dict.
If you specify `values=[list]`, then an OrderedDict will be used.

Assignment after construction only supports a dict.

---

diff --git a/IPython/html/static/notebook/js/widgets/widget_selection.js b/IPython/html/static/notebook/js/widgets/widget_selection.js
index 5d9791c..30c7ce4 100644
--- a/IPython/html/static/notebook/js/widgets/widget_selection.js
+++ b/IPython/html/static/notebook/js/widgets/widget_selection.js
@@ -57,14 +57,14 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
             // changed by another view or by a state update from the back-end.
 
             if (options === undefined || options.updated_view != this) {
-                var selected_item_text = this.model.get('_value');
+                var selected_item_text = this.model.get('value_name');
                 if (selected_item_text.length === 0) {
                     this.$droplabel.text(' ');
                 } else {
                     this.$droplabel.text(selected_item_text);
                 }
                 
-                var items = this.model.get('labels');
+                var items = this.model.get('value_names');
                 var $replace_droplist = $('<ul />')
                     .addClass('dropdown-menu');
                 var that = this;
@@ -107,7 +107,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
             
             // Calling model.set will trigger all of the other views of the 
             // model to update.
-            this.model.set('_value', $(e.target).text(), {updated_view: this});
+            this.model.set('value_name', $(e.target).text(), {updated_view: this});
             this.touch();
         },
         
@@ -139,7 +139,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
             // changed by another view or by a state update from the back-end.
             if (options === undefined || options.updated_view != this) {
                 // Add missing items to the DOM.
-                var items = this.model.get('labels');
+                var items = this.model.get('value_names');
                 var disabled = this.model.get('disabled');
                 var that = this;
                 _.each(items, function(item, index) {
@@ -159,7 +159,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
                     }
                     
                     var $item_element = that.$container.find(item_query);
-                    if (that.model.get('_value') == item) {
+                    if (that.model.get('value_name') == item) {
                         $item_element.prop('checked', true);
                     } else {
                         $item_element.prop('checked', false);
@@ -199,14 +199,14 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
             
             // Calling model.set will trigger all of the other views of the 
             // model to update.
-            this.model.set('_value', $(e.target).val(), {updated_view: this});
+            this.model.set('value_name', $(e.target).val(), {updated_view: this});
             this.touch();
         },
     });
     WidgetManager.register_widget_view('RadioButtonsView', RadioButtonsView);
 
 
-    var ToggleButtonsView = IPython.DOMWidgetView.extend({    
+    var ToggleButtonsView = IPython.DOMWidgetView.extend({
         render : function(){
             // Called when view is rendered.
             this.$el
@@ -230,7 +230,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
             // changed by another view or by a state update from the back-end.
             if (options === undefined || options.updated_view != this) {
                 // Add missing items to the DOM.
-                var items = this.model.get('labels');
+                var items = this.model.get('value_names');
                 var disabled = this.model.get('disabled');
                 var that = this;
                 _.each(items, function(item, index) {
@@ -245,7 +245,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
                     }
                     
                     var $item_element = that.$buttongroup.find(item_query);
-                    if (that.model.get('_value') == item) {
+                    if (that.model.get('value_name') == item) {
                         $item_element.addClass('active');
                     } else {
                         $item_element.removeClass('active');
@@ -285,7 +285,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
             
             // Calling model.set will trigger all of the other views of the 
             // model to update.
-            this.model.set('_value', $(e.target).text(), {updated_view: this});
+            this.model.set('value_name', $(e.target).text(), {updated_view: this});
             this.touch();
         },    
     });
@@ -316,21 +316,21 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
             // changed by another view or by a state update from the back-end.
             if (options === undefined || options.updated_view != this) {
                 // Add missing items to the DOM.
-                var items = this.model.get('labels');
+                var items = this.model.get('value_names');
                 var that = this;
                 _.each(items, function(item, index) {
                    var item_query = ' :contains("' + item + '")';
                     if (that.$listbox.find(item_query).length === 0) {
                         $('<option />')
                             .text(item)
-                            .attr('_value', item)
+                            .attr('value_name', item)
                             .appendTo(that.$listbox)
                             .on('click', $.proxy(that.handle_click, that));
                     } 
                 });
 
                 // Select the correct element
-                this.$listbox.val(this.model.get('_value'));
+                this.$listbox.val(this.model.get('value_name'));
                 
                 // Disable listbox if needed
                 var disabled = this.model.get('disabled');
@@ -368,7 +368,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
             
             // Calling model.set will trigger all of the other views of the 
             // model to update.
-            this.model.set('_value', $(e.target).text(), {updated_view: this});
+            this.model.set('value_name', $(e.target).text(), {updated_view: this});
             this.touch();
         },    
     });
diff --git a/IPython/html/tests/casperjs/test_cases/widgets_selection.js b/IPython/html/tests/casperjs/test_cases/widgets_selection.js
index abf68a7..e21eced 100644
--- a/IPython/html/tests/casperjs/test_cases/widgets_selection.js
+++ b/IPython/html/tests/casperjs/test_cases/widgets_selection.js
@@ -123,7 +123,9 @@ casper.notebook_test(function () {
 
     index = this.append_cell(
         'for widget in selection:\n' +
-        '    widget.values = list(widget.values) + ["z"]\n' +
+        '    d = widget.values.copy()\n' +
+        '    d["z"] = "z"\n' +
+        '    widget.values = d\n' +
         'selection[0].value = "z"');
     this.execute_cell_then(index, function(index){
 
diff --git a/IPython/html/widgets/widget_selection.py b/IPython/html/widgets/widget_selection.py
index b167027..399b932 100644
--- a/IPython/html/widgets/widget_selection.py
+++ b/IPython/html/widgets/widget_selection.py
@@ -1,4 +1,4 @@
-"""SelectionWidget class.  
+"""SelectionWidget classes.
 
 Represents an enumeration using a widget.
 """
@@ -13,66 +13,84 @@ Represents an enumeration using a widget.
 #-----------------------------------------------------------------------------
 # Imports
 #-----------------------------------------------------------------------------
+
+from collections import OrderedDict
 from threading import Lock
 
 from .widget import DOMWidget
-from IPython.utils.traitlets import Unicode, List, Bool, Any, Dict
+from IPython.utils.traitlets import Unicode, List, Bool, Any, Dict, TraitError
+from IPython.utils.py3compat import unicode_type
 
 #-----------------------------------------------------------------------------
 # SelectionWidget
 #-----------------------------------------------------------------------------
 class _SelectionWidget(DOMWidget):
-    value = Any(help="Selected value") 
-    values = List(help="List of values the user can select")
-    labels = List(help="""List of string representations for each value.  
-        These string representations are used to display the values in the
-        front-end.""", sync=True) # Only synced to the back-end.
+    """Base class for Selection widgets
+    
+    ``values`` can be specified as a list or dict. If given as a list,
+    it will be transformed to a dict of the form ``{str(value):value}``.
+    """
+    
+    value = Any(help="Selected value")
+    values = Dict(help="""Dictionary of {name: value} the user can select.
+    
+    The keys of this dictionary are the strings that will be displayed in the UI,
+    representing the actual Python choices.
+    
+    The keys of this dictionary are also available as value_names.
+    """)
+    value_name = Unicode(help="The name of the selected value", sync=True)
+    value_names = List(Unicode, help="""List of names for each value.
+        
+        If values is specified as a list, this is the string representation of each element.
+        Otherwise, it is the keys of the values dictionary.
+        
+        These strings are used to display the choices in the front-end.""", sync=True)
     disabled = Bool(False, help="Enable or disable user changes", sync=True)
     description = Unicode(help="Description of the value this widget represents", sync=True)
+    
 
-    _value = Unicode(sync=True) # Bi-directionally synced.
-
-    def __init__(self, *pargs, **kwargs):
-        """Constructor"""
+    def __init__(self, *args, **kwargs):
         self.value_lock = Lock()
-        self.on_trait_change(self._string_value_set, ['_value'])
-        DOMWidget.__init__(self, *pargs, **kwargs)
-
-    def _labels_changed(self, name=None, old=None, new=None):
-        """Handles when the value_names Dict has been changed.
+        if 'values' in kwargs:
+            values = kwargs['values']
+            # convert list values to an dict of {str(v):v}
+            if isinstance(values, list):
+                # preserve list order with an OrderedDict
+                kwargs['values'] = OrderedDict((unicode_type(v), v) for v in values)
+        DOMWidget.__init__(self, *args, **kwargs)
+    
+    def _values_changed(self, name, old, new):
+        """Handles when the values dict has been changed.
 
-        This method sets the _reverse_value_names Dict to the inverse of the new
-        value for the value_names Dict."""
+        Setting values implies setting value names from the keys of the dict.
+        """
+        self.value_names = list(new.keys())
+    
+    def _value_names_changed(self, name, old, new):
         if len(new) != len(self.values):
-            raise TypeError('Labels list must be the same size as the values list.')
-
-    def _values_changed(self, name=None, old=None, new=None):
-        """Handles when the value_names Dict has been changed.
-
-        This method sets the _reverse_value_names Dict to the inverse of the new
-        value for the value_names Dict."""
-        if len(new) != len(self.labels):
-            self.labels = [(self.labels[i] if i < len(self.labels) else str(v)) for i, v in enumerate(new)]
+            raise TraitError("Expected %i value names, got %i." % (len(self.values), len(new)))
 
     def _value_changed(self, name, old, new):
         """Called when value has been changed"""
         if self.value_lock.acquire(False):
             try:
-                # Make sure the value is in the list of values.
-                if new in self.values:
-                    # Set the string version of the value.
-                    self._value = self.labels[self.values.index(new)]
-                else:
-                    raise TypeError('Value must be a value in the values list.')
+                # Make sure the value is one of the options
+                for k,v in self.values.items():
+                    if new == v:
+                        # set the selected value name
+                        self.value_name = k
+                        return
+                raise TraitError('Value not found: %r' % new)
             finally:
                 self.value_lock.release()
 
-    def _string_value_set(self, name, old, new):
-        """Called when _value has been changed."""
+    def _value_name_changed(self, name, old, new):
+        """Called when the value name has been changed (typically by the frontend)."""
         if self.value_lock.acquire(False):
             try:
-                if new in self.labels:
-                    self.value = self.values[self.labels.index(new)]
+                if new in self.values:
+                    self.value = self.values[new]
                 else:
                     self.value = None
             finally: