diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py
index 456133d..417acd7 100644
--- a/IPython/html/widgets/widget.py
+++ b/IPython/html/widgets/widget.py
@@ -292,6 +292,13 @@ class Widget(LoggingConfigurable):
True if the callback should be unregistered."""
self._display_callbacks.register_callback(callback, remove=remove)
+ def add_trait(self, traitname, trait):
+ """Dynamically add a trait attribute to the Widget."""
+ super(Widget, self).add_trait(traitname, trait)
+ if trait.get_metadata('sync'):
+ self.keys.append(traitname)
+ self.send_state(traitname)
+
#-------------------------------------------------------------------------
# Support methods
#-------------------------------------------------------------------------
diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py
index 1b364f7..695bab1 100644
--- a/IPython/utils/tests/test_traitlets.py
+++ b/IPython/utils/tests/test_traitlets.py
@@ -1563,3 +1563,55 @@ class TestForwardDeclaredTypeList(TraitTestBase):
###
# End Forward Declaration Tests
###
+
+class TestDynamicTraits(TestCase):
+
+ def setUp(self):
+ self._notify1 = []
+
+ def notify1(self, name, old, new):
+ self._notify1.append((name, old, new))
+
+ def test_notify_all(self):
+
+ class A(HasTraits):
+ pass
+
+ a = A()
+ self.assertTrue(not hasattr(a, 'x'))
+ self.assertTrue(not hasattr(a, 'y'))
+
+ # Dynamically add trait x.
+ a.add_trait('x', Int())
+ self.assertTrue(hasattr(a, 'x'))
+ self.assertTrue(isinstance(a, (A, )))
+
+ # Dynamically add trait y.
+ a.add_trait('y', Float())
+ self.assertTrue(hasattr(a, 'y'))
+ self.assertTrue(isinstance(a, (A, )))
+ self.assertEqual(a.__class__.__name__, A.__name__)
+
+ # Create a new instance and verify that x and y
+ # aren't defined.
+ b = A()
+ self.assertTrue(not hasattr(b, 'x'))
+ self.assertTrue(not hasattr(b, 'y'))
+
+ # Verify that notification works like normal.
+ a.on_trait_change(self.notify1)
+ a.x = 0
+ self.assertEqual(len(self._notify1), 0)
+ a.y = 0.0
+ self.assertEqual(len(self._notify1), 0)
+ a.x = 10
+ self.assertTrue(('x', 0, 10) in self._notify1)
+ a.y = 10.0
+ self.assertTrue(('y', 0.0, 10.0) in self._notify1)
+ self.assertRaises(TraitError, setattr, a, 'x', 'bad string')
+ self.assertRaises(TraitError, setattr, a, 'y', 'bad string')
+ self._notify1 = []
+ a.on_trait_change(self.notify1, remove=True)
+ a.x = 20
+ a.y = 20.0
+ self.assertEqual(len(self._notify1), 0)
diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py
index 8669822..6728849 100644
--- a/IPython/utils/traitlets.py
+++ b/IPython/utils/traitlets.py
@@ -804,6 +804,12 @@ class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)):
else:
return trait.get_metadata(key, default)
+ def add_trait(self, traitname, trait):
+ """Dynamically add a trait attribute to the HasTraits instance."""
+ self.__class__ = type(self.__class__.__name__, (self.__class__,),
+ {traitname: trait})
+ trait.set_default_value(self)
+
#-----------------------------------------------------------------------------
# Actual TraitTypes implementations/subclasses
#-----------------------------------------------------------------------------