##// END OF EJS Templates
Merge pull request #8142 from SylvainCorlay/slider_validation...
Min RK -
r20989:e99202ca merge
parent child Browse files
Show More
@@ -25,7 +25,7 b' from IPython.core import release, crashhandler'
25 from IPython.core.profiledir import ProfileDir, ProfileDirError
25 from IPython.core.profiledir import ProfileDir, ProfileDirError
26 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir, ensure_dir_exists
26 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir, ensure_dir_exists
27 from IPython.utils import py3compat
27 from IPython.utils import py3compat
28 from IPython.utils.traitlets import List, Unicode, Type, Bool, Dict, Set, Instance
28 from IPython.utils.traitlets import List, Unicode, Type, Bool, Dict, Set, Instance, Undefined
29
29
30 if os.name == 'nt':
30 if os.name == 'nt':
31 programdata = os.environ.get('PROGRAMDATA', None)
31 programdata = os.environ.get('PROGRAMDATA', None)
@@ -215,7 +215,7 b' class BaseIPythonApplication(Application):'
215 return crashhandler.crash_handler_lite(etype, evalue, tb)
215 return crashhandler.crash_handler_lite(etype, evalue, tb)
216
216
217 def _ipython_dir_changed(self, name, old, new):
217 def _ipython_dir_changed(self, name, old, new):
218 if old is not None:
218 if old is not None and old is not Undefined:
219 str_old = py3compat.cast_bytes_py2(os.path.abspath(old),
219 str_old = py3compat.cast_bytes_py2(os.path.abspath(old),
220 sys.getfilesystemencoding()
220 sys.getfilesystemencoding()
221 )
221 )
@@ -1,4 +1,4 b''
1 """Float class.
1 """Float class.
2
2
3 Represents an unbounded float using a widget.
3 Represents an unbounded float using a widget.
4 """
4 """
@@ -15,7 +15,8 b' Represents an unbounded float using a widget.'
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 from .widget import DOMWidget, register
16 from .widget import DOMWidget, register
17 from .trait_types import Color
17 from .trait_types import Color
18 from IPython.utils.traitlets import Unicode, CFloat, Bool, CaselessStrEnum, Tuple
18 from IPython.utils.traitlets import (Unicode, CFloat, Bool, CaselessStrEnum,
19 Tuple, TraitError)
19 from IPython.utils.warn import DeprecatedClass
20 from IPython.utils.warn import DeprecatedClass
20
21
21 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
@@ -31,39 +32,37 b' class _Float(DOMWidget):'
31 kwargs['value'] = value
32 kwargs['value'] = value
32 super(_Float, self).__init__(**kwargs)
33 super(_Float, self).__init__(**kwargs)
33
34
35
34 class _BoundedFloat(_Float):
36 class _BoundedFloat(_Float):
35 max = CFloat(100.0, help="Max value", sync=True)
37 max = CFloat(100.0, help="Max value", sync=True)
36 min = CFloat(0.0, help="Min value", sync=True)
38 min = CFloat(0.0, help="Min value", sync=True)
37 step = CFloat(0.1, help="Minimum step that the value can take (ignored by some views)", sync=True)
39 step = CFloat(0.1, help="Minimum step to increment the value (ignored by some views)", sync=True)
38
40
39 def __init__(self, *pargs, **kwargs):
41 def __init__(self, *pargs, **kwargs):
40 """Constructor"""
42 """Constructor"""
41 super(_BoundedFloat, self).__init__(*pargs, **kwargs)
43 super(_BoundedFloat, self).__init__(*pargs, **kwargs)
42 self._handle_value_changed('value', None, self.value)
43 self._handle_max_changed('max', None, self.max)
44 self._handle_min_changed('min', None, self.min)
45 self.on_trait_change(self._handle_value_changed, 'value')
46 self.on_trait_change(self._handle_max_changed, 'max')
47 self.on_trait_change(self._handle_min_changed, 'min')
48
44
49 def _handle_value_changed(self, name, old, new):
45 def _value_validate(self, value, trait):
50 """Validate value."""
46 """Cap and floor value"""
51 if self.min > new or new > self.max:
47 if self.min > value or self.max < value:
52 self.value = min(max(new, self.min), self.max)
48 value = min(max(value, self.min), self.max)
49 return value
53
50
54 def _handle_max_changed(self, name, old, new):
51 def _min_validate(self, min, trait):
55 """Make sure the min is always <= the max."""
52 """Enforce min <= value <= max"""
56 if new < self.min:
53 if min > self.max:
57 raise ValueError("setting max < min")
54 raise TraitError("Setting min > max")
58 if new < self.value:
55 if min > self.value:
59 self.value = new
56 self.value = min
57 return min
60
58
61 def _handle_min_changed(self, name, old, new):
59 def _max_validate(self, max, trait):
62 """Make sure the max is always >= the min."""
60 """Enforce min <= value <= max"""
63 if new > self.max:
61 if max < self.min:
64 raise ValueError("setting min > max")
62 raise TraitError("setting max < min")
65 if new > self.value:
63 if max < self.value:
66 self.value = new
64 self.value = max
65 return max
67
66
68
67
69 @register('IPython.FloatText')
68 @register('IPython.FloatText')
@@ -76,9 +75,9 b' class FloatText(_Float):'
76 value : float
75 value : float
77 value displayed
76 value displayed
78 description : str
77 description : str
79 description displayed next to the textbox
78 description displayed next to the text box
80 color : str Unicode color code (eg. '#C13535'), optional
79 color : str Unicode color code (eg. '#C13535'), optional
81 color of the value displayed
80 color of the value displayed
82 """
81 """
83 _view_name = Unicode('FloatTextView', sync=True)
82 _view_name = Unicode('FloatTextView', sync=True)
84
83
@@ -1,4 +1,4 b''
1 """Int class.
1 """Int class.
2
2
3 Represents an unbounded int using a widget.
3 Represents an unbounded int using a widget.
4 """
4 """
@@ -15,7 +15,8 b' Represents an unbounded int using a widget.'
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 from .widget import DOMWidget, register
16 from .widget import DOMWidget, register
17 from .trait_types import Color
17 from .trait_types import Color
18 from IPython.utils.traitlets import Unicode, CInt, Bool, CaselessStrEnum, Tuple
18 from IPython.utils.traitlets import (Unicode, CInt, Bool, CaselessStrEnum,
19 Tuple, TraitError)
19 from IPython.utils.warn import DeprecatedClass
20 from IPython.utils.warn import DeprecatedClass
20
21
21 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
@@ -32,41 +33,39 b' class _Int(DOMWidget):'
32 kwargs['value'] = value
33 kwargs['value'] = value
33 super(_Int, self).__init__(**kwargs)
34 super(_Int, self).__init__(**kwargs)
34
35
36
35 class _BoundedInt(_Int):
37 class _BoundedInt(_Int):
36 """Base class used to create widgets that represent a int that is bounded
38 """Base class used to create widgets that represent a int that is bounded
37 by a minium and maximum."""
39 by a minium and maximum."""
38 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
40 step = CInt(1, help="Minimum step to increment the value (ignored by some views)", sync=True)
39 max = CInt(100, help="Max value", sync=True)
41 max = CInt(100, help="Max value", sync=True)
40 min = CInt(0, help="Min value", sync=True)
42 min = CInt(0, help="Min value", sync=True)
41
43
42 def __init__(self, *pargs, **kwargs):
44 def __init__(self, *pargs, **kwargs):
43 """Constructor"""
45 """Constructor"""
44 super(_BoundedInt, self).__init__(*pargs, **kwargs)
46 super(_BoundedInt, self).__init__(*pargs, **kwargs)
45 self._handle_value_changed('value', None, self.value)
47
46 self._handle_max_changed('max', None, self.max)
48 def _value_validate(self, value, trait):
47 self._handle_min_changed('min', None, self.min)
49 """Cap and floor value"""
48 self.on_trait_change(self._handle_value_changed, 'value')
50 if self.min > value or self.max < value:
49 self.on_trait_change(self._handle_max_changed, 'max')
51 value = min(max(value, self.min), self.max)
50 self.on_trait_change(self._handle_min_changed, 'min')
52 return value
51
53
52 def _handle_value_changed(self, name, old, new):
54 def _min_validate(self, min, trait):
53 """Validate value."""
55 """Enforce min <= value <= max"""
54 if self.min > new or new > self.max:
56 if min > self.max:
55 self.value = min(max(new, self.min), self.max)
57 raise TraitError("Setting min > max")
56
58 if min > self.value:
57 def _handle_max_changed(self, name, old, new):
59 self.value = min
58 """Make sure the min is always <= the max."""
60 return min
59 if new < self.min:
61
60 raise ValueError("setting max < min")
62 def _max_validate(self, max, trait):
61 if new < self.value:
63 """Enforce min <= value <= max"""
62 self.value = new
64 if max < self.min:
63
65 raise TraitError("setting max < min")
64 def _handle_min_changed(self, name, old, new):
66 if max < self.value:
65 """Make sure the max is always >= the min."""
67 self.value = max
66 if new > self.max:
68 return max
67 raise ValueError("setting min > max")
68 if new > self.value:
69 self.value = new
70
69
71 @register('IPython.IntText')
70 @register('IPython.IntText')
72 class IntText(_Int):
71 class IntText(_Int):
@@ -1333,11 +1333,20 b' def test_pickle_hastraits():'
1333
1333
1334 def test_hold_trait_notifications():
1334 def test_hold_trait_notifications():
1335 changes = []
1335 changes = []
1336
1336 class Test(HasTraits):
1337 class Test(HasTraits):
1337 a = Integer(0)
1338 a = Integer(0)
1339 b = Integer(0)
1340
1338 def _a_changed(self, name, old, new):
1341 def _a_changed(self, name, old, new):
1339 changes.append((old, new))
1342 changes.append((old, new))
1340
1343
1344 def _b_validate(self, value, trait):
1345 if value != 0:
1346 raise TraitError('Only 0 is a valid value')
1347 return value
1348
1349 # Test context manager and nesting
1341 t = Test()
1350 t = Test()
1342 with t.hold_trait_notifications():
1351 with t.hold_trait_notifications():
1343 with t.hold_trait_notifications():
1352 with t.hold_trait_notifications():
@@ -1356,8 +1365,16 b' def test_hold_trait_notifications():'
1356 t.a = 4
1365 t.a = 4
1357 nt.assert_equal(t.a, 4)
1366 nt.assert_equal(t.a, 4)
1358 nt.assert_equal(changes, [])
1367 nt.assert_equal(changes, [])
1359 nt.assert_equal(changes, [(0,1), (1,2), (2,3), (3,4)])
1360
1368
1369 nt.assert_equal(changes, [(3,4)])
1370 # Test roll-back
1371 try:
1372 with t.hold_trait_notifications():
1373 t.b = 1 # raises a Trait error
1374 except:
1375 pass
1376 nt.assert_equal(t.b, 0)
1377
1361
1378
1362 class OrderTraits(HasTraits):
1379 class OrderTraits(HasTraits):
1363 notified = Dict()
1380 notified = Dict()
@@ -319,7 +319,6 b' class TraitType(object):'
319 accept superclasses for :class:`This` values.
319 accept superclasses for :class:`This` values.
320 """
320 """
321
321
322
323 metadata = {}
322 metadata = {}
324 default_value = Undefined
323 default_value = Undefined
325 allow_none = False
324 allow_none = False
@@ -447,7 +446,7 b' class TraitType(object):'
447 try:
446 try:
448 old_value = obj._trait_values[self.name]
447 old_value = obj._trait_values[self.name]
449 except KeyError:
448 except KeyError:
450 old_value = None
449 old_value = Undefined
451
450
452 obj._trait_values[self.name] = new_value
451 obj._trait_values[self.name] = new_value
453 try:
452 try:
@@ -465,13 +464,14 b' class TraitType(object):'
465 return value
464 return value
466 if hasattr(self, 'validate'):
465 if hasattr(self, 'validate'):
467 value = self.validate(obj, value)
466 value = self.validate(obj, value)
468 try:
467 if obj._cross_validation_lock is False:
469 obj_validate = getattr(obj, '_%s_validate' % self.name)
468 value = self._cross_validate(obj, value)
470 except (AttributeError, RuntimeError):
469 return value
471 # Qt mixins raise RuntimeError on missing attrs accessed before __init__
470
472 pass
471 def _cross_validate(self, obj, value):
473 else:
472 if hasattr(obj, '_%s_validate' % self.name):
474 value = obj_validate(value, self)
473 cross_validate = getattr(obj, '_%s_validate' % self.name)
474 value = cross_validate(value, self)
475 return value
475 return value
476
476
477 def __or__(self, other):
477 def __or__(self, other):
@@ -542,6 +542,7 b' class MetaHasTraits(type):'
542 v.this_class = cls
542 v.this_class = cls
543 super(MetaHasTraits, cls).__init__(name, bases, classdict)
543 super(MetaHasTraits, cls).__init__(name, bases, classdict)
544
544
545
545 class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)):
546 class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)):
546
547
547 def __new__(cls, *args, **kw):
548 def __new__(cls, *args, **kw):
@@ -555,6 +556,7 b' class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)):'
555 inst._trait_values = {}
556 inst._trait_values = {}
556 inst._trait_notifiers = {}
557 inst._trait_notifiers = {}
557 inst._trait_dyn_inits = {}
558 inst._trait_dyn_inits = {}
559 inst._cross_validation_lock = True
558 # Here we tell all the TraitType instances to set their default
560 # Here we tell all the TraitType instances to set their default
559 # values on the instance.
561 # values on the instance.
560 for key in dir(cls):
562 for key in dir(cls):
@@ -570,43 +572,74 b' class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)):'
570 value.instance_init()
572 value.instance_init()
571 if key not in kw:
573 if key not in kw:
572 value.set_default_value(inst)
574 value.set_default_value(inst)
573
575 inst._cross_validation_lock = False
574 return inst
576 return inst
575
577
576 def __init__(self, *args, **kw):
578 def __init__(self, *args, **kw):
577 # Allow trait values to be set using keyword arguments.
579 # Allow trait values to be set using keyword arguments.
578 # We need to use setattr for this to trigger validation and
580 # We need to use setattr for this to trigger validation and
579 # notifications.
581 # notifications.
580
581 with self.hold_trait_notifications():
582 with self.hold_trait_notifications():
582 for key, value in iteritems(kw):
583 for key, value in iteritems(kw):
583 setattr(self, key, value)
584 setattr(self, key, value)
584
585
585 @contextlib.contextmanager
586 @contextlib.contextmanager
586 def hold_trait_notifications(self):
587 def hold_trait_notifications(self):
587 """Context manager for bundling trait change notifications
588 """Context manager for bundling trait change notifications and cross
588
589 validation.
589 Use this when doing multiple trait assignments (init, config),
590
590 to avoid race conditions in trait notifiers requesting other trait values.
591 Use this when doing multiple trait assignments (init, config), to avoid
592 race conditions in trait notifiers requesting other trait values.
591 All trait notifications will fire after all values have been assigned.
593 All trait notifications will fire after all values have been assigned.
592 """
594 """
593 _notify_trait = self._notify_trait
595 if self._cross_validation_lock is True:
594 notifications = []
595 self._notify_trait = lambda *a: notifications.append(a)
596
597 try:
598 yield
596 yield
599 finally:
597 return
600 self._notify_trait = _notify_trait
598 else:
601 if isinstance(_notify_trait, types.MethodType):
599 self._cross_validation_lock = True
602 # FIXME: remove when support is bumped to 3.4.
600 cache = {}
603 # when original method is restored,
601 notifications = {}
604 # remove the redundant value from __dict__
602 _notify_trait = self._notify_trait
605 # (only used to preserve pickleability on Python < 3.4)
603
606 self.__dict__.pop('_notify_trait', None)
604 def cache_values(*a):
607 # trigger delayed notifications
605 cache[a[0]] = a
608 for args in notifications:
606
609 self._notify_trait(*args)
607 def hold_notifications(*a):
608 notifications[a[0]] = a
609
610 self._notify_trait = cache_values
611
612 try:
613 yield
614 finally:
615 try:
616 self._notify_trait = hold_notifications
617 for name in cache:
618 if hasattr(self, '_%s_validate' % name):
619 cross_validate = getattr(self, '_%s_validate' % name)
620 setattr(self, name, cross_validate(getattr(self, name), self))
621 except TraitError as e:
622 self._notify_trait = lambda *x: None
623 for name in cache:
624 if cache[name][1] is not Undefined:
625 setattr(self, name, cache[name][1])
626 else:
627 delattr(self, name)
628 cache = {}
629 notifications = {}
630 raise e
631 finally:
632 self._notify_trait = _notify_trait
633 self._cross_validation_lock = False
634 if isinstance(_notify_trait, types.MethodType):
635 # FIXME: remove when support is bumped to 3.4.
636 # when original method is restored,
637 # remove the redundant value from __dict__
638 # (only used to preserve pickleability on Python < 3.4)
639 self.__dict__.pop('_notify_trait', None)
640 # trigger delayed notifications
641 for v in dict(cache, **notifications).values():
642 self._notify_trait(*v)
610
643
611 def _notify_trait(self, name, old_value, new_value):
644 def _notify_trait(self, name, old_value, new_value):
612
645
General Comments 0
You need to be logged in to leave comments. Login now