Repository: ipython/ipython

Issue:
Feature: Dict traitlet klass attribute for values
Just like `List`, `Dict` traitlet type could have an optional `klass` attribute, that would be a traitlet instance enforcing validation of values of the dict. 



Discussion:
What about the keys? E.g. to be acceptable for JSON, keys must be strings.

Good point. 
Although that would require the `key_type` (or whatever we name it) to be specified for every widget attribute which might be a bit verbose. Or is there an elegant way to make it so that every traitlet such that `sync=True` has its keys validated. 

Maybe it's cleaner to do it as a separate traitlet type JSONableDict. Or
maybe validating that keys are strings is superfluous - getting that wrong
is not a common failure mode, I think.
On 2 Feb 2015 18:20, "Sylvain Corlay" notifications@github.com wrote:

> Good point.
> Although that would require the key_type (or whatever we name it) to be
> specified for every widget attribute which might be a bit verbose. Or is
> there an elegant way to make it so that every traitlet such that sync=True
> has its keys validated.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/ipython/ipython/issues/7671#issuecomment-72580464.

Yeah, especially since there are other reasons why something would not be jsonable.


Resolving patch:
diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py
--- a/IPython/utils/traitlets.py
+++ b/IPython/utils/traitlets.py
@@ -1645,13 +1645,33 @@ def instance_init(self, obj):
 
 class Dict(Instance):
     """An instance of a Python dict."""
+    _trait = None
 
-    def __init__(self, default_value={}, allow_none=False, **metadata):
+    def __init__(self, trait=None, default_value=NoDefaultSpecified, allow_none=False, **metadata):
         """Create a dict trait type from a dict.
 
         The default value is created by doing ``dict(default_value)``,
         which creates a copy of the ``default_value``.
+
+        trait : TraitType [ optional ]
+            the type for restricting the contents of the Container.  If unspecified,
+            types are not checked.
+
+        default_value : SequenceType [ optional ]
+            The default value for the Dict.  Must be dict, tuple, or None, and
+            will be cast to a dict if not None. If `trait` is specified, the
+            `default_value` must conform to the constraints it specifies.
+
+        allow_none : bool [ default False ]
+            Whether to allow the value to be None
+
         """
+        if default_value is NoDefaultSpecified and trait is not None:
+            if not is_trait(trait):
+                default_value = trait
+                trait = None
+        if default_value is NoDefaultSpecified:
+            default_value = {}
         if default_value is None:
             args = None
         elif isinstance(default_value, dict):
@@ -1661,9 +1681,48 @@ def __init__(self, default_value={}, allow_none=False, **metadata):
         else:
             raise TypeError('default value of Dict was %s' % default_value)
 
+        if is_trait(trait):
+            self._trait = trait() if isinstance(trait, type) else trait
+            self._trait.name = 'element'
+        elif trait is not None:
+            raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait))
+
         super(Dict,self).__init__(klass=dict, args=args,
                                   allow_none=allow_none, **metadata)
 
+    def element_error(self, obj, element, validator):
+        e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \
+            % (self.name, class_of(obj), validator.info(), repr_type(element))
+        raise TraitError(e)
+
+    def validate(self, obj, value):
+        value = super(Dict, self).validate(obj, value)
+        if value is None:
+            return value
+        value = self.validate_elements(obj, value)
+        return value
+
+    def validate_elements(self, obj, value):
+        if self._trait is None or isinstance(self._trait, Any):
+            return value
+        validated = {}
+        for key in value:
+            v = value[key]
+            try:
+                v = self._trait._validate(obj, v)
+            except TraitError:
+                self.element_error(obj, v, self._trait)
+            else:
+                validated[key] = v
+        return self.klass(validated)
+
+    def instance_init(self, obj):
+        if isinstance(self._trait, TraitType):
+            self._trait.this_class = self.this_class
+        if hasattr(self._trait, '_resolve_classes'):
+            self._trait._resolve_classes(obj)
+        super(Dict, self).instance_init(obj)
+
 
 class EventfulDict(Instance):
     """An instance of an EventfulDict."""


Test patch:
diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py
--- a/IPython/utils/tests/test_traitlets.py
+++ b/IPython/utils/tests/test_traitlets.py
@@ -1079,6 +1079,19 @@ def test_dict_assignment():
     nt.assert_equal(d, c.value)
     nt.assert_true(c.value is d)
 
+class ValidatedDictTrait(HasTraits):
+
+    value = Dict(Unicode())
+
+class TestInstanceDict(TraitTestBase):
+
+    obj = ValidatedDictTrait()
+
+    _default_value = {}
+    _good_values = [{'0': 'foo'}, {'1': 'bar'}]
+    _bad_values = [{'0': 0}, {'1': 1}]
+
+
 def test_dict_default_value():
     """Check that the `{}` default value of the Dict traitlet constructor is
     actually copied."""