diff --git a/IPython/html/widgets/widget_float.py b/IPython/html/widgets/widget_float.py index afc7936..a7cf157 100644 --- a/IPython/html/widgets/widget_float.py +++ b/IPython/html/widgets/widget_float.py @@ -1,4 +1,4 @@ -"""Float class. +"""Float class. Represents an unbounded float using a widget. """ @@ -15,7 +15,8 @@ Represents an unbounded float using a widget. #----------------------------------------------------------------------------- from .widget import DOMWidget, register from .trait_types import Color -from IPython.utils.traitlets import Unicode, CFloat, Bool, CaselessStrEnum, Tuple +from IPython.utils.traitlets import (Unicode, CFloat, Bool, CaselessStrEnum, + Tuple, TraitError) from IPython.utils.warn import DeprecatedClass #----------------------------------------------------------------------------- @@ -31,39 +32,37 @@ class _Float(DOMWidget): kwargs['value'] = value super(_Float, self).__init__(**kwargs) + class _BoundedFloat(_Float): max = CFloat(100.0, help="Max value", sync=True) min = CFloat(0.0, help="Min value", sync=True) - step = CFloat(0.1, help="Minimum step that the value can take (ignored by some views)", sync=True) + step = CFloat(0.1, help="Minimum step to increment the value (ignored by some views)", sync=True) def __init__(self, *pargs, **kwargs): """Constructor""" super(_BoundedFloat, self).__init__(*pargs, **kwargs) - self._handle_value_changed('value', None, self.value) - self._handle_max_changed('max', None, self.max) - self._handle_min_changed('min', None, self.min) - self.on_trait_change(self._handle_value_changed, 'value') - self.on_trait_change(self._handle_max_changed, 'max') - self.on_trait_change(self._handle_min_changed, 'min') - def _handle_value_changed(self, name, old, new): - """Validate value.""" - if self.min > new or new > self.max: - self.value = min(max(new, self.min), self.max) + def _value_validate(self, value, trait): + """Cap and floor value""" + if self.min > value or self.max < value: + value = min(max(value, self.min), self.max) + return value - def _handle_max_changed(self, name, old, new): - """Make sure the min is always <= the max.""" - if new < self.min: - raise ValueError("setting max < min") - if new < self.value: - self.value = new + def _min_validate(self, min, trait): + """Enforce min <= value <= max""" + if min > self.max: + raise TraitError("Setting min > max") + if min > self.value: + self.value = min + return min - def _handle_min_changed(self, name, old, new): - """Make sure the max is always >= the min.""" - if new > self.max: - raise ValueError("setting min > max") - if new > self.value: - self.value = new + def _max_validate(self, max, trait): + """Enforce min <= value <= max""" + if max < self.min: + raise TraitError("setting max < min") + if max < self.value: + self.value = max + return max @register('IPython.FloatText') @@ -76,9 +75,9 @@ class FloatText(_Float): value : float value displayed description : str - description displayed next to the textbox + description displayed next to the text box color : str Unicode color code (eg. '#C13535'), optional - color of the value displayed + color of the value displayed """ _view_name = Unicode('FloatTextView', sync=True) diff --git a/IPython/html/widgets/widget_int.py b/IPython/html/widgets/widget_int.py index e006054..e76f26b 100644 --- a/IPython/html/widgets/widget_int.py +++ b/IPython/html/widgets/widget_int.py @@ -1,4 +1,4 @@ -"""Int class. +"""Int class. Represents an unbounded int using a widget. """ @@ -15,7 +15,8 @@ Represents an unbounded int using a widget. #----------------------------------------------------------------------------- from .widget import DOMWidget, register from .trait_types import Color -from IPython.utils.traitlets import Unicode, CInt, Bool, CaselessStrEnum, Tuple +from IPython.utils.traitlets import (Unicode, CInt, Bool, CaselessStrEnum, + Tuple, TraitError) from IPython.utils.warn import DeprecatedClass #----------------------------------------------------------------------------- @@ -32,41 +33,39 @@ class _Int(DOMWidget): kwargs['value'] = value super(_Int, self).__init__(**kwargs) + class _BoundedInt(_Int): """Base class used to create widgets that represent a int that is bounded by a minium and maximum.""" - step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True) + step = CInt(1, help="Minimum step to increment the value (ignored by some views)", sync=True) max = CInt(100, help="Max value", sync=True) min = CInt(0, help="Min value", sync=True) def __init__(self, *pargs, **kwargs): """Constructor""" super(_BoundedInt, self).__init__(*pargs, **kwargs) - self._handle_value_changed('value', None, self.value) - self._handle_max_changed('max', None, self.max) - self._handle_min_changed('min', None, self.min) - self.on_trait_change(self._handle_value_changed, 'value') - self.on_trait_change(self._handle_max_changed, 'max') - self.on_trait_change(self._handle_min_changed, 'min') - - def _handle_value_changed(self, name, old, new): - """Validate value.""" - if self.min > new or new > self.max: - self.value = min(max(new, self.min), self.max) - - def _handle_max_changed(self, name, old, new): - """Make sure the min is always <= the max.""" - if new < self.min: - raise ValueError("setting max < min") - if new < self.value: - self.value = new - - def _handle_min_changed(self, name, old, new): - """Make sure the max is always >= the min.""" - if new > self.max: - raise ValueError("setting min > max") - if new > self.value: - self.value = new + + def _value_validate(self, value, trait): + """Cap and floor value""" + if self.min > value or self.max < value: + value = min(max(value, self.min), self.max) + return value + + def _min_validate(self, min, trait): + """Enforce min <= value <= max""" + if min > self.max: + raise TraitError("Setting min > max") + if min > self.value: + self.value = min + return min + + def _max_validate(self, max, trait): + """Enforce min <= value <= max""" + if max < self.min: + raise TraitError("setting max < min") + if max < self.value: + self.value = max + return max @register('IPython.IntText') class IntText(_Int): diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py index 076091a..cf9016f 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -319,7 +319,6 @@ class TraitType(object): accept superclasses for :class:`This` values. """ - metadata = {} default_value = Undefined allow_none = False @@ -447,7 +446,7 @@ class TraitType(object): try: old_value = obj._trait_values[self.name] except KeyError: - old_value = None + old_value = Undefined obj._trait_values[self.name] = new_value try: @@ -465,13 +464,14 @@ class TraitType(object): return value if hasattr(self, 'validate'): value = self.validate(obj, value) - try: - obj_validate = getattr(obj, '_%s_validate' % self.name) - except (AttributeError, RuntimeError): - # Qt mixins raise RuntimeError on missing attrs accessed before __init__ - pass - else: - value = obj_validate(value, self) + if obj._cross_validation_lock is False: + value = self._cross_validate(obj, value) + return value + + def _cross_validate(self, obj, value): + if hasattr(obj, '_%s_validate' % self.name): + cross_validate = getattr(obj, '_%s_validate' % self.name) + value = cross_validate(value, self) return value def __or__(self, other): @@ -542,6 +542,7 @@ class MetaHasTraits(type): v.this_class = cls super(MetaHasTraits, cls).__init__(name, bases, classdict) + class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)): def __new__(cls, *args, **kw): @@ -555,6 +556,7 @@ class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)): inst._trait_values = {} inst._trait_notifiers = {} inst._trait_dyn_inits = {} + inst._cross_validation_lock = True # Here we tell all the TraitType instances to set their default # values on the instance. for key in dir(cls): @@ -570,43 +572,71 @@ class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)): value.instance_init() if key not in kw: value.set_default_value(inst) - + inst._cross_validation_lock = False return inst def __init__(self, *args, **kw): # Allow trait values to be set using keyword arguments. # We need to use setattr for this to trigger validation and # notifications. - with self.hold_trait_notifications(): for key, value in iteritems(kw): setattr(self, key, value) - + @contextlib.contextmanager def hold_trait_notifications(self): - """Context manager for bundling trait change notifications - - Use this when doing multiple trait assignments (init, config), - to avoid race conditions in trait notifiers requesting other trait values. + """Context manager for bundling trait change notifications and cross + validation. + + Use this when doing multiple trait assignments (init, config), to avoid + race conditions in trait notifiers requesting other trait values. All trait notifications will fire after all values have been assigned. """ - _notify_trait = self._notify_trait - notifications = [] - self._notify_trait = lambda *a: notifications.append(a) - - try: + if self._cross_validation_lock is True: yield - finally: - self._notify_trait = _notify_trait - if isinstance(_notify_trait, types.MethodType): - # FIXME: remove when support is bumped to 3.4. - # when original method is restored, - # remove the redundant value from __dict__ - # (only used to preserve pickleability on Python < 3.4) - self.__dict__.pop('_notify_trait', None) - # trigger delayed notifications - for args in notifications: - self._notify_trait(*args) + else: + self._cross_validation_lock = True + cache = {} + notifications = {} + _notify_trait = self._notify_trait + + def cache_values(*a): + cache[a[0]] = a + + def hold_notifications(*a): + notifications[a[0]] = a + + self._notify_trait = cache_values + + try: + yield + finally: + try: + self._notify_trait = hold_notifications + for name in cache: + if hasattr(self, '_%s_validate' % name): + cross_validate = getattr(self, '_%s_validate' % name) + setattr(self, name, cross_validate(getattr(self, name), self)) + except TraitError as e: + self._notify_trait = lambda *x: None + for name in cache: + if cache[name][1] is not Undefined: + setattr(self, name, cache[name][1]) + notifications = {} + raise e + finally: + self._cross_validation_lock = False + self._notify_trait = _notify_trait + if isinstance(_notify_trait, types.MethodType): + # FIXME: remove when support is bumped to 3.4. + # when original method is restored, + # remove the redundant value from __dict__ + # (only used to preserve pickleability on Python < 3.4) + self.__dict__.pop('_notify_trait', None) + + # trigger delayed notifications + for name in notifications: + self._notify_trait(*(notifications[name])) def _notify_trait(self, name, old_value, new_value):