diff --git a/IPython/html/static/widgets/js/widget.js b/IPython/html/static/widgets/js/widget.js index 29193e6..9b2fa4d 100644 --- a/IPython/html/static/widgets/js/widget.js +++ b/IPython/html/static/widgets/js/widget.js @@ -213,7 +213,7 @@ define(["widgets/js/manager", var that = this; var packed; if (value instanceof Backbone.Model) { - return value.id; + return "IPY_MODEL_" + value.id; } else if ($.isArray(value)) { packed = []; @@ -252,13 +252,15 @@ define(["widgets/js/manager", }); return unpacked; + } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") { + var model = this.widget_manager.get_model(value.slice(10, value.length)); + if (model) { + return model; + } else { + return value; + } } else { - var model = this.widget_manager.get_model(value); - if (model) { - return model; - } else { return value; - } } }, diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py index 4131826..345a4c7 100644 --- a/IPython/html/widgets/widget.py +++ b/IPython/html/widgets/widget.py @@ -195,8 +195,15 @@ class Widget(LoggingConfigurable): A single property's name to get. """ keys = self.keys if key is None else [key] - return {k: self._pack_widgets(getattr(self, k)) for k in keys} - + state = {} + for k in keys: + f = self.trait_metadata(k, 'to_json') + if f is None: + f = self._trait_to_json + value = getattr(self, k) + state[k] = f(value) + return state + def send(self, content): """Sends a custom msg to the widget model in the front-end. @@ -280,7 +287,10 @@ class Widget(LoggingConfigurable): """Called when a state is received from the front-end.""" for name in self.keys: if name in sync_data: - value = self._unpack_widgets(sync_data[name]) + f = self.trait_metadata(name, 'from_json') + if f is None: + f = self._trait_from_json + value = f(sync_data[name]) with self._lock_property(name, value): setattr(self, name, value) @@ -299,31 +309,34 @@ class Widget(LoggingConfigurable): """Called when a view has been displayed for this widget instance""" self._display_callbacks(self, **kwargs) - def _pack_widgets(self, x): - """Recursively converts all widget instances to model id strings. + def _trait_to_json(self, x): + """Convert a trait value to json - Children widgets will be stored and transmitted to the front-end by - their model ids. Return value must be JSON-able.""" + Traverse lists/tuples and dicts and serialize their values as well. + Replace any widgets with their model_id + """ if isinstance(x, dict): - return {k: self._pack_widgets(v) for k, v in x.items()} + return {k: self._trait_to_json(v) for k, v in x.items()} elif isinstance(x, (list, tuple)): - return [self._pack_widgets(v) for v in x] + return [self._trait_to_json(v) for v in x] elif isinstance(x, Widget): - return x.model_id + return "IPY_MODEL_" + x.model_id else: return x # Value must be JSON-able - def _unpack_widgets(self, x): - """Recursively converts all model id strings to widget instances. + def _trait_from_json(self, x): + """Convert json values to objects - Children widgets will be stored and transmitted to the front-end by - their model ids.""" + Replace any strings representing valid model id values to Widget references. + """ if isinstance(x, dict): - return {k: self._unpack_widgets(v) for k, v in x.items()} + return {k: self._trait_from_json(v) for k, v in x.items()} elif isinstance(x, (list, tuple)): - return [self._unpack_widgets(v) for v in x] - elif isinstance(x, string_types): - return x if x not in Widget.widgets else Widget.widgets[x] + return [self._trait_from_json(v) for v in x] + elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets: + # we want to support having child widgets at any level in a hierarchy + # trusting that a widget UUID will not appear out in the wild + return Widget.widgets[x] else: return x diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py index 9dbeb91..8992345 100644 --- a/IPython/utils/tests/test_traitlets.py +++ b/IPython/utils/tests/test_traitlets.py @@ -560,6 +560,27 @@ class TestInstance(TestCase): self.assertRaises(TraitError, setattr, a, 'inst', Bar) self.assertRaises(TraitError, setattr, a, 'inst', Bah()) + def test_default_klass(self): + class Foo(object): pass + class Bar(Foo): pass + class Bah(object): pass + + class FooInstance(Instance): + klass = Foo + + class A(HasTraits): + inst = FooInstance() + + a = A() + self.assertTrue(a.inst is None) + a.inst = Foo() + self.assertTrue(isinstance(a.inst, Foo)) + a.inst = Bar() + self.assertTrue(isinstance(a.inst, Foo)) + self.assertRaises(TraitError, setattr, a, 'inst', Foo) + self.assertRaises(TraitError, setattr, a, 'inst', Bar) + self.assertRaises(TraitError, setattr, a, 'inst', Bah()) + def test_unique_default_value(self): class Foo(object): pass class A(HasTraits): diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py index 5234713..1a68b04 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -809,8 +809,12 @@ class Instance(ClassBasedTraitType): """A trait whose value must be an instance of a specified class. The value can also be an instance of a subclass of the specified class. + + Subclasses can declare default classes by overriding the klass attribute """ + klass = None + def __init__(self, klass=None, args=None, kw=None, allow_none=True, **metadata ): """Construct an Instance trait. @@ -836,14 +840,17 @@ class Instance(ClassBasedTraitType): ----- If both ``args`` and ``kw`` are None, then the default value is None. If ``args`` is a tuple and ``kw`` is a dict, then the default is - created as ``klass(*args, **kw)``. If either ``args`` or ``kw`` is - not (but not both), None is replace by ``()`` or ``{}``. + created as ``klass(*args, **kw)``. If exactly one of ``args`` or ``kw`` is + None, the None is replaced by ``()`` or ``{}``, respectively. """ - - if (klass is None) or (not (inspect.isclass(klass) or isinstance(klass, py3compat.string_types))): - raise TraitError('The klass argument must be a class' - ' you gave: %r' % klass) - self.klass = klass + if klass is None: + klass = self.klass + + if (klass is not None) and (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)): + self.klass = klass + else: + raise TraitError('The klass attribute must be a class' + ' not: %r' % klass) # self.klass is a class, so handle default_value if args is None and kw is None: