##// END OF EJS Templates
Merge pull request #8124 from jdfreder/dynamic...
Min RK -
r20932:a748bc5d merge
parent child Browse files
Show More
@@ -1,490 +1,497 b''
1 1 """Base Widget class. Allows user to create widgets in the back-end that render
2 2 in the IPython notebook front-end.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15 from contextlib import contextmanager
16 16 import collections
17 17
18 18 from IPython.core.getipython import get_ipython
19 19 from IPython.kernel.comm import Comm
20 20 from IPython.config import LoggingConfigurable
21 21 from IPython.utils.importstring import import_item
22 22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
23 23 CaselessStrEnum, Tuple, CUnicode, Int, Set
24 24 from IPython.utils.py3compat import string_types
25 25 from .trait_types import Color
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Classes
29 29 #-----------------------------------------------------------------------------
30 30 class CallbackDispatcher(LoggingConfigurable):
31 31 """A structure for registering and running callbacks"""
32 32 callbacks = List()
33 33
34 34 def __call__(self, *args, **kwargs):
35 35 """Call all of the registered callbacks."""
36 36 value = None
37 37 for callback in self.callbacks:
38 38 try:
39 39 local_value = callback(*args, **kwargs)
40 40 except Exception as e:
41 41 ip = get_ipython()
42 42 if ip is None:
43 43 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
44 44 else:
45 45 ip.showtraceback()
46 46 else:
47 47 value = local_value if local_value is not None else value
48 48 return value
49 49
50 50 def register_callback(self, callback, remove=False):
51 51 """(Un)Register a callback
52 52
53 53 Parameters
54 54 ----------
55 55 callback: method handle
56 56 Method to be registered or unregistered.
57 57 remove=False: bool
58 58 Whether to unregister the callback."""
59 59
60 60 # (Un)Register the callback.
61 61 if remove and callback in self.callbacks:
62 62 self.callbacks.remove(callback)
63 63 elif not remove and callback not in self.callbacks:
64 64 self.callbacks.append(callback)
65 65
66 66 def _show_traceback(method):
67 67 """decorator for showing tracebacks in IPython"""
68 68 def m(self, *args, **kwargs):
69 69 try:
70 70 return(method(self, *args, **kwargs))
71 71 except Exception as e:
72 72 ip = get_ipython()
73 73 if ip is None:
74 74 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
75 75 else:
76 76 ip.showtraceback()
77 77 return m
78 78
79 79
80 80 def register(key=None):
81 81 """Returns a decorator registering a widget class in the widget registry.
82 82 If no key is provided, the class name is used as a key. A key is
83 83 provided for each core IPython widget so that the frontend can use
84 84 this key regardless of the language of the kernel"""
85 85 def wrap(widget):
86 86 l = key if key is not None else widget.__module__ + widget.__name__
87 87 Widget.widget_types[l] = widget
88 88 return widget
89 89 return wrap
90 90
91 91
92 92 class Widget(LoggingConfigurable):
93 93 #-------------------------------------------------------------------------
94 94 # Class attributes
95 95 #-------------------------------------------------------------------------
96 96 _widget_construction_callback = None
97 97 widgets = {}
98 98 widget_types = {}
99 99
100 100 @staticmethod
101 101 def on_widget_constructed(callback):
102 102 """Registers a callback to be called when a widget is constructed.
103 103
104 104 The callback must have the following signature:
105 105 callback(widget)"""
106 106 Widget._widget_construction_callback = callback
107 107
108 108 @staticmethod
109 109 def _call_widget_constructed(widget):
110 110 """Static method, called when a widget is constructed."""
111 111 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
112 112 Widget._widget_construction_callback(widget)
113 113
114 114 @staticmethod
115 115 def handle_comm_opened(comm, msg):
116 116 """Static method, called when a widget is constructed."""
117 117 widget_class = import_item(msg['content']['data']['widget_class'])
118 118 widget = widget_class(comm=comm)
119 119
120 120
121 121 #-------------------------------------------------------------------------
122 122 # Traits
123 123 #-------------------------------------------------------------------------
124 124 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
125 125 in which to find _model_name. If empty, look in the global registry.""")
126 126 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
127 127 registered in the front-end to create and sync this widget with.""")
128 128 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
129 129 If empty, look in the global registry.""", sync=True)
130 130 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
131 131 to use to represent the widget.""", sync=True)
132 132 comm = Instance('IPython.kernel.comm.Comm')
133 133
134 134 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
135 135 front-end can send before receiving an idle msg from the back-end.""")
136 136
137 137 version = Int(0, sync=True, help="""Widget's version""")
138 138 keys = List()
139 139 def _keys_default(self):
140 140 return [name for name in self.traits(sync=True)]
141 141
142 142 _property_lock = Tuple((None, None))
143 143 _send_state_lock = Int(0)
144 144 _states_to_send = Set()
145 145 _display_callbacks = Instance(CallbackDispatcher, ())
146 146 _msg_callbacks = Instance(CallbackDispatcher, ())
147 147
148 148 #-------------------------------------------------------------------------
149 149 # (Con/de)structor
150 150 #-------------------------------------------------------------------------
151 151 def __init__(self, **kwargs):
152 152 """Public constructor"""
153 153 self._model_id = kwargs.pop('model_id', None)
154 154 super(Widget, self).__init__(**kwargs)
155 155
156 156 Widget._call_widget_constructed(self)
157 157 self.open()
158 158
159 159 def __del__(self):
160 160 """Object disposal"""
161 161 self.close()
162 162
163 163 #-------------------------------------------------------------------------
164 164 # Properties
165 165 #-------------------------------------------------------------------------
166 166
167 167 def open(self):
168 168 """Open a comm to the frontend if one isn't already open."""
169 169 if self.comm is None:
170 170 args = dict(target_name='ipython.widget',
171 171 data={'model_name': self._model_name,
172 172 'model_module': self._model_module})
173 173 if self._model_id is not None:
174 174 args['comm_id'] = self._model_id
175 175 self.comm = Comm(**args)
176 176
177 177 def _comm_changed(self, name, new):
178 178 """Called when the comm is changed."""
179 179 if new is None:
180 180 return
181 181 self._model_id = self.model_id
182 182
183 183 self.comm.on_msg(self._handle_msg)
184 184 Widget.widgets[self.model_id] = self
185 185
186 186 # first update
187 187 self.send_state()
188 188
189 189 @property
190 190 def model_id(self):
191 191 """Gets the model id of this widget.
192 192
193 193 If a Comm doesn't exist yet, a Comm will be created automagically."""
194 194 return self.comm.comm_id
195 195
196 196 #-------------------------------------------------------------------------
197 197 # Methods
198 198 #-------------------------------------------------------------------------
199 199
200 200 def close(self):
201 201 """Close method.
202 202
203 203 Closes the underlying comm.
204 204 When the comm is closed, all of the widget views are automatically
205 205 removed from the front-end."""
206 206 if self.comm is not None:
207 207 Widget.widgets.pop(self.model_id, None)
208 208 self.comm.close()
209 209 self.comm = None
210 210
211 211 def send_state(self, key=None):
212 212 """Sends the widget state, or a piece of it, to the front-end.
213 213
214 214 Parameters
215 215 ----------
216 216 key : unicode, or iterable (optional)
217 217 A single property's name or iterable of property names to sync with the front-end.
218 218 """
219 219 self._send({
220 220 "method" : "update",
221 221 "state" : self.get_state(key=key)
222 222 })
223 223
224 224 def get_state(self, key=None):
225 225 """Gets the widget state, or a piece of it.
226 226
227 227 Parameters
228 228 ----------
229 229 key : unicode or iterable (optional)
230 230 A single property's name or iterable of property names to get.
231 231 """
232 232 if key is None:
233 233 keys = self.keys
234 234 elif isinstance(key, string_types):
235 235 keys = [key]
236 236 elif isinstance(key, collections.Iterable):
237 237 keys = key
238 238 else:
239 239 raise ValueError("key must be a string, an iterable of keys, or None")
240 240 state = {}
241 241 for k in keys:
242 242 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
243 243 value = getattr(self, k)
244 244 state[k] = f(value)
245 245 return state
246 246
247 247 def set_state(self, sync_data):
248 248 """Called when a state is received from the front-end."""
249 249 for name in self.keys:
250 250 if name in sync_data:
251 251 json_value = sync_data[name]
252 252 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
253 253 with self._lock_property(name, json_value):
254 254 setattr(self, name, from_json(json_value))
255 255
256 256 def send(self, content):
257 257 """Sends a custom msg to the widget model in the front-end.
258 258
259 259 Parameters
260 260 ----------
261 261 content : dict
262 262 Content of the message to send.
263 263 """
264 264 self._send({"method": "custom", "content": content})
265 265
266 266 def on_msg(self, callback, remove=False):
267 267 """(Un)Register a custom msg receive callback.
268 268
269 269 Parameters
270 270 ----------
271 271 callback: callable
272 272 callback will be passed two arguments when a message arrives::
273 273
274 274 callback(widget, content)
275 275
276 276 remove: bool
277 277 True if the callback should be unregistered."""
278 278 self._msg_callbacks.register_callback(callback, remove=remove)
279 279
280 280 def on_displayed(self, callback, remove=False):
281 281 """(Un)Register a widget displayed callback.
282 282
283 283 Parameters
284 284 ----------
285 285 callback: method handler
286 286 Must have a signature of::
287 287
288 288 callback(widget, **kwargs)
289 289
290 290 kwargs from display are passed through without modification.
291 291 remove: bool
292 292 True if the callback should be unregistered."""
293 293 self._display_callbacks.register_callback(callback, remove=remove)
294 294
295 def add_trait(self, traitname, trait):
296 """Dynamically add a trait attribute to the Widget."""
297 super(Widget, self).add_trait(traitname, trait)
298 if trait.get_metadata('sync'):
299 self.keys.append(traitname)
300 self.send_state(traitname)
301
295 302 #-------------------------------------------------------------------------
296 303 # Support methods
297 304 #-------------------------------------------------------------------------
298 305 @contextmanager
299 306 def _lock_property(self, key, value):
300 307 """Lock a property-value pair.
301 308
302 309 The value should be the JSON state of the property.
303 310
304 311 NOTE: This, in addition to the single lock for all state changes, is
305 312 flawed. In the future we may want to look into buffering state changes
306 313 back to the front-end."""
307 314 self._property_lock = (key, value)
308 315 try:
309 316 yield
310 317 finally:
311 318 self._property_lock = (None, None)
312 319
313 320 @contextmanager
314 321 def hold_sync(self):
315 322 """Hold syncing any state until the context manager is released"""
316 323 # We increment a value so that this can be nested. Syncing will happen when
317 324 # all levels have been released.
318 325 self._send_state_lock += 1
319 326 try:
320 327 yield
321 328 finally:
322 329 self._send_state_lock -=1
323 330 if self._send_state_lock == 0:
324 331 self.send_state(self._states_to_send)
325 332 self._states_to_send.clear()
326 333
327 334 def _should_send_property(self, key, value):
328 335 """Check the property lock (property_lock)"""
329 336 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
330 337 if (key == self._property_lock[0]
331 338 and to_json(value) == self._property_lock[1]):
332 339 return False
333 340 elif self._send_state_lock > 0:
334 341 self._states_to_send.add(key)
335 342 return False
336 343 else:
337 344 return True
338 345
339 346 # Event handlers
340 347 @_show_traceback
341 348 def _handle_msg(self, msg):
342 349 """Called when a msg is received from the front-end"""
343 350 data = msg['content']['data']
344 351 method = data['method']
345 352
346 353 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
347 354 if method == 'backbone':
348 355 if 'sync_data' in data:
349 356 sync_data = data['sync_data']
350 357 self.set_state(sync_data) # handles all methods
351 358
352 359 # Handle a state request.
353 360 elif method == 'request_state':
354 361 self.send_state()
355 362
356 363 # Handle a custom msg from the front-end.
357 364 elif method == 'custom':
358 365 if 'content' in data:
359 366 self._handle_custom_msg(data['content'])
360 367
361 368 # Catch remainder.
362 369 else:
363 370 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
364 371
365 372 def _handle_custom_msg(self, content):
366 373 """Called when a custom msg is received."""
367 374 self._msg_callbacks(self, content)
368 375
369 376 def _notify_trait(self, name, old_value, new_value):
370 377 """Called when a property has been changed."""
371 378 # Trigger default traitlet callback machinery. This allows any user
372 379 # registered validation to be processed prior to allowing the widget
373 380 # machinery to handle the state.
374 381 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
375 382
376 383 # Send the state after the user registered callbacks for trait changes
377 384 # have all fired (allows for user to validate values).
378 385 if self.comm is not None and name in self.keys:
379 386 # Make sure this isn't information that the front-end just sent us.
380 387 if self._should_send_property(name, new_value):
381 388 # Send new state to front-end
382 389 self.send_state(key=name)
383 390
384 391 def _handle_displayed(self, **kwargs):
385 392 """Called when a view has been displayed for this widget instance"""
386 393 self._display_callbacks(self, **kwargs)
387 394
388 395 def _trait_to_json(self, x):
389 396 """Convert a trait value to json
390 397
391 398 Traverse lists/tuples and dicts and serialize their values as well.
392 399 Replace any widgets with their model_id
393 400 """
394 401 if isinstance(x, dict):
395 402 return {k: self._trait_to_json(v) for k, v in x.items()}
396 403 elif isinstance(x, (list, tuple)):
397 404 return [self._trait_to_json(v) for v in x]
398 405 elif isinstance(x, Widget):
399 406 return "IPY_MODEL_" + x.model_id
400 407 else:
401 408 return x # Value must be JSON-able
402 409
403 410 def _trait_from_json(self, x):
404 411 """Convert json values to objects
405 412
406 413 Replace any strings representing valid model id values to Widget references.
407 414 """
408 415 if isinstance(x, dict):
409 416 return {k: self._trait_from_json(v) for k, v in x.items()}
410 417 elif isinstance(x, (list, tuple)):
411 418 return [self._trait_from_json(v) for v in x]
412 419 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
413 420 # we want to support having child widgets at any level in a hierarchy
414 421 # trusting that a widget UUID will not appear out in the wild
415 422 return Widget.widgets[x[10:]]
416 423 else:
417 424 return x
418 425
419 426 def _ipython_display_(self, **kwargs):
420 427 """Called when `IPython.display.display` is called on the widget."""
421 428 # Show view.
422 429 if self._view_name is not None:
423 430 self._send({"method": "display"})
424 431 self._handle_displayed(**kwargs)
425 432
426 433 def _send(self, msg):
427 434 """Sends a message to the model in the front-end."""
428 435 self.comm.send(msg)
429 436
430 437
431 438 class DOMWidget(Widget):
432 439 visible = Bool(True, allow_none=True, help="Whether the widget is visible. False collapses the empty space, while None preserves the empty space.", sync=True)
433 440 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
434 441 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
435 442
436 443 width = CUnicode(sync=True)
437 444 height = CUnicode(sync=True)
438 445 # A default padding of 2.5 px makes the widgets look nice when displayed inline.
439 446 padding = CUnicode(sync=True)
440 447 margin = CUnicode(sync=True)
441 448
442 449 color = Color(None, allow_none=True, sync=True)
443 450 background_color = Color(None, allow_none=True, sync=True)
444 451 border_color = Color(None, allow_none=True, sync=True)
445 452
446 453 border_width = CUnicode(sync=True)
447 454 border_radius = CUnicode(sync=True)
448 455 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
449 456 'none',
450 457 'hidden',
451 458 'dotted',
452 459 'dashed',
453 460 'solid',
454 461 'double',
455 462 'groove',
456 463 'ridge',
457 464 'inset',
458 465 'outset',
459 466 'initial',
460 467 'inherit', ''],
461 468 default_value='', sync=True)
462 469
463 470 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
464 471 'normal',
465 472 'italic',
466 473 'oblique',
467 474 'initial',
468 475 'inherit', ''],
469 476 default_value='', sync=True)
470 477 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
471 478 'normal',
472 479 'bold',
473 480 'bolder',
474 481 'lighter',
475 482 'initial',
476 483 'inherit', ''] + list(map(str, range(100,1000,100))),
477 484 default_value='', sync=True)
478 485 font_size = CUnicode(sync=True)
479 486 font_family = Unicode(sync=True)
480 487
481 488 def __init__(self, *pargs, **kwargs):
482 489 super(DOMWidget, self).__init__(*pargs, **kwargs)
483 490
484 491 def _validate_border(name, old, new):
485 492 if new is not None and new != '':
486 493 if name != 'border_width' and not self.border_width:
487 494 self.border_width = 1
488 495 if name != 'border_style' and self.border_style == '':
489 496 self.border_style = 'solid'
490 497 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
@@ -1,1565 +1,1617 b''
1 1 # encoding: utf-8
2 2 """Tests for IPython.utils.traitlets."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6 #
7 7 # Adapted from enthought.traits, Copyright (c) Enthought, Inc.,
8 8 # also under the terms of the Modified BSD License.
9 9
10 10 import pickle
11 11 import re
12 12 import sys
13 13 from unittest import TestCase
14 14
15 15 import nose.tools as nt
16 16 from nose import SkipTest
17 17
18 18 from IPython.utils.traitlets import (
19 19 HasTraits, MetaHasTraits, TraitType, Any, Bool, CBytes, Dict, Enum,
20 20 Int, Long, Integer, Float, Complex, Bytes, Unicode, TraitError,
21 21 Union, Undefined, Type, This, Instance, TCPAddress, List, Tuple,
22 22 ObjectName, DottedObjectName, CRegExp, link, directional_link,
23 23 EventfulList, EventfulDict, ForwardDeclaredType, ForwardDeclaredInstance,
24 24 )
25 25 from IPython.utils import py3compat
26 26 from IPython.testing.decorators import skipif
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Helper classes for testing
30 30 #-----------------------------------------------------------------------------
31 31
32 32
33 33 class HasTraitsStub(HasTraits):
34 34
35 35 def _notify_trait(self, name, old, new):
36 36 self._notify_name = name
37 37 self._notify_old = old
38 38 self._notify_new = new
39 39
40 40
41 41 #-----------------------------------------------------------------------------
42 42 # Test classes
43 43 #-----------------------------------------------------------------------------
44 44
45 45
46 46 class TestTraitType(TestCase):
47 47
48 48 def test_get_undefined(self):
49 49 class A(HasTraits):
50 50 a = TraitType
51 51 a = A()
52 52 self.assertEqual(a.a, Undefined)
53 53
54 54 def test_set(self):
55 55 class A(HasTraitsStub):
56 56 a = TraitType
57 57
58 58 a = A()
59 59 a.a = 10
60 60 self.assertEqual(a.a, 10)
61 61 self.assertEqual(a._notify_name, 'a')
62 62 self.assertEqual(a._notify_old, Undefined)
63 63 self.assertEqual(a._notify_new, 10)
64 64
65 65 def test_validate(self):
66 66 class MyTT(TraitType):
67 67 def validate(self, inst, value):
68 68 return -1
69 69 class A(HasTraitsStub):
70 70 tt = MyTT
71 71
72 72 a = A()
73 73 a.tt = 10
74 74 self.assertEqual(a.tt, -1)
75 75
76 76 def test_default_validate(self):
77 77 class MyIntTT(TraitType):
78 78 def validate(self, obj, value):
79 79 if isinstance(value, int):
80 80 return value
81 81 self.error(obj, value)
82 82 class A(HasTraits):
83 83 tt = MyIntTT(10)
84 84 a = A()
85 85 self.assertEqual(a.tt, 10)
86 86
87 87 # Defaults are validated when the HasTraits is instantiated
88 88 class B(HasTraits):
89 89 tt = MyIntTT('bad default')
90 90 self.assertRaises(TraitError, B)
91 91
92 92 def test_info(self):
93 93 class A(HasTraits):
94 94 tt = TraitType
95 95 a = A()
96 96 self.assertEqual(A.tt.info(), 'any value')
97 97
98 98 def test_error(self):
99 99 class A(HasTraits):
100 100 tt = TraitType
101 101 a = A()
102 102 self.assertRaises(TraitError, A.tt.error, a, 10)
103 103
104 104 def test_dynamic_initializer(self):
105 105 class A(HasTraits):
106 106 x = Int(10)
107 107 def _x_default(self):
108 108 return 11
109 109 class B(A):
110 110 x = Int(20)
111 111 class C(A):
112 112 def _x_default(self):
113 113 return 21
114 114
115 115 a = A()
116 116 self.assertEqual(a._trait_values, {})
117 117 self.assertEqual(list(a._trait_dyn_inits.keys()), ['x'])
118 118 self.assertEqual(a.x, 11)
119 119 self.assertEqual(a._trait_values, {'x': 11})
120 120 b = B()
121 121 self.assertEqual(b._trait_values, {'x': 20})
122 122 self.assertEqual(list(a._trait_dyn_inits.keys()), ['x'])
123 123 self.assertEqual(b.x, 20)
124 124 c = C()
125 125 self.assertEqual(c._trait_values, {})
126 126 self.assertEqual(list(a._trait_dyn_inits.keys()), ['x'])
127 127 self.assertEqual(c.x, 21)
128 128 self.assertEqual(c._trait_values, {'x': 21})
129 129 # Ensure that the base class remains unmolested when the _default
130 130 # initializer gets overridden in a subclass.
131 131 a = A()
132 132 c = C()
133 133 self.assertEqual(a._trait_values, {})
134 134 self.assertEqual(list(a._trait_dyn_inits.keys()), ['x'])
135 135 self.assertEqual(a.x, 11)
136 136 self.assertEqual(a._trait_values, {'x': 11})
137 137
138 138
139 139
140 140 class TestHasTraitsMeta(TestCase):
141 141
142 142 def test_metaclass(self):
143 143 self.assertEqual(type(HasTraits), MetaHasTraits)
144 144
145 145 class A(HasTraits):
146 146 a = Int
147 147
148 148 a = A()
149 149 self.assertEqual(type(a.__class__), MetaHasTraits)
150 150 self.assertEqual(a.a,0)
151 151 a.a = 10
152 152 self.assertEqual(a.a,10)
153 153
154 154 class B(HasTraits):
155 155 b = Int()
156 156
157 157 b = B()
158 158 self.assertEqual(b.b,0)
159 159 b.b = 10
160 160 self.assertEqual(b.b,10)
161 161
162 162 class C(HasTraits):
163 163 c = Int(30)
164 164
165 165 c = C()
166 166 self.assertEqual(c.c,30)
167 167 c.c = 10
168 168 self.assertEqual(c.c,10)
169 169
170 170 def test_this_class(self):
171 171 class A(HasTraits):
172 172 t = This()
173 173 tt = This()
174 174 class B(A):
175 175 tt = This()
176 176 ttt = This()
177 177 self.assertEqual(A.t.this_class, A)
178 178 self.assertEqual(B.t.this_class, A)
179 179 self.assertEqual(B.tt.this_class, B)
180 180 self.assertEqual(B.ttt.this_class, B)
181 181
182 182 class TestHasTraitsNotify(TestCase):
183 183
184 184 def setUp(self):
185 185 self._notify1 = []
186 186 self._notify2 = []
187 187
188 188 def notify1(self, name, old, new):
189 189 self._notify1.append((name, old, new))
190 190
191 191 def notify2(self, name, old, new):
192 192 self._notify2.append((name, old, new))
193 193
194 194 def test_notify_all(self):
195 195
196 196 class A(HasTraits):
197 197 a = Int
198 198 b = Float
199 199
200 200 a = A()
201 201 a.on_trait_change(self.notify1)
202 202 a.a = 0
203 203 self.assertEqual(len(self._notify1),0)
204 204 a.b = 0.0
205 205 self.assertEqual(len(self._notify1),0)
206 206 a.a = 10
207 207 self.assertTrue(('a',0,10) in self._notify1)
208 208 a.b = 10.0
209 209 self.assertTrue(('b',0.0,10.0) in self._notify1)
210 210 self.assertRaises(TraitError,setattr,a,'a','bad string')
211 211 self.assertRaises(TraitError,setattr,a,'b','bad string')
212 212 self._notify1 = []
213 213 a.on_trait_change(self.notify1,remove=True)
214 214 a.a = 20
215 215 a.b = 20.0
216 216 self.assertEqual(len(self._notify1),0)
217 217
218 218 def test_notify_one(self):
219 219
220 220 class A(HasTraits):
221 221 a = Int
222 222 b = Float
223 223
224 224 a = A()
225 225 a.on_trait_change(self.notify1, 'a')
226 226 a.a = 0
227 227 self.assertEqual(len(self._notify1),0)
228 228 a.a = 10
229 229 self.assertTrue(('a',0,10) in self._notify1)
230 230 self.assertRaises(TraitError,setattr,a,'a','bad string')
231 231
232 232 def test_subclass(self):
233 233
234 234 class A(HasTraits):
235 235 a = Int
236 236
237 237 class B(A):
238 238 b = Float
239 239
240 240 b = B()
241 241 self.assertEqual(b.a,0)
242 242 self.assertEqual(b.b,0.0)
243 243 b.a = 100
244 244 b.b = 100.0
245 245 self.assertEqual(b.a,100)
246 246 self.assertEqual(b.b,100.0)
247 247
248 248 def test_notify_subclass(self):
249 249
250 250 class A(HasTraits):
251 251 a = Int
252 252
253 253 class B(A):
254 254 b = Float
255 255
256 256 b = B()
257 257 b.on_trait_change(self.notify1, 'a')
258 258 b.on_trait_change(self.notify2, 'b')
259 259 b.a = 0
260 260 b.b = 0.0
261 261 self.assertEqual(len(self._notify1),0)
262 262 self.assertEqual(len(self._notify2),0)
263 263 b.a = 10
264 264 b.b = 10.0
265 265 self.assertTrue(('a',0,10) in self._notify1)
266 266 self.assertTrue(('b',0.0,10.0) in self._notify2)
267 267
268 268 def test_static_notify(self):
269 269
270 270 class A(HasTraits):
271 271 a = Int
272 272 _notify1 = []
273 273 def _a_changed(self, name, old, new):
274 274 self._notify1.append((name, old, new))
275 275
276 276 a = A()
277 277 a.a = 0
278 278 # This is broken!!!
279 279 self.assertEqual(len(a._notify1),0)
280 280 a.a = 10
281 281 self.assertTrue(('a',0,10) in a._notify1)
282 282
283 283 class B(A):
284 284 b = Float
285 285 _notify2 = []
286 286 def _b_changed(self, name, old, new):
287 287 self._notify2.append((name, old, new))
288 288
289 289 b = B()
290 290 b.a = 10
291 291 b.b = 10.0
292 292 self.assertTrue(('a',0,10) in b._notify1)
293 293 self.assertTrue(('b',0.0,10.0) in b._notify2)
294 294
295 295 def test_notify_args(self):
296 296
297 297 def callback0():
298 298 self.cb = ()
299 299 def callback1(name):
300 300 self.cb = (name,)
301 301 def callback2(name, new):
302 302 self.cb = (name, new)
303 303 def callback3(name, old, new):
304 304 self.cb = (name, old, new)
305 305
306 306 class A(HasTraits):
307 307 a = Int
308 308
309 309 a = A()
310 310 a.on_trait_change(callback0, 'a')
311 311 a.a = 10
312 312 self.assertEqual(self.cb,())
313 313 a.on_trait_change(callback0, 'a', remove=True)
314 314
315 315 a.on_trait_change(callback1, 'a')
316 316 a.a = 100
317 317 self.assertEqual(self.cb,('a',))
318 318 a.on_trait_change(callback1, 'a', remove=True)
319 319
320 320 a.on_trait_change(callback2, 'a')
321 321 a.a = 1000
322 322 self.assertEqual(self.cb,('a',1000))
323 323 a.on_trait_change(callback2, 'a', remove=True)
324 324
325 325 a.on_trait_change(callback3, 'a')
326 326 a.a = 10000
327 327 self.assertEqual(self.cb,('a',1000,10000))
328 328 a.on_trait_change(callback3, 'a', remove=True)
329 329
330 330 self.assertEqual(len(a._trait_notifiers['a']),0)
331 331
332 332 def test_notify_only_once(self):
333 333
334 334 class A(HasTraits):
335 335 listen_to = ['a']
336 336
337 337 a = Int(0)
338 338 b = 0
339 339
340 340 def __init__(self, **kwargs):
341 341 super(A, self).__init__(**kwargs)
342 342 self.on_trait_change(self.listener1, ['a'])
343 343
344 344 def listener1(self, name, old, new):
345 345 self.b += 1
346 346
347 347 class B(A):
348 348
349 349 c = 0
350 350 d = 0
351 351
352 352 def __init__(self, **kwargs):
353 353 super(B, self).__init__(**kwargs)
354 354 self.on_trait_change(self.listener2)
355 355
356 356 def listener2(self, name, old, new):
357 357 self.c += 1
358 358
359 359 def _a_changed(self, name, old, new):
360 360 self.d += 1
361 361
362 362 b = B()
363 363 b.a += 1
364 364 self.assertEqual(b.b, b.c)
365 365 self.assertEqual(b.b, b.d)
366 366 b.a += 1
367 367 self.assertEqual(b.b, b.c)
368 368 self.assertEqual(b.b, b.d)
369 369
370 370
371 371 class TestHasTraits(TestCase):
372 372
373 373 def test_trait_names(self):
374 374 class A(HasTraits):
375 375 i = Int
376 376 f = Float
377 377 a = A()
378 378 self.assertEqual(sorted(a.trait_names()),['f','i'])
379 379 self.assertEqual(sorted(A.class_trait_names()),['f','i'])
380 380
381 381 def test_trait_metadata(self):
382 382 class A(HasTraits):
383 383 i = Int(config_key='MY_VALUE')
384 384 a = A()
385 385 self.assertEqual(a.trait_metadata('i','config_key'), 'MY_VALUE')
386 386
387 387 def test_trait_metadata_default(self):
388 388 class A(HasTraits):
389 389 i = Int()
390 390 a = A()
391 391 self.assertEqual(a.trait_metadata('i', 'config_key'), None)
392 392 self.assertEqual(a.trait_metadata('i', 'config_key', 'default'), 'default')
393 393
394 394 def test_traits(self):
395 395 class A(HasTraits):
396 396 i = Int
397 397 f = Float
398 398 a = A()
399 399 self.assertEqual(a.traits(), dict(i=A.i, f=A.f))
400 400 self.assertEqual(A.class_traits(), dict(i=A.i, f=A.f))
401 401
402 402 def test_traits_metadata(self):
403 403 class A(HasTraits):
404 404 i = Int(config_key='VALUE1', other_thing='VALUE2')
405 405 f = Float(config_key='VALUE3', other_thing='VALUE2')
406 406 j = Int(0)
407 407 a = A()
408 408 self.assertEqual(a.traits(), dict(i=A.i, f=A.f, j=A.j))
409 409 traits = a.traits(config_key='VALUE1', other_thing='VALUE2')
410 410 self.assertEqual(traits, dict(i=A.i))
411 411
412 412 # This passes, but it shouldn't because I am replicating a bug in
413 413 # traits.
414 414 traits = a.traits(config_key=lambda v: True)
415 415 self.assertEqual(traits, dict(i=A.i, f=A.f, j=A.j))
416 416
417 417 def test_init(self):
418 418 class A(HasTraits):
419 419 i = Int()
420 420 x = Float()
421 421 a = A(i=1, x=10.0)
422 422 self.assertEqual(a.i, 1)
423 423 self.assertEqual(a.x, 10.0)
424 424
425 425 def test_positional_args(self):
426 426 class A(HasTraits):
427 427 i = Int(0)
428 428 def __init__(self, i):
429 429 super(A, self).__init__()
430 430 self.i = i
431 431
432 432 a = A(5)
433 433 self.assertEqual(a.i, 5)
434 434 # should raise TypeError if no positional arg given
435 435 self.assertRaises(TypeError, A)
436 436
437 437 #-----------------------------------------------------------------------------
438 438 # Tests for specific trait types
439 439 #-----------------------------------------------------------------------------
440 440
441 441
442 442 class TestType(TestCase):
443 443
444 444 def test_default(self):
445 445
446 446 class B(object): pass
447 447 class A(HasTraits):
448 448 klass = Type
449 449
450 450 a = A()
451 451 self.assertEqual(a.klass, None)
452 452
453 453 a.klass = B
454 454 self.assertEqual(a.klass, B)
455 455 self.assertRaises(TraitError, setattr, a, 'klass', 10)
456 456
457 457 def test_value(self):
458 458
459 459 class B(object): pass
460 460 class C(object): pass
461 461 class A(HasTraits):
462 462 klass = Type(B)
463 463
464 464 a = A()
465 465 self.assertEqual(a.klass, B)
466 466 self.assertRaises(TraitError, setattr, a, 'klass', C)
467 467 self.assertRaises(TraitError, setattr, a, 'klass', object)
468 468 a.klass = B
469 469
470 470 def test_allow_none(self):
471 471
472 472 class B(object): pass
473 473 class C(B): pass
474 474 class A(HasTraits):
475 475 klass = Type(B, allow_none=False)
476 476
477 477 a = A()
478 478 self.assertEqual(a.klass, B)
479 479 self.assertRaises(TraitError, setattr, a, 'klass', None)
480 480 a.klass = C
481 481 self.assertEqual(a.klass, C)
482 482
483 483 def test_validate_klass(self):
484 484
485 485 class A(HasTraits):
486 486 klass = Type('no strings allowed')
487 487
488 488 self.assertRaises(ImportError, A)
489 489
490 490 class A(HasTraits):
491 491 klass = Type('rub.adub.Duck')
492 492
493 493 self.assertRaises(ImportError, A)
494 494
495 495 def test_validate_default(self):
496 496
497 497 class B(object): pass
498 498 class A(HasTraits):
499 499 klass = Type('bad default', B)
500 500
501 501 self.assertRaises(ImportError, A)
502 502
503 503 class C(HasTraits):
504 504 klass = Type(None, B, allow_none=False)
505 505
506 506 self.assertRaises(TraitError, C)
507 507
508 508 def test_str_klass(self):
509 509
510 510 class A(HasTraits):
511 511 klass = Type('IPython.utils.ipstruct.Struct')
512 512
513 513 from IPython.utils.ipstruct import Struct
514 514 a = A()
515 515 a.klass = Struct
516 516 self.assertEqual(a.klass, Struct)
517 517
518 518 self.assertRaises(TraitError, setattr, a, 'klass', 10)
519 519
520 520 def test_set_str_klass(self):
521 521
522 522 class A(HasTraits):
523 523 klass = Type()
524 524
525 525 a = A(klass='IPython.utils.ipstruct.Struct')
526 526 from IPython.utils.ipstruct import Struct
527 527 self.assertEqual(a.klass, Struct)
528 528
529 529 class TestInstance(TestCase):
530 530
531 531 def test_basic(self):
532 532 class Foo(object): pass
533 533 class Bar(Foo): pass
534 534 class Bah(object): pass
535 535
536 536 class A(HasTraits):
537 537 inst = Instance(Foo)
538 538
539 539 a = A()
540 540 self.assertTrue(a.inst is None)
541 541 a.inst = Foo()
542 542 self.assertTrue(isinstance(a.inst, Foo))
543 543 a.inst = Bar()
544 544 self.assertTrue(isinstance(a.inst, Foo))
545 545 self.assertRaises(TraitError, setattr, a, 'inst', Foo)
546 546 self.assertRaises(TraitError, setattr, a, 'inst', Bar)
547 547 self.assertRaises(TraitError, setattr, a, 'inst', Bah())
548 548
549 549 def test_default_klass(self):
550 550 class Foo(object): pass
551 551 class Bar(Foo): pass
552 552 class Bah(object): pass
553 553
554 554 class FooInstance(Instance):
555 555 klass = Foo
556 556
557 557 class A(HasTraits):
558 558 inst = FooInstance()
559 559
560 560 a = A()
561 561 self.assertTrue(a.inst is None)
562 562 a.inst = Foo()
563 563 self.assertTrue(isinstance(a.inst, Foo))
564 564 a.inst = Bar()
565 565 self.assertTrue(isinstance(a.inst, Foo))
566 566 self.assertRaises(TraitError, setattr, a, 'inst', Foo)
567 567 self.assertRaises(TraitError, setattr, a, 'inst', Bar)
568 568 self.assertRaises(TraitError, setattr, a, 'inst', Bah())
569 569
570 570 def test_unique_default_value(self):
571 571 class Foo(object): pass
572 572 class A(HasTraits):
573 573 inst = Instance(Foo,(),{})
574 574
575 575 a = A()
576 576 b = A()
577 577 self.assertTrue(a.inst is not b.inst)
578 578
579 579 def test_args_kw(self):
580 580 class Foo(object):
581 581 def __init__(self, c): self.c = c
582 582 class Bar(object): pass
583 583 class Bah(object):
584 584 def __init__(self, c, d):
585 585 self.c = c; self.d = d
586 586
587 587 class A(HasTraits):
588 588 inst = Instance(Foo, (10,))
589 589 a = A()
590 590 self.assertEqual(a.inst.c, 10)
591 591
592 592 class B(HasTraits):
593 593 inst = Instance(Bah, args=(10,), kw=dict(d=20))
594 594 b = B()
595 595 self.assertEqual(b.inst.c, 10)
596 596 self.assertEqual(b.inst.d, 20)
597 597
598 598 class C(HasTraits):
599 599 inst = Instance(Foo)
600 600 c = C()
601 601 self.assertTrue(c.inst is None)
602 602
603 603 def test_bad_default(self):
604 604 class Foo(object): pass
605 605
606 606 class A(HasTraits):
607 607 inst = Instance(Foo, allow_none=False)
608 608
609 609 self.assertRaises(TraitError, A)
610 610
611 611 def test_instance(self):
612 612 class Foo(object): pass
613 613
614 614 def inner():
615 615 class A(HasTraits):
616 616 inst = Instance(Foo())
617 617
618 618 self.assertRaises(TraitError, inner)
619 619
620 620
621 621 class TestThis(TestCase):
622 622
623 623 def test_this_class(self):
624 624 class Foo(HasTraits):
625 625 this = This
626 626
627 627 f = Foo()
628 628 self.assertEqual(f.this, None)
629 629 g = Foo()
630 630 f.this = g
631 631 self.assertEqual(f.this, g)
632 632 self.assertRaises(TraitError, setattr, f, 'this', 10)
633 633
634 634 def test_this_inst(self):
635 635 class Foo(HasTraits):
636 636 this = This()
637 637
638 638 f = Foo()
639 639 f.this = Foo()
640 640 self.assertTrue(isinstance(f.this, Foo))
641 641
642 642 def test_subclass(self):
643 643 class Foo(HasTraits):
644 644 t = This()
645 645 class Bar(Foo):
646 646 pass
647 647 f = Foo()
648 648 b = Bar()
649 649 f.t = b
650 650 b.t = f
651 651 self.assertEqual(f.t, b)
652 652 self.assertEqual(b.t, f)
653 653
654 654 def test_subclass_override(self):
655 655 class Foo(HasTraits):
656 656 t = This()
657 657 class Bar(Foo):
658 658 t = This()
659 659 f = Foo()
660 660 b = Bar()
661 661 f.t = b
662 662 self.assertEqual(f.t, b)
663 663 self.assertRaises(TraitError, setattr, b, 't', f)
664 664
665 665 def test_this_in_container(self):
666 666
667 667 class Tree(HasTraits):
668 668 value = Unicode()
669 669 leaves = List(This())
670 670
671 671 tree = Tree(
672 672 value='foo',
673 673 leaves=[Tree('bar'), Tree('buzz')]
674 674 )
675 675
676 676 with self.assertRaises(TraitError):
677 677 tree.leaves = [1, 2]
678 678
679 679 class TraitTestBase(TestCase):
680 680 """A best testing class for basic trait types."""
681 681
682 682 def assign(self, value):
683 683 self.obj.value = value
684 684
685 685 def coerce(self, value):
686 686 return value
687 687
688 688 def test_good_values(self):
689 689 if hasattr(self, '_good_values'):
690 690 for value in self._good_values:
691 691 self.assign(value)
692 692 self.assertEqual(self.obj.value, self.coerce(value))
693 693
694 694 def test_bad_values(self):
695 695 if hasattr(self, '_bad_values'):
696 696 for value in self._bad_values:
697 697 try:
698 698 self.assertRaises(TraitError, self.assign, value)
699 699 except AssertionError:
700 700 assert False, value
701 701
702 702 def test_default_value(self):
703 703 if hasattr(self, '_default_value'):
704 704 self.assertEqual(self._default_value, self.obj.value)
705 705
706 706 def test_allow_none(self):
707 707 if (hasattr(self, '_bad_values') and hasattr(self, '_good_values') and
708 708 None in self._bad_values):
709 709 trait=self.obj.traits()['value']
710 710 try:
711 711 trait.allow_none = True
712 712 self._bad_values.remove(None)
713 713 #skip coerce. Allow None casts None to None.
714 714 self.assign(None)
715 715 self.assertEqual(self.obj.value,None)
716 716 self.test_good_values()
717 717 self.test_bad_values()
718 718 finally:
719 719 #tear down
720 720 trait.allow_none = False
721 721 self._bad_values.append(None)
722 722
723 723 def tearDown(self):
724 724 # restore default value after tests, if set
725 725 if hasattr(self, '_default_value'):
726 726 self.obj.value = self._default_value
727 727
728 728
729 729 class AnyTrait(HasTraits):
730 730
731 731 value = Any
732 732
733 733 class AnyTraitTest(TraitTestBase):
734 734
735 735 obj = AnyTrait()
736 736
737 737 _default_value = None
738 738 _good_values = [10.0, 'ten', u'ten', [10], {'ten': 10},(10,), None, 1j]
739 739 _bad_values = []
740 740
741 741 class UnionTrait(HasTraits):
742 742
743 743 value = Union([Type(), Bool()])
744 744
745 745 class UnionTraitTest(TraitTestBase):
746 746
747 747 obj = UnionTrait(value='IPython.utils.ipstruct.Struct')
748 748 _good_values = [int, float, True]
749 749 _bad_values = [[], (0,), 1j]
750 750
751 751 class OrTrait(HasTraits):
752 752
753 753 value = Bool() | Unicode()
754 754
755 755 class OrTraitTest(TraitTestBase):
756 756
757 757 obj = OrTrait()
758 758 _good_values = [True, False, 'ten']
759 759 _bad_values = [[], (0,), 1j]
760 760
761 761 class IntTrait(HasTraits):
762 762
763 763 value = Int(99)
764 764
765 765 class TestInt(TraitTestBase):
766 766
767 767 obj = IntTrait()
768 768 _default_value = 99
769 769 _good_values = [10, -10]
770 770 _bad_values = ['ten', u'ten', [10], {'ten': 10},(10,), None, 1j,
771 771 10.1, -10.1, '10L', '-10L', '10.1', '-10.1', u'10L',
772 772 u'-10L', u'10.1', u'-10.1', '10', '-10', u'10', u'-10']
773 773 if not py3compat.PY3:
774 774 _bad_values.extend([long(10), long(-10), 10*sys.maxint, -10*sys.maxint])
775 775
776 776
777 777 class LongTrait(HasTraits):
778 778
779 779 value = Long(99 if py3compat.PY3 else long(99))
780 780
781 781 class TestLong(TraitTestBase):
782 782
783 783 obj = LongTrait()
784 784
785 785 _default_value = 99 if py3compat.PY3 else long(99)
786 786 _good_values = [10, -10]
787 787 _bad_values = ['ten', u'ten', [10], {'ten': 10},(10,),
788 788 None, 1j, 10.1, -10.1, '10', '-10', '10L', '-10L', '10.1',
789 789 '-10.1', u'10', u'-10', u'10L', u'-10L', u'10.1',
790 790 u'-10.1']
791 791 if not py3compat.PY3:
792 792 # maxint undefined on py3, because int == long
793 793 _good_values.extend([long(10), long(-10), 10*sys.maxint, -10*sys.maxint])
794 794 _bad_values.extend([[long(10)], (long(10),)])
795 795
796 796 @skipif(py3compat.PY3, "not relevant on py3")
797 797 def test_cast_small(self):
798 798 """Long casts ints to long"""
799 799 self.obj.value = 10
800 800 self.assertEqual(type(self.obj.value), long)
801 801
802 802
803 803 class IntegerTrait(HasTraits):
804 804 value = Integer(1)
805 805
806 806 class TestInteger(TestLong):
807 807 obj = IntegerTrait()
808 808 _default_value = 1
809 809
810 810 def coerce(self, n):
811 811 return int(n)
812 812
813 813 @skipif(py3compat.PY3, "not relevant on py3")
814 814 def test_cast_small(self):
815 815 """Integer casts small longs to int"""
816 816 if py3compat.PY3:
817 817 raise SkipTest("not relevant on py3")
818 818
819 819 self.obj.value = long(100)
820 820 self.assertEqual(type(self.obj.value), int)
821 821
822 822
823 823 class FloatTrait(HasTraits):
824 824
825 825 value = Float(99.0)
826 826
827 827 class TestFloat(TraitTestBase):
828 828
829 829 obj = FloatTrait()
830 830
831 831 _default_value = 99.0
832 832 _good_values = [10, -10, 10.1, -10.1]
833 833 _bad_values = ['ten', u'ten', [10], {'ten': 10},(10,), None,
834 834 1j, '10', '-10', '10L', '-10L', '10.1', '-10.1', u'10',
835 835 u'-10', u'10L', u'-10L', u'10.1', u'-10.1']
836 836 if not py3compat.PY3:
837 837 _bad_values.extend([long(10), long(-10)])
838 838
839 839
840 840 class ComplexTrait(HasTraits):
841 841
842 842 value = Complex(99.0-99.0j)
843 843
844 844 class TestComplex(TraitTestBase):
845 845
846 846 obj = ComplexTrait()
847 847
848 848 _default_value = 99.0-99.0j
849 849 _good_values = [10, -10, 10.1, -10.1, 10j, 10+10j, 10-10j,
850 850 10.1j, 10.1+10.1j, 10.1-10.1j]
851 851 _bad_values = [u'10L', u'-10L', 'ten', [10], {'ten': 10},(10,), None]
852 852 if not py3compat.PY3:
853 853 _bad_values.extend([long(10), long(-10)])
854 854
855 855
856 856 class BytesTrait(HasTraits):
857 857
858 858 value = Bytes(b'string')
859 859
860 860 class TestBytes(TraitTestBase):
861 861
862 862 obj = BytesTrait()
863 863
864 864 _default_value = b'string'
865 865 _good_values = [b'10', b'-10', b'10L',
866 866 b'-10L', b'10.1', b'-10.1', b'string']
867 867 _bad_values = [10, -10, 10.1, -10.1, 1j, [10],
868 868 ['ten'],{'ten': 10},(10,), None, u'string']
869 869 if not py3compat.PY3:
870 870 _bad_values.extend([long(10), long(-10)])
871 871
872 872
873 873 class UnicodeTrait(HasTraits):
874 874
875 875 value = Unicode(u'unicode')
876 876
877 877 class TestUnicode(TraitTestBase):
878 878
879 879 obj = UnicodeTrait()
880 880
881 881 _default_value = u'unicode'
882 882 _good_values = ['10', '-10', '10L', '-10L', '10.1',
883 883 '-10.1', '', u'', 'string', u'string', u"€"]
884 884 _bad_values = [10, -10, 10.1, -10.1, 1j,
885 885 [10], ['ten'], [u'ten'], {'ten': 10},(10,), None]
886 886 if not py3compat.PY3:
887 887 _bad_values.extend([long(10), long(-10)])
888 888
889 889
890 890 class ObjectNameTrait(HasTraits):
891 891 value = ObjectName("abc")
892 892
893 893 class TestObjectName(TraitTestBase):
894 894 obj = ObjectNameTrait()
895 895
896 896 _default_value = "abc"
897 897 _good_values = ["a", "gh", "g9", "g_", "_G", u"a345_"]
898 898 _bad_values = [1, "", u"€", "9g", "!", "#abc", "aj@", "a.b", "a()", "a[0]",
899 899 None, object(), object]
900 900 if sys.version_info[0] < 3:
901 901 _bad_values.append(u"ΓΎ")
902 902 else:
903 903 _good_values.append(u"ΓΎ") # ΓΎ=1 is valid in Python 3 (PEP 3131).
904 904
905 905
906 906 class DottedObjectNameTrait(HasTraits):
907 907 value = DottedObjectName("a.b")
908 908
909 909 class TestDottedObjectName(TraitTestBase):
910 910 obj = DottedObjectNameTrait()
911 911
912 912 _default_value = "a.b"
913 913 _good_values = ["A", "y.t", "y765.__repr__", "os.path.join", u"os.path.join"]
914 914 _bad_values = [1, u"abc.€", "_.@", ".", ".abc", "abc.", ".abc.", None]
915 915 if sys.version_info[0] < 3:
916 916 _bad_values.append(u"t.ΓΎ")
917 917 else:
918 918 _good_values.append(u"t.ΓΎ")
919 919
920 920
921 921 class TCPAddressTrait(HasTraits):
922 922 value = TCPAddress()
923 923
924 924 class TestTCPAddress(TraitTestBase):
925 925
926 926 obj = TCPAddressTrait()
927 927
928 928 _default_value = ('127.0.0.1',0)
929 929 _good_values = [('localhost',0),('192.168.0.1',1000),('www.google.com',80)]
930 930 _bad_values = [(0,0),('localhost',10.0),('localhost',-1), None]
931 931
932 932 class ListTrait(HasTraits):
933 933
934 934 value = List(Int)
935 935
936 936 class TestList(TraitTestBase):
937 937
938 938 obj = ListTrait()
939 939
940 940 _default_value = []
941 941 _good_values = [[], [1], list(range(10)), (1,2)]
942 942 _bad_values = [10, [1,'a'], 'a']
943 943
944 944 def coerce(self, value):
945 945 if value is not None:
946 946 value = list(value)
947 947 return value
948 948
949 949 class Foo(object):
950 950 pass
951 951
952 952 class NoneInstanceListTrait(HasTraits):
953 953
954 954 value = List(Instance(Foo, allow_none=False))
955 955
956 956 class TestNoneInstanceList(TraitTestBase):
957 957
958 958 obj = NoneInstanceListTrait()
959 959
960 960 _default_value = []
961 961 _good_values = [[Foo(), Foo()], []]
962 962 _bad_values = [[None], [Foo(), None]]
963 963
964 964
965 965 class InstanceListTrait(HasTraits):
966 966
967 967 value = List(Instance(__name__+'.Foo'))
968 968
969 969 class TestInstanceList(TraitTestBase):
970 970
971 971 obj = InstanceListTrait()
972 972
973 973 def test_klass(self):
974 974 """Test that the instance klass is properly assigned."""
975 975 self.assertIs(self.obj.traits()['value']._trait.klass, Foo)
976 976
977 977 _default_value = []
978 978 _good_values = [[Foo(), Foo(), None], []]
979 979 _bad_values = [['1', 2,], '1', [Foo], None]
980 980
981 981 class UnionListTrait(HasTraits):
982 982
983 983 value = List(Int() | Bool())
984 984
985 985 class TestUnionListTrait(HasTraits):
986 986
987 987 obj = UnionListTrait()
988 988
989 989 _default_value = []
990 990 _good_values = [[True, 1], [False, True]]
991 991 _bad_values = [[1, 'True'], False]
992 992
993 993
994 994 class LenListTrait(HasTraits):
995 995
996 996 value = List(Int, [0], minlen=1, maxlen=2)
997 997
998 998 class TestLenList(TraitTestBase):
999 999
1000 1000 obj = LenListTrait()
1001 1001
1002 1002 _default_value = [0]
1003 1003 _good_values = [[1], [1,2], (1,2)]
1004 1004 _bad_values = [10, [1,'a'], 'a', [], list(range(3))]
1005 1005
1006 1006 def coerce(self, value):
1007 1007 if value is not None:
1008 1008 value = list(value)
1009 1009 return value
1010 1010
1011 1011 class TupleTrait(HasTraits):
1012 1012
1013 1013 value = Tuple(Int(allow_none=True))
1014 1014
1015 1015 class TestTupleTrait(TraitTestBase):
1016 1016
1017 1017 obj = TupleTrait()
1018 1018
1019 1019 _default_value = None
1020 1020 _good_values = [(1,), None, (0,), [1], (None,)]
1021 1021 _bad_values = [10, (1,2), ('a'), ()]
1022 1022
1023 1023 def coerce(self, value):
1024 1024 if value is not None:
1025 1025 value = tuple(value)
1026 1026 return value
1027 1027
1028 1028 def test_invalid_args(self):
1029 1029 self.assertRaises(TypeError, Tuple, 5)
1030 1030 self.assertRaises(TypeError, Tuple, default_value='hello')
1031 1031 t = Tuple(Int, CBytes, default_value=(1,5))
1032 1032
1033 1033 class LooseTupleTrait(HasTraits):
1034 1034
1035 1035 value = Tuple((1,2,3))
1036 1036
1037 1037 class TestLooseTupleTrait(TraitTestBase):
1038 1038
1039 1039 obj = LooseTupleTrait()
1040 1040
1041 1041 _default_value = (1,2,3)
1042 1042 _good_values = [(1,), None, [1], (0,), tuple(range(5)), tuple('hello'), ('a',5), ()]
1043 1043 _bad_values = [10, 'hello', {}]
1044 1044
1045 1045 def coerce(self, value):
1046 1046 if value is not None:
1047 1047 value = tuple(value)
1048 1048 return value
1049 1049
1050 1050 def test_invalid_args(self):
1051 1051 self.assertRaises(TypeError, Tuple, 5)
1052 1052 self.assertRaises(TypeError, Tuple, default_value='hello')
1053 1053 t = Tuple(Int, CBytes, default_value=(1,5))
1054 1054
1055 1055
1056 1056 class MultiTupleTrait(HasTraits):
1057 1057
1058 1058 value = Tuple(Int, Bytes, default_value=[99,b'bottles'])
1059 1059
1060 1060 class TestMultiTuple(TraitTestBase):
1061 1061
1062 1062 obj = MultiTupleTrait()
1063 1063
1064 1064 _default_value = (99,b'bottles')
1065 1065 _good_values = [(1,b'a'), (2,b'b')]
1066 1066 _bad_values = ((),10, b'a', (1,b'a',3), (b'a',1), (1, u'a'))
1067 1067
1068 1068 class CRegExpTrait(HasTraits):
1069 1069
1070 1070 value = CRegExp(r'')
1071 1071
1072 1072 class TestCRegExp(TraitTestBase):
1073 1073
1074 1074 def coerce(self, value):
1075 1075 return re.compile(value)
1076 1076
1077 1077 obj = CRegExpTrait()
1078 1078
1079 1079 _default_value = re.compile(r'')
1080 1080 _good_values = [r'\d+', re.compile(r'\d+')]
1081 1081 _bad_values = ['(', None, ()]
1082 1082
1083 1083 class DictTrait(HasTraits):
1084 1084 value = Dict()
1085 1085
1086 1086 def test_dict_assignment():
1087 1087 d = dict()
1088 1088 c = DictTrait()
1089 1089 c.value = d
1090 1090 d['a'] = 5
1091 1091 nt.assert_equal(d, c.value)
1092 1092 nt.assert_true(c.value is d)
1093 1093
1094 1094 class ValidatedDictTrait(HasTraits):
1095 1095
1096 1096 value = Dict(Unicode())
1097 1097
1098 1098 class TestInstanceDict(TraitTestBase):
1099 1099
1100 1100 obj = ValidatedDictTrait()
1101 1101
1102 1102 _default_value = {}
1103 1103 _good_values = [{'0': 'foo'}, {'1': 'bar'}]
1104 1104 _bad_values = [{'0': 0}, {'1': 1}]
1105 1105
1106 1106
1107 1107 def test_dict_default_value():
1108 1108 """Check that the `{}` default value of the Dict traitlet constructor is
1109 1109 actually copied."""
1110 1110
1111 1111 d1, d2 = Dict(), Dict()
1112 1112 nt.assert_false(d1.get_default_value() is d2.get_default_value())
1113 1113
1114 1114
1115 1115 class TestValidationHook(TestCase):
1116 1116
1117 1117 def test_parity_trait(self):
1118 1118 """Verify that the early validation hook is effective"""
1119 1119
1120 1120 class Parity(HasTraits):
1121 1121
1122 1122 value = Int(0)
1123 1123 parity = Enum(['odd', 'even'], default_value='even', allow_none=False)
1124 1124
1125 1125 def _value_validate(self, value, trait):
1126 1126 if self.parity == 'even' and value % 2:
1127 1127 raise TraitError('Expected an even number')
1128 1128 if self.parity == 'odd' and (value % 2 == 0):
1129 1129 raise TraitError('Expected an odd number')
1130 1130 return value
1131 1131
1132 1132 u = Parity()
1133 1133 u.parity = 'odd'
1134 1134 u.value = 1 # OK
1135 1135 with self.assertRaises(TraitError):
1136 1136 u.value = 2 # Trait Error
1137 1137
1138 1138 u.parity = 'even'
1139 1139 u.value = 2 # OK
1140 1140
1141 1141
1142 1142 class TestLink(TestCase):
1143 1143
1144 1144 def test_connect_same(self):
1145 1145 """Verify two traitlets of the same type can be linked together using link."""
1146 1146
1147 1147 # Create two simple classes with Int traitlets.
1148 1148 class A(HasTraits):
1149 1149 value = Int()
1150 1150 a = A(value=9)
1151 1151 b = A(value=8)
1152 1152
1153 1153 # Conenct the two classes.
1154 1154 c = link((a, 'value'), (b, 'value'))
1155 1155
1156 1156 # Make sure the values are the same at the point of linking.
1157 1157 self.assertEqual(a.value, b.value)
1158 1158
1159 1159 # Change one of the values to make sure they stay in sync.
1160 1160 a.value = 5
1161 1161 self.assertEqual(a.value, b.value)
1162 1162 b.value = 6
1163 1163 self.assertEqual(a.value, b.value)
1164 1164
1165 1165 def test_link_different(self):
1166 1166 """Verify two traitlets of different types can be linked together using link."""
1167 1167
1168 1168 # Create two simple classes with Int traitlets.
1169 1169 class A(HasTraits):
1170 1170 value = Int()
1171 1171 class B(HasTraits):
1172 1172 count = Int()
1173 1173 a = A(value=9)
1174 1174 b = B(count=8)
1175 1175
1176 1176 # Conenct the two classes.
1177 1177 c = link((a, 'value'), (b, 'count'))
1178 1178
1179 1179 # Make sure the values are the same at the point of linking.
1180 1180 self.assertEqual(a.value, b.count)
1181 1181
1182 1182 # Change one of the values to make sure they stay in sync.
1183 1183 a.value = 5
1184 1184 self.assertEqual(a.value, b.count)
1185 1185 b.count = 4
1186 1186 self.assertEqual(a.value, b.count)
1187 1187
1188 1188 def test_unlink(self):
1189 1189 """Verify two linked traitlets can be unlinked."""
1190 1190
1191 1191 # Create two simple classes with Int traitlets.
1192 1192 class A(HasTraits):
1193 1193 value = Int()
1194 1194 a = A(value=9)
1195 1195 b = A(value=8)
1196 1196
1197 1197 # Connect the two classes.
1198 1198 c = link((a, 'value'), (b, 'value'))
1199 1199 a.value = 4
1200 1200 c.unlink()
1201 1201
1202 1202 # Change one of the values to make sure they don't stay in sync.
1203 1203 a.value = 5
1204 1204 self.assertNotEqual(a.value, b.value)
1205 1205
1206 1206 def test_callbacks(self):
1207 1207 """Verify two linked traitlets have their callbacks called once."""
1208 1208
1209 1209 # Create two simple classes with Int traitlets.
1210 1210 class A(HasTraits):
1211 1211 value = Int()
1212 1212 class B(HasTraits):
1213 1213 count = Int()
1214 1214 a = A(value=9)
1215 1215 b = B(count=8)
1216 1216
1217 1217 # Register callbacks that count.
1218 1218 callback_count = []
1219 1219 def a_callback(name, old, new):
1220 1220 callback_count.append('a')
1221 1221 a.on_trait_change(a_callback, 'value')
1222 1222 def b_callback(name, old, new):
1223 1223 callback_count.append('b')
1224 1224 b.on_trait_change(b_callback, 'count')
1225 1225
1226 1226 # Connect the two classes.
1227 1227 c = link((a, 'value'), (b, 'count'))
1228 1228
1229 1229 # Make sure b's count was set to a's value once.
1230 1230 self.assertEqual(''.join(callback_count), 'b')
1231 1231 del callback_count[:]
1232 1232
1233 1233 # Make sure a's value was set to b's count once.
1234 1234 b.count = 5
1235 1235 self.assertEqual(''.join(callback_count), 'ba')
1236 1236 del callback_count[:]
1237 1237
1238 1238 # Make sure b's count was set to a's value once.
1239 1239 a.value = 4
1240 1240 self.assertEqual(''.join(callback_count), 'ab')
1241 1241 del callback_count[:]
1242 1242
1243 1243 class TestDirectionalLink(TestCase):
1244 1244 def test_connect_same(self):
1245 1245 """Verify two traitlets of the same type can be linked together using directional_link."""
1246 1246
1247 1247 # Create two simple classes with Int traitlets.
1248 1248 class A(HasTraits):
1249 1249 value = Int()
1250 1250 a = A(value=9)
1251 1251 b = A(value=8)
1252 1252
1253 1253 # Conenct the two classes.
1254 1254 c = directional_link((a, 'value'), (b, 'value'))
1255 1255
1256 1256 # Make sure the values are the same at the point of linking.
1257 1257 self.assertEqual(a.value, b.value)
1258 1258
1259 1259 # Change one the value of the source and check that it synchronizes the target.
1260 1260 a.value = 5
1261 1261 self.assertEqual(b.value, 5)
1262 1262 # Change one the value of the target and check that it has no impact on the source
1263 1263 b.value = 6
1264 1264 self.assertEqual(a.value, 5)
1265 1265
1266 1266 def test_link_different(self):
1267 1267 """Verify two traitlets of different types can be linked together using link."""
1268 1268
1269 1269 # Create two simple classes with Int traitlets.
1270 1270 class A(HasTraits):
1271 1271 value = Int()
1272 1272 class B(HasTraits):
1273 1273 count = Int()
1274 1274 a = A(value=9)
1275 1275 b = B(count=8)
1276 1276
1277 1277 # Conenct the two classes.
1278 1278 c = directional_link((a, 'value'), (b, 'count'))
1279 1279
1280 1280 # Make sure the values are the same at the point of linking.
1281 1281 self.assertEqual(a.value, b.count)
1282 1282
1283 1283 # Change one the value of the source and check that it synchronizes the target.
1284 1284 a.value = 5
1285 1285 self.assertEqual(b.count, 5)
1286 1286 # Change one the value of the target and check that it has no impact on the source
1287 1287 b.value = 6
1288 1288 self.assertEqual(a.value, 5)
1289 1289
1290 1290 def test_unlink(self):
1291 1291 """Verify two linked traitlets can be unlinked."""
1292 1292
1293 1293 # Create two simple classes with Int traitlets.
1294 1294 class A(HasTraits):
1295 1295 value = Int()
1296 1296 a = A(value=9)
1297 1297 b = A(value=8)
1298 1298
1299 1299 # Connect the two classes.
1300 1300 c = directional_link((a, 'value'), (b, 'value'))
1301 1301 a.value = 4
1302 1302 c.unlink()
1303 1303
1304 1304 # Change one of the values to make sure they don't stay in sync.
1305 1305 a.value = 5
1306 1306 self.assertNotEqual(a.value, b.value)
1307 1307
1308 1308 class Pickleable(HasTraits):
1309 1309 i = Int()
1310 1310 j = Int()
1311 1311
1312 1312 def _i_default(self):
1313 1313 return 1
1314 1314
1315 1315 def _i_changed(self, name, old, new):
1316 1316 self.j = new
1317 1317
1318 1318 def test_pickle_hastraits():
1319 1319 c = Pickleable()
1320 1320 for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
1321 1321 p = pickle.dumps(c, protocol)
1322 1322 c2 = pickle.loads(p)
1323 1323 nt.assert_equal(c2.i, c.i)
1324 1324 nt.assert_equal(c2.j, c.j)
1325 1325
1326 1326 c.i = 5
1327 1327 for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
1328 1328 p = pickle.dumps(c, protocol)
1329 1329 c2 = pickle.loads(p)
1330 1330 nt.assert_equal(c2.i, c.i)
1331 1331 nt.assert_equal(c2.j, c.j)
1332 1332
1333 1333
1334 1334 def test_hold_trait_notifications():
1335 1335 changes = []
1336 1336 class Test(HasTraits):
1337 1337 a = Integer(0)
1338 1338 def _a_changed(self, name, old, new):
1339 1339 changes.append((old, new))
1340 1340
1341 1341 t = Test()
1342 1342 with t.hold_trait_notifications():
1343 1343 with t.hold_trait_notifications():
1344 1344 t.a = 1
1345 1345 nt.assert_equal(t.a, 1)
1346 1346 nt.assert_equal(changes, [])
1347 1347 t.a = 2
1348 1348 nt.assert_equal(t.a, 2)
1349 1349 with t.hold_trait_notifications():
1350 1350 t.a = 3
1351 1351 nt.assert_equal(t.a, 3)
1352 1352 nt.assert_equal(changes, [])
1353 1353 t.a = 4
1354 1354 nt.assert_equal(t.a, 4)
1355 1355 nt.assert_equal(changes, [])
1356 1356 t.a = 4
1357 1357 nt.assert_equal(t.a, 4)
1358 1358 nt.assert_equal(changes, [])
1359 1359 nt.assert_equal(changes, [(0,1), (1,2), (2,3), (3,4)])
1360 1360
1361 1361
1362 1362 class OrderTraits(HasTraits):
1363 1363 notified = Dict()
1364 1364
1365 1365 a = Unicode()
1366 1366 b = Unicode()
1367 1367 c = Unicode()
1368 1368 d = Unicode()
1369 1369 e = Unicode()
1370 1370 f = Unicode()
1371 1371 g = Unicode()
1372 1372 h = Unicode()
1373 1373 i = Unicode()
1374 1374 j = Unicode()
1375 1375 k = Unicode()
1376 1376 l = Unicode()
1377 1377
1378 1378 def _notify(self, name, old, new):
1379 1379 """check the value of all traits when each trait change is triggered
1380 1380
1381 1381 This verifies that the values are not sensitive
1382 1382 to dict ordering when loaded from kwargs
1383 1383 """
1384 1384 # check the value of the other traits
1385 1385 # when a given trait change notification fires
1386 1386 self.notified[name] = {
1387 1387 c: getattr(self, c) for c in 'abcdefghijkl'
1388 1388 }
1389 1389
1390 1390 def __init__(self, **kwargs):
1391 1391 self.on_trait_change(self._notify)
1392 1392 super(OrderTraits, self).__init__(**kwargs)
1393 1393
1394 1394 def test_notification_order():
1395 1395 d = {c:c for c in 'abcdefghijkl'}
1396 1396 obj = OrderTraits()
1397 1397 nt.assert_equal(obj.notified, {})
1398 1398 obj = OrderTraits(**d)
1399 1399 notifications = {
1400 1400 c: d for c in 'abcdefghijkl'
1401 1401 }
1402 1402 nt.assert_equal(obj.notified, notifications)
1403 1403
1404 1404
1405 1405 class TestEventful(TestCase):
1406 1406
1407 1407 def test_list(self):
1408 1408 """Does the EventfulList work?"""
1409 1409 event_cache = []
1410 1410
1411 1411 class A(HasTraits):
1412 1412 x = EventfulList([c for c in 'abc'])
1413 1413 a = A()
1414 1414 a.x.on_events(lambda i, x: event_cache.append('insert'), \
1415 1415 lambda i, x: event_cache.append('set'), \
1416 1416 lambda i: event_cache.append('del'), \
1417 1417 lambda: event_cache.append('reverse'), \
1418 1418 lambda *p, **k: event_cache.append('sort'))
1419 1419
1420 1420 a.x.remove('c')
1421 1421 # ab
1422 1422 a.x.insert(0, 'z')
1423 1423 # zab
1424 1424 del a.x[1]
1425 1425 # zb
1426 1426 a.x.reverse()
1427 1427 # bz
1428 1428 a.x[1] = 'o'
1429 1429 # bo
1430 1430 a.x.append('a')
1431 1431 # boa
1432 1432 a.x.sort()
1433 1433 # abo
1434 1434
1435 1435 # Were the correct events captured?
1436 1436 self.assertEqual(event_cache, ['del', 'insert', 'del', 'reverse', 'set', 'set', 'sort'])
1437 1437
1438 1438 # Is the output correct?
1439 1439 self.assertEqual(a.x, [c for c in 'abo'])
1440 1440
1441 1441 def test_dict(self):
1442 1442 """Does the EventfulDict work?"""
1443 1443 event_cache = []
1444 1444
1445 1445 class A(HasTraits):
1446 1446 x = EventfulDict({c: c for c in 'abc'})
1447 1447 a = A()
1448 1448 a.x.on_events(lambda k, v: event_cache.append('add'), \
1449 1449 lambda k, v: event_cache.append('set'), \
1450 1450 lambda k: event_cache.append('del'))
1451 1451
1452 1452 del a.x['c']
1453 1453 # ab
1454 1454 a.x['z'] = 1
1455 1455 # abz
1456 1456 a.x['z'] = 'z'
1457 1457 # abz
1458 1458 a.x.pop('a')
1459 1459 # bz
1460 1460
1461 1461 # Were the correct events captured?
1462 1462 self.assertEqual(event_cache, ['del', 'add', 'set', 'del'])
1463 1463
1464 1464 # Is the output correct?
1465 1465 self.assertEqual(a.x, {c: c for c in 'bz'})
1466 1466
1467 1467 ###
1468 1468 # Traits for Forward Declaration Tests
1469 1469 ###
1470 1470 class ForwardDeclaredInstanceTrait(HasTraits):
1471 1471
1472 1472 value = ForwardDeclaredInstance('ForwardDeclaredBar')
1473 1473
1474 1474 class ForwardDeclaredTypeTrait(HasTraits):
1475 1475
1476 1476 value = ForwardDeclaredType('ForwardDeclaredBar')
1477 1477
1478 1478 class ForwardDeclaredInstanceListTrait(HasTraits):
1479 1479
1480 1480 value = List(ForwardDeclaredInstance('ForwardDeclaredBar'))
1481 1481
1482 1482 class ForwardDeclaredTypeListTrait(HasTraits):
1483 1483
1484 1484 value = List(ForwardDeclaredType('ForwardDeclaredBar'))
1485 1485 ###
1486 1486 # End Traits for Forward Declaration Tests
1487 1487 ###
1488 1488
1489 1489 ###
1490 1490 # Classes for Forward Declaration Tests
1491 1491 ###
1492 1492 class ForwardDeclaredBar(object):
1493 1493 pass
1494 1494
1495 1495 class ForwardDeclaredBarSub(ForwardDeclaredBar):
1496 1496 pass
1497 1497 ###
1498 1498 # End Classes for Forward Declaration Tests
1499 1499 ###
1500 1500
1501 1501 ###
1502 1502 # Forward Declaration Tests
1503 1503 ###
1504 1504 class TestForwardDeclaredInstanceTrait(TraitTestBase):
1505 1505
1506 1506 obj = ForwardDeclaredInstanceTrait()
1507 1507 _default_value = None
1508 1508 _good_values = [None, ForwardDeclaredBar(), ForwardDeclaredBarSub()]
1509 1509 _bad_values = ['foo', 3, ForwardDeclaredBar, ForwardDeclaredBarSub]
1510 1510
1511 1511 class TestForwardDeclaredTypeTrait(TraitTestBase):
1512 1512
1513 1513 obj = ForwardDeclaredTypeTrait()
1514 1514 _default_value = None
1515 1515 _good_values = [None, ForwardDeclaredBar, ForwardDeclaredBarSub]
1516 1516 _bad_values = ['foo', 3, ForwardDeclaredBar(), ForwardDeclaredBarSub()]
1517 1517
1518 1518 class TestForwardDeclaredInstanceList(TraitTestBase):
1519 1519
1520 1520 obj = ForwardDeclaredInstanceListTrait()
1521 1521
1522 1522 def test_klass(self):
1523 1523 """Test that the instance klass is properly assigned."""
1524 1524 self.assertIs(self.obj.traits()['value']._trait.klass, ForwardDeclaredBar)
1525 1525
1526 1526 _default_value = []
1527 1527 _good_values = [
1528 1528 [ForwardDeclaredBar(), ForwardDeclaredBarSub(), None],
1529 1529 [None],
1530 1530 [],
1531 1531 ]
1532 1532 _bad_values = [
1533 1533 ForwardDeclaredBar(),
1534 1534 [ForwardDeclaredBar(), 3],
1535 1535 '1',
1536 1536 # Note that this is the type, not an instance.
1537 1537 [ForwardDeclaredBar],
1538 1538 None,
1539 1539 ]
1540 1540
1541 1541 class TestForwardDeclaredTypeList(TraitTestBase):
1542 1542
1543 1543 obj = ForwardDeclaredTypeListTrait()
1544 1544
1545 1545 def test_klass(self):
1546 1546 """Test that the instance klass is properly assigned."""
1547 1547 self.assertIs(self.obj.traits()['value']._trait.klass, ForwardDeclaredBar)
1548 1548
1549 1549 _default_value = []
1550 1550 _good_values = [
1551 1551 [ForwardDeclaredBar, ForwardDeclaredBarSub, None],
1552 1552 [],
1553 1553 [None],
1554 1554 ]
1555 1555 _bad_values = [
1556 1556 ForwardDeclaredBar,
1557 1557 [ForwardDeclaredBar, 3],
1558 1558 '1',
1559 1559 # Note that this is an instance, not the type.
1560 1560 [ForwardDeclaredBar()],
1561 1561 None,
1562 1562 ]
1563 1563 ###
1564 1564 # End Forward Declaration Tests
1565 1565 ###
1566
1567 class TestDynamicTraits(TestCase):
1568
1569 def setUp(self):
1570 self._notify1 = []
1571
1572 def notify1(self, name, old, new):
1573 self._notify1.append((name, old, new))
1574
1575 def test_notify_all(self):
1576
1577 class A(HasTraits):
1578 pass
1579
1580 a = A()
1581 self.assertTrue(not hasattr(a, 'x'))
1582 self.assertTrue(not hasattr(a, 'y'))
1583
1584 # Dynamically add trait x.
1585 a.add_trait('x', Int())
1586 self.assertTrue(hasattr(a, 'x'))
1587 self.assertTrue(isinstance(a, (A, )))
1588
1589 # Dynamically add trait y.
1590 a.add_trait('y', Float())
1591 self.assertTrue(hasattr(a, 'y'))
1592 self.assertTrue(isinstance(a, (A, )))
1593 self.assertEqual(a.__class__.__name__, A.__name__)
1594
1595 # Create a new instance and verify that x and y
1596 # aren't defined.
1597 b = A()
1598 self.assertTrue(not hasattr(b, 'x'))
1599 self.assertTrue(not hasattr(b, 'y'))
1600
1601 # Verify that notification works like normal.
1602 a.on_trait_change(self.notify1)
1603 a.x = 0
1604 self.assertEqual(len(self._notify1), 0)
1605 a.y = 0.0
1606 self.assertEqual(len(self._notify1), 0)
1607 a.x = 10
1608 self.assertTrue(('x', 0, 10) in self._notify1)
1609 a.y = 10.0
1610 self.assertTrue(('y', 0.0, 10.0) in self._notify1)
1611 self.assertRaises(TraitError, setattr, a, 'x', 'bad string')
1612 self.assertRaises(TraitError, setattr, a, 'y', 'bad string')
1613 self._notify1 = []
1614 a.on_trait_change(self.notify1, remove=True)
1615 a.x = 20
1616 a.y = 20.0
1617 self.assertEqual(len(self._notify1), 0)
@@ -1,1834 +1,1840 b''
1 1 # encoding: utf-8
2 2 """
3 3 A lightweight Traits like module.
4 4
5 5 This is designed to provide a lightweight, simple, pure Python version of
6 6 many of the capabilities of enthought.traits. This includes:
7 7
8 8 * Validation
9 9 * Type specification with defaults
10 10 * Static and dynamic notification
11 11 * Basic predefined types
12 12 * An API that is similar to enthought.traits
13 13
14 14 We don't support:
15 15
16 16 * Delegation
17 17 * Automatic GUI generation
18 18 * A full set of trait types. Most importantly, we don't provide container
19 19 traits (list, dict, tuple) that can trigger notifications if their
20 20 contents change.
21 21 * API compatibility with enthought.traits
22 22
23 23 There are also some important difference in our design:
24 24
25 25 * enthought.traits does not validate default values. We do.
26 26
27 27 We choose to create this module because we need these capabilities, but
28 28 we need them to be pure Python so they work in all Python implementations,
29 29 including Jython and IronPython.
30 30
31 31 Inheritance diagram:
32 32
33 33 .. inheritance-diagram:: IPython.utils.traitlets
34 34 :parts: 3
35 35 """
36 36
37 37 # Copyright (c) IPython Development Team.
38 38 # Distributed under the terms of the Modified BSD License.
39 39 #
40 40 # Adapted from enthought.traits, Copyright (c) Enthought, Inc.,
41 41 # also under the terms of the Modified BSD License.
42 42
43 43 import contextlib
44 44 import inspect
45 45 import re
46 46 import sys
47 47 import types
48 48 from types import FunctionType
49 49 try:
50 50 from types import ClassType, InstanceType
51 51 ClassTypes = (ClassType, type)
52 52 except:
53 53 ClassTypes = (type,)
54 54 from warnings import warn
55 55
56 56 from .getargspec import getargspec
57 57 from .importstring import import_item
58 58 from IPython.utils import py3compat
59 59 from IPython.utils import eventful
60 60 from IPython.utils.py3compat import iteritems, string_types
61 61 from IPython.testing.skipdoctest import skip_doctest
62 62
63 63 SequenceTypes = (list, tuple, set, frozenset)
64 64
65 65 #-----------------------------------------------------------------------------
66 66 # Basic classes
67 67 #-----------------------------------------------------------------------------
68 68
69 69
70 70 class NoDefaultSpecified ( object ): pass
71 71 NoDefaultSpecified = NoDefaultSpecified()
72 72
73 73
74 74 class Undefined ( object ): pass
75 75 Undefined = Undefined()
76 76
77 77 class TraitError(Exception):
78 78 pass
79 79
80 80 #-----------------------------------------------------------------------------
81 81 # Utilities
82 82 #-----------------------------------------------------------------------------
83 83
84 84
85 85 def class_of ( object ):
86 86 """ Returns a string containing the class name of an object with the
87 87 correct indefinite article ('a' or 'an') preceding it (e.g., 'an Image',
88 88 'a PlotValue').
89 89 """
90 90 if isinstance( object, py3compat.string_types ):
91 91 return add_article( object )
92 92
93 93 return add_article( object.__class__.__name__ )
94 94
95 95
96 96 def add_article ( name ):
97 97 """ Returns a string containing the correct indefinite article ('a' or 'an')
98 98 prefixed to the specified string.
99 99 """
100 100 if name[:1].lower() in 'aeiou':
101 101 return 'an ' + name
102 102
103 103 return 'a ' + name
104 104
105 105
106 106 def repr_type(obj):
107 107 """ Return a string representation of a value and its type for readable
108 108 error messages.
109 109 """
110 110 the_type = type(obj)
111 111 if (not py3compat.PY3) and the_type is InstanceType:
112 112 # Old-style class.
113 113 the_type = obj.__class__
114 114 msg = '%r %r' % (obj, the_type)
115 115 return msg
116 116
117 117
118 118 def is_trait(t):
119 119 """ Returns whether the given value is an instance or subclass of TraitType.
120 120 """
121 121 return (isinstance(t, TraitType) or
122 122 (isinstance(t, type) and issubclass(t, TraitType)))
123 123
124 124
125 125 def parse_notifier_name(name):
126 126 """Convert the name argument to a list of names.
127 127
128 128 Examples
129 129 --------
130 130
131 131 >>> parse_notifier_name('a')
132 132 ['a']
133 133 >>> parse_notifier_name(['a','b'])
134 134 ['a', 'b']
135 135 >>> parse_notifier_name(None)
136 136 ['anytrait']
137 137 """
138 138 if isinstance(name, string_types):
139 139 return [name]
140 140 elif name is None:
141 141 return ['anytrait']
142 142 elif isinstance(name, (list, tuple)):
143 143 for n in name:
144 144 assert isinstance(n, string_types), "names must be strings"
145 145 return name
146 146
147 147
148 148 class _SimpleTest:
149 149 def __init__ ( self, value ): self.value = value
150 150 def __call__ ( self, test ):
151 151 return test == self.value
152 152 def __repr__(self):
153 153 return "<SimpleTest(%r)" % self.value
154 154 def __str__(self):
155 155 return self.__repr__()
156 156
157 157
158 158 def getmembers(object, predicate=None):
159 159 """A safe version of inspect.getmembers that handles missing attributes.
160 160
161 161 This is useful when there are descriptor based attributes that for
162 162 some reason raise AttributeError even though they exist. This happens
163 163 in zope.inteface with the __provides__ attribute.
164 164 """
165 165 results = []
166 166 for key in dir(object):
167 167 try:
168 168 value = getattr(object, key)
169 169 except AttributeError:
170 170 pass
171 171 else:
172 172 if not predicate or predicate(value):
173 173 results.append((key, value))
174 174 results.sort()
175 175 return results
176 176
177 177 def _validate_link(*tuples):
178 178 """Validate arguments for traitlet link functions"""
179 179 for t in tuples:
180 180 if not len(t) == 2:
181 181 raise TypeError("Each linked traitlet must be specified as (HasTraits, 'trait_name'), not %r" % t)
182 182 obj, trait_name = t
183 183 if not isinstance(obj, HasTraits):
184 184 raise TypeError("Each object must be HasTraits, not %r" % type(obj))
185 185 if not trait_name in obj.traits():
186 186 raise TypeError("%r has no trait %r" % (obj, trait_name))
187 187
188 188 @skip_doctest
189 189 class link(object):
190 190 """Link traits from different objects together so they remain in sync.
191 191
192 192 Parameters
193 193 ----------
194 194 *args : pairs of objects/attributes
195 195
196 196 Examples
197 197 --------
198 198
199 199 >>> c = link((obj1, 'value'), (obj2, 'value'), (obj3, 'value'))
200 200 >>> obj1.value = 5 # updates other objects as well
201 201 """
202 202 updating = False
203 203 def __init__(self, *args):
204 204 if len(args) < 2:
205 205 raise TypeError('At least two traitlets must be provided.')
206 206 _validate_link(*args)
207 207
208 208 self.objects = {}
209 209
210 210 initial = getattr(args[0][0], args[0][1])
211 211 for obj, attr in args:
212 212 setattr(obj, attr, initial)
213 213
214 214 callback = self._make_closure(obj, attr)
215 215 obj.on_trait_change(callback, attr)
216 216 self.objects[(obj, attr)] = callback
217 217
218 218 @contextlib.contextmanager
219 219 def _busy_updating(self):
220 220 self.updating = True
221 221 try:
222 222 yield
223 223 finally:
224 224 self.updating = False
225 225
226 226 def _make_closure(self, sending_obj, sending_attr):
227 227 def update(name, old, new):
228 228 self._update(sending_obj, sending_attr, new)
229 229 return update
230 230
231 231 def _update(self, sending_obj, sending_attr, new):
232 232 if self.updating:
233 233 return
234 234 with self._busy_updating():
235 235 for obj, attr in self.objects.keys():
236 236 setattr(obj, attr, new)
237 237
238 238 def unlink(self):
239 239 for key, callback in self.objects.items():
240 240 (obj, attr) = key
241 241 obj.on_trait_change(callback, attr, remove=True)
242 242
243 243 @skip_doctest
244 244 class directional_link(object):
245 245 """Link the trait of a source object with traits of target objects.
246 246
247 247 Parameters
248 248 ----------
249 249 source : pair of object, name
250 250 targets : pairs of objects/attributes
251 251
252 252 Examples
253 253 --------
254 254
255 255 >>> c = directional_link((src, 'value'), (tgt1, 'value'), (tgt2, 'value'))
256 256 >>> src.value = 5 # updates target objects
257 257 >>> tgt1.value = 6 # does not update other objects
258 258 """
259 259 updating = False
260 260
261 261 def __init__(self, source, *targets):
262 262 if len(targets) < 1:
263 263 raise TypeError('At least two traitlets must be provided.')
264 264 _validate_link(source, *targets)
265 265 self.source = source
266 266 self.targets = targets
267 267
268 268 # Update current value
269 269 src_attr_value = getattr(source[0], source[1])
270 270 for obj, attr in targets:
271 271 setattr(obj, attr, src_attr_value)
272 272
273 273 # Wire
274 274 self.source[0].on_trait_change(self._update, self.source[1])
275 275
276 276 @contextlib.contextmanager
277 277 def _busy_updating(self):
278 278 self.updating = True
279 279 try:
280 280 yield
281 281 finally:
282 282 self.updating = False
283 283
284 284 def _update(self, name, old, new):
285 285 if self.updating:
286 286 return
287 287 with self._busy_updating():
288 288 for obj, attr in self.targets:
289 289 setattr(obj, attr, new)
290 290
291 291 def unlink(self):
292 292 self.source[0].on_trait_change(self._update, self.source[1], remove=True)
293 293 self.source = None
294 294 self.targets = []
295 295
296 296 dlink = directional_link
297 297
298 298
299 299 #-----------------------------------------------------------------------------
300 300 # Base TraitType for all traits
301 301 #-----------------------------------------------------------------------------
302 302
303 303
304 304 class TraitType(object):
305 305 """A base class for all trait descriptors.
306 306
307 307 Notes
308 308 -----
309 309 Our implementation of traits is based on Python's descriptor
310 310 prototol. This class is the base class for all such descriptors. The
311 311 only magic we use is a custom metaclass for the main :class:`HasTraits`
312 312 class that does the following:
313 313
314 314 1. Sets the :attr:`name` attribute of every :class:`TraitType`
315 315 instance in the class dict to the name of the attribute.
316 316 2. Sets the :attr:`this_class` attribute of every :class:`TraitType`
317 317 instance in the class dict to the *class* that declared the trait.
318 318 This is used by the :class:`This` trait to allow subclasses to
319 319 accept superclasses for :class:`This` values.
320 320 """
321 321
322 322
323 323 metadata = {}
324 324 default_value = Undefined
325 325 allow_none = False
326 326 info_text = 'any value'
327 327
328 328 def __init__(self, default_value=NoDefaultSpecified, allow_none=None, **metadata):
329 329 """Create a TraitType.
330 330 """
331 331 if default_value is not NoDefaultSpecified:
332 332 self.default_value = default_value
333 333 if allow_none is not None:
334 334 self.allow_none = allow_none
335 335
336 336 if 'default' in metadata:
337 337 # Warn the user that they probably meant default_value.
338 338 warn(
339 339 "Parameter 'default' passed to TraitType. "
340 340 "Did you mean 'default_value'?"
341 341 )
342 342
343 343 if len(metadata) > 0:
344 344 if len(self.metadata) > 0:
345 345 self._metadata = self.metadata.copy()
346 346 self._metadata.update(metadata)
347 347 else:
348 348 self._metadata = metadata
349 349 else:
350 350 self._metadata = self.metadata
351 351
352 352 self.init()
353 353
354 354 def init(self):
355 355 pass
356 356
357 357 def get_default_value(self):
358 358 """Create a new instance of the default value."""
359 359 return self.default_value
360 360
361 361 def instance_init(self):
362 362 """Part of the initialization which may depends on the underlying
363 363 HasTraits instance.
364 364
365 365 It is typically overloaded for specific trait types.
366 366
367 367 This method is called by :meth:`HasTraits.__new__` and in the
368 368 :meth:`TraitType.instance_init` method of trait types holding
369 369 other trait types.
370 370 """
371 371 pass
372 372
373 373 def init_default_value(self, obj):
374 374 """Instantiate the default value for the trait type.
375 375
376 376 This method is called by :meth:`TraitType.set_default_value` in the
377 377 case a default value is provided at construction time or later when
378 378 accessing the trait value for the first time in
379 379 :meth:`HasTraits.__get__`.
380 380 """
381 381 value = self.get_default_value()
382 382 value = self._validate(obj, value)
383 383 obj._trait_values[self.name] = value
384 384 return value
385 385
386 386 def set_default_value(self, obj):
387 387 """Set the default value on a per instance basis.
388 388
389 389 This method is called by :meth:`HasTraits.__new__` to instantiate and
390 390 validate the default value. The creation and validation of
391 391 default values must be delayed until the parent :class:`HasTraits`
392 392 class has been instantiated.
393 393 Parameters
394 394 ----------
395 395 obj : :class:`HasTraits` instance
396 396 The parent :class:`HasTraits` instance that has just been
397 397 created.
398 398 """
399 399 # Check for a deferred initializer defined in the same class as the
400 400 # trait declaration or above.
401 401 mro = type(obj).mro()
402 402 meth_name = '_%s_default' % self.name
403 403 for cls in mro[:mro.index(self.this_class)+1]:
404 404 if meth_name in cls.__dict__:
405 405 break
406 406 else:
407 407 # We didn't find one. Do static initialization.
408 408 self.init_default_value(obj)
409 409 return
410 410 # Complete the dynamic initialization.
411 411 obj._trait_dyn_inits[self.name] = meth_name
412 412
413 413 def __get__(self, obj, cls=None):
414 414 """Get the value of the trait by self.name for the instance.
415 415
416 416 Default values are instantiated when :meth:`HasTraits.__new__`
417 417 is called. Thus by the time this method gets called either the
418 418 default value or a user defined value (they called :meth:`__set__`)
419 419 is in the :class:`HasTraits` instance.
420 420 """
421 421 if obj is None:
422 422 return self
423 423 else:
424 424 try:
425 425 value = obj._trait_values[self.name]
426 426 except KeyError:
427 427 # Check for a dynamic initializer.
428 428 if self.name in obj._trait_dyn_inits:
429 429 method = getattr(obj, obj._trait_dyn_inits[self.name])
430 430 value = method()
431 431 # FIXME: Do we really validate here?
432 432 value = self._validate(obj, value)
433 433 obj._trait_values[self.name] = value
434 434 return value
435 435 else:
436 436 return self.init_default_value(obj)
437 437 except Exception:
438 438 # HasTraits should call set_default_value to populate
439 439 # this. So this should never be reached.
440 440 raise TraitError('Unexpected error in TraitType: '
441 441 'default value not set properly')
442 442 else:
443 443 return value
444 444
445 445 def __set__(self, obj, value):
446 446 new_value = self._validate(obj, value)
447 447 try:
448 448 old_value = obj._trait_values[self.name]
449 449 except KeyError:
450 450 old_value = None
451 451
452 452 obj._trait_values[self.name] = new_value
453 453 try:
454 454 silent = bool(old_value == new_value)
455 455 except:
456 456 # if there is an error in comparing, default to notify
457 457 silent = False
458 458 if silent is not True:
459 459 # we explicitly compare silent to True just in case the equality
460 460 # comparison above returns something other than True/False
461 461 obj._notify_trait(self.name, old_value, new_value)
462 462
463 463 def _validate(self, obj, value):
464 464 if value is None and self.allow_none:
465 465 return value
466 466 if hasattr(self, 'validate'):
467 467 value = self.validate(obj, value)
468 468 try:
469 469 obj_validate = getattr(obj, '_%s_validate' % self.name)
470 470 except (AttributeError, RuntimeError):
471 471 # Qt mixins raise RuntimeError on missing attrs accessed before __init__
472 472 pass
473 473 else:
474 474 value = obj_validate(value, self)
475 475 return value
476 476
477 477 def __or__(self, other):
478 478 if isinstance(other, Union):
479 479 return Union([self] + other.trait_types)
480 480 else:
481 481 return Union([self, other])
482 482
483 483 def info(self):
484 484 return self.info_text
485 485
486 486 def error(self, obj, value):
487 487 if obj is not None:
488 488 e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
489 489 % (self.name, class_of(obj),
490 490 self.info(), repr_type(value))
491 491 else:
492 492 e = "The '%s' trait must be %s, but a value of %r was specified." \
493 493 % (self.name, self.info(), repr_type(value))
494 494 raise TraitError(e)
495 495
496 496 def get_metadata(self, key, default=None):
497 497 return getattr(self, '_metadata', {}).get(key, default)
498 498
499 499 def set_metadata(self, key, value):
500 500 getattr(self, '_metadata', {})[key] = value
501 501
502 502
503 503 #-----------------------------------------------------------------------------
504 504 # The HasTraits implementation
505 505 #-----------------------------------------------------------------------------
506 506
507 507
508 508 class MetaHasTraits(type):
509 509 """A metaclass for HasTraits.
510 510
511 511 This metaclass makes sure that any TraitType class attributes are
512 512 instantiated and sets their name attribute.
513 513 """
514 514
515 515 def __new__(mcls, name, bases, classdict):
516 516 """Create the HasTraits class.
517 517
518 518 This instantiates all TraitTypes in the class dict and sets their
519 519 :attr:`name` attribute.
520 520 """
521 521 # print "MetaHasTraitlets (mcls, name): ", mcls, name
522 522 # print "MetaHasTraitlets (bases): ", bases
523 523 # print "MetaHasTraitlets (classdict): ", classdict
524 524 for k,v in iteritems(classdict):
525 525 if isinstance(v, TraitType):
526 526 v.name = k
527 527 elif inspect.isclass(v):
528 528 if issubclass(v, TraitType):
529 529 vinst = v()
530 530 vinst.name = k
531 531 classdict[k] = vinst
532 532 return super(MetaHasTraits, mcls).__new__(mcls, name, bases, classdict)
533 533
534 534 def __init__(cls, name, bases, classdict):
535 535 """Finish initializing the HasTraits class.
536 536
537 537 This sets the :attr:`this_class` attribute of each TraitType in the
538 538 class dict to the newly created class ``cls``.
539 539 """
540 540 for k, v in iteritems(classdict):
541 541 if isinstance(v, TraitType):
542 542 v.this_class = cls
543 543 super(MetaHasTraits, cls).__init__(name, bases, classdict)
544 544
545 545 class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)):
546 546
547 547 def __new__(cls, *args, **kw):
548 548 # This is needed because object.__new__ only accepts
549 549 # the cls argument.
550 550 new_meth = super(HasTraits, cls).__new__
551 551 if new_meth is object.__new__:
552 552 inst = new_meth(cls)
553 553 else:
554 554 inst = new_meth(cls, **kw)
555 555 inst._trait_values = {}
556 556 inst._trait_notifiers = {}
557 557 inst._trait_dyn_inits = {}
558 558 # Here we tell all the TraitType instances to set their default
559 559 # values on the instance.
560 560 for key in dir(cls):
561 561 # Some descriptors raise AttributeError like zope.interface's
562 562 # __provides__ attributes even though they exist. This causes
563 563 # AttributeErrors even though they are listed in dir(cls).
564 564 try:
565 565 value = getattr(cls, key)
566 566 except AttributeError:
567 567 pass
568 568 else:
569 569 if isinstance(value, TraitType):
570 570 value.instance_init()
571 571 if key not in kw:
572 572 value.set_default_value(inst)
573 573
574 574 return inst
575 575
576 576 def __init__(self, *args, **kw):
577 577 # Allow trait values to be set using keyword arguments.
578 578 # We need to use setattr for this to trigger validation and
579 579 # notifications.
580 580
581 581 with self.hold_trait_notifications():
582 582 for key, value in iteritems(kw):
583 583 setattr(self, key, value)
584 584
585 585 @contextlib.contextmanager
586 586 def hold_trait_notifications(self):
587 587 """Context manager for bundling trait change notifications
588 588
589 589 Use this when doing multiple trait assignments (init, config),
590 590 to avoid race conditions in trait notifiers requesting other trait values.
591 591 All trait notifications will fire after all values have been assigned.
592 592 """
593 593 _notify_trait = self._notify_trait
594 594 notifications = []
595 595 self._notify_trait = lambda *a: notifications.append(a)
596 596
597 597 try:
598 598 yield
599 599 finally:
600 600 self._notify_trait = _notify_trait
601 601 if isinstance(_notify_trait, types.MethodType):
602 602 # FIXME: remove when support is bumped to 3.4.
603 603 # when original method is restored,
604 604 # remove the redundant value from __dict__
605 605 # (only used to preserve pickleability on Python < 3.4)
606 606 self.__dict__.pop('_notify_trait', None)
607 607 # trigger delayed notifications
608 608 for args in notifications:
609 609 self._notify_trait(*args)
610 610
611 611 def _notify_trait(self, name, old_value, new_value):
612 612
613 613 # First dynamic ones
614 614 callables = []
615 615 callables.extend(self._trait_notifiers.get(name,[]))
616 616 callables.extend(self._trait_notifiers.get('anytrait',[]))
617 617
618 618 # Now static ones
619 619 try:
620 620 cb = getattr(self, '_%s_changed' % name)
621 621 except:
622 622 pass
623 623 else:
624 624 callables.append(cb)
625 625
626 626 # Call them all now
627 627 for c in callables:
628 628 # Traits catches and logs errors here. I allow them to raise
629 629 if callable(c):
630 630 argspec = getargspec(c)
631 631
632 632 nargs = len(argspec[0])
633 633 # Bound methods have an additional 'self' argument
634 634 # I don't know how to treat unbound methods, but they
635 635 # can't really be used for callbacks.
636 636 if isinstance(c, types.MethodType):
637 637 offset = -1
638 638 else:
639 639 offset = 0
640 640 if nargs + offset == 0:
641 641 c()
642 642 elif nargs + offset == 1:
643 643 c(name)
644 644 elif nargs + offset == 2:
645 645 c(name, new_value)
646 646 elif nargs + offset == 3:
647 647 c(name, old_value, new_value)
648 648 else:
649 649 raise TraitError('a trait changed callback '
650 650 'must have 0-3 arguments.')
651 651 else:
652 652 raise TraitError('a trait changed callback '
653 653 'must be callable.')
654 654
655 655
656 656 def _add_notifiers(self, handler, name):
657 657 if name not in self._trait_notifiers:
658 658 nlist = []
659 659 self._trait_notifiers[name] = nlist
660 660 else:
661 661 nlist = self._trait_notifiers[name]
662 662 if handler not in nlist:
663 663 nlist.append(handler)
664 664
665 665 def _remove_notifiers(self, handler, name):
666 666 if name in self._trait_notifiers:
667 667 nlist = self._trait_notifiers[name]
668 668 try:
669 669 index = nlist.index(handler)
670 670 except ValueError:
671 671 pass
672 672 else:
673 673 del nlist[index]
674 674
675 675 def on_trait_change(self, handler, name=None, remove=False):
676 676 """Setup a handler to be called when a trait changes.
677 677
678 678 This is used to setup dynamic notifications of trait changes.
679 679
680 680 Static handlers can be created by creating methods on a HasTraits
681 681 subclass with the naming convention '_[traitname]_changed'. Thus,
682 682 to create static handler for the trait 'a', create the method
683 683 _a_changed(self, name, old, new) (fewer arguments can be used, see
684 684 below).
685 685
686 686 Parameters
687 687 ----------
688 688 handler : callable
689 689 A callable that is called when a trait changes. Its
690 690 signature can be handler(), handler(name), handler(name, new)
691 691 or handler(name, old, new).
692 692 name : list, str, None
693 693 If None, the handler will apply to all traits. If a list
694 694 of str, handler will apply to all names in the list. If a
695 695 str, the handler will apply just to that name.
696 696 remove : bool
697 697 If False (the default), then install the handler. If True
698 698 then unintall it.
699 699 """
700 700 if remove:
701 701 names = parse_notifier_name(name)
702 702 for n in names:
703 703 self._remove_notifiers(handler, n)
704 704 else:
705 705 names = parse_notifier_name(name)
706 706 for n in names:
707 707 self._add_notifiers(handler, n)
708 708
709 709 @classmethod
710 710 def class_trait_names(cls, **metadata):
711 711 """Get a list of all the names of this class' traits.
712 712
713 713 This method is just like the :meth:`trait_names` method,
714 714 but is unbound.
715 715 """
716 716 return cls.class_traits(**metadata).keys()
717 717
718 718 @classmethod
719 719 def class_traits(cls, **metadata):
720 720 """Get a `dict` of all the traits of this class. The dictionary
721 721 is keyed on the name and the values are the TraitType objects.
722 722
723 723 This method is just like the :meth:`traits` method, but is unbound.
724 724
725 725 The TraitTypes returned don't know anything about the values
726 726 that the various HasTrait's instances are holding.
727 727
728 728 The metadata kwargs allow functions to be passed in which
729 729 filter traits based on metadata values. The functions should
730 730 take a single value as an argument and return a boolean. If
731 731 any function returns False, then the trait is not included in
732 732 the output. This does not allow for any simple way of
733 733 testing that a metadata name exists and has any
734 734 value because get_metadata returns None if a metadata key
735 735 doesn't exist.
736 736 """
737 737 traits = dict([memb for memb in getmembers(cls) if
738 738 isinstance(memb[1], TraitType)])
739 739
740 740 if len(metadata) == 0:
741 741 return traits
742 742
743 743 for meta_name, meta_eval in metadata.items():
744 744 if type(meta_eval) is not FunctionType:
745 745 metadata[meta_name] = _SimpleTest(meta_eval)
746 746
747 747 result = {}
748 748 for name, trait in traits.items():
749 749 for meta_name, meta_eval in metadata.items():
750 750 if not meta_eval(trait.get_metadata(meta_name)):
751 751 break
752 752 else:
753 753 result[name] = trait
754 754
755 755 return result
756 756
757 757 def trait_names(self, **metadata):
758 758 """Get a list of all the names of this class' traits."""
759 759 return self.traits(**metadata).keys()
760 760
761 761 def traits(self, **metadata):
762 762 """Get a `dict` of all the traits of this class. The dictionary
763 763 is keyed on the name and the values are the TraitType objects.
764 764
765 765 The TraitTypes returned don't know anything about the values
766 766 that the various HasTrait's instances are holding.
767 767
768 768 The metadata kwargs allow functions to be passed in which
769 769 filter traits based on metadata values. The functions should
770 770 take a single value as an argument and return a boolean. If
771 771 any function returns False, then the trait is not included in
772 772 the output. This does not allow for any simple way of
773 773 testing that a metadata name exists and has any
774 774 value because get_metadata returns None if a metadata key
775 775 doesn't exist.
776 776 """
777 777 traits = dict([memb for memb in getmembers(self.__class__) if
778 778 isinstance(memb[1], TraitType)])
779 779
780 780 if len(metadata) == 0:
781 781 return traits
782 782
783 783 for meta_name, meta_eval in metadata.items():
784 784 if type(meta_eval) is not FunctionType:
785 785 metadata[meta_name] = _SimpleTest(meta_eval)
786 786
787 787 result = {}
788 788 for name, trait in traits.items():
789 789 for meta_name, meta_eval in metadata.items():
790 790 if not meta_eval(trait.get_metadata(meta_name)):
791 791 break
792 792 else:
793 793 result[name] = trait
794 794
795 795 return result
796 796
797 797 def trait_metadata(self, traitname, key, default=None):
798 798 """Get metadata values for trait by key."""
799 799 try:
800 800 trait = getattr(self.__class__, traitname)
801 801 except AttributeError:
802 802 raise TraitError("Class %s does not have a trait named %s" %
803 803 (self.__class__.__name__, traitname))
804 804 else:
805 805 return trait.get_metadata(key, default)
806 806
807 def add_trait(self, traitname, trait):
808 """Dynamically add a trait attribute to the HasTraits instance."""
809 self.__class__ = type(self.__class__.__name__, (self.__class__,),
810 {traitname: trait})
811 trait.set_default_value(self)
812
807 813 #-----------------------------------------------------------------------------
808 814 # Actual TraitTypes implementations/subclasses
809 815 #-----------------------------------------------------------------------------
810 816
811 817 #-----------------------------------------------------------------------------
812 818 # TraitTypes subclasses for handling classes and instances of classes
813 819 #-----------------------------------------------------------------------------
814 820
815 821
816 822 class ClassBasedTraitType(TraitType):
817 823 """
818 824 A trait with error reporting and string -> type resolution for Type,
819 825 Instance and This.
820 826 """
821 827
822 828 def _resolve_string(self, string):
823 829 """
824 830 Resolve a string supplied for a type into an actual object.
825 831 """
826 832 return import_item(string)
827 833
828 834 def error(self, obj, value):
829 835 kind = type(value)
830 836 if (not py3compat.PY3) and kind is InstanceType:
831 837 msg = 'class %s' % value.__class__.__name__
832 838 else:
833 839 msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) )
834 840
835 841 if obj is not None:
836 842 e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
837 843 % (self.name, class_of(obj),
838 844 self.info(), msg)
839 845 else:
840 846 e = "The '%s' trait must be %s, but a value of %r was specified." \
841 847 % (self.name, self.info(), msg)
842 848
843 849 raise TraitError(e)
844 850
845 851
846 852 class Type(ClassBasedTraitType):
847 853 """A trait whose value must be a subclass of a specified class."""
848 854
849 855 def __init__ (self, default_value=None, klass=None, allow_none=True, **metadata ):
850 856 """Construct a Type trait
851 857
852 858 A Type trait specifies that its values must be subclasses of
853 859 a particular class.
854 860
855 861 If only ``default_value`` is given, it is used for the ``klass`` as
856 862 well.
857 863
858 864 Parameters
859 865 ----------
860 866 default_value : class, str or None
861 867 The default value must be a subclass of klass. If an str,
862 868 the str must be a fully specified class name, like 'foo.bar.Bah'.
863 869 The string is resolved into real class, when the parent
864 870 :class:`HasTraits` class is instantiated.
865 871 klass : class, str, None
866 872 Values of this trait must be a subclass of klass. The klass
867 873 may be specified in a string like: 'foo.bar.MyClass'.
868 874 The string is resolved into real class, when the parent
869 875 :class:`HasTraits` class is instantiated.
870 876 allow_none : bool [ default True ]
871 877 Indicates whether None is allowed as an assignable value. Even if
872 878 ``False``, the default value may be ``None``.
873 879 """
874 880 if default_value is None:
875 881 if klass is None:
876 882 klass = object
877 883 elif klass is None:
878 884 klass = default_value
879 885
880 886 if not (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)):
881 887 raise TraitError("A Type trait must specify a class.")
882 888
883 889 self.klass = klass
884 890
885 891 super(Type, self).__init__(default_value, allow_none=allow_none, **metadata)
886 892
887 893 def validate(self, obj, value):
888 894 """Validates that the value is a valid object instance."""
889 895 if isinstance(value, py3compat.string_types):
890 896 try:
891 897 value = self._resolve_string(value)
892 898 except ImportError:
893 899 raise TraitError("The '%s' trait of %s instance must be a type, but "
894 900 "%r could not be imported" % (self.name, obj, value))
895 901 try:
896 902 if issubclass(value, self.klass):
897 903 return value
898 904 except:
899 905 pass
900 906
901 907 self.error(obj, value)
902 908
903 909 def info(self):
904 910 """ Returns a description of the trait."""
905 911 if isinstance(self.klass, py3compat.string_types):
906 912 klass = self.klass
907 913 else:
908 914 klass = self.klass.__name__
909 915 result = 'a subclass of ' + klass
910 916 if self.allow_none:
911 917 return result + ' or None'
912 918 return result
913 919
914 920 def instance_init(self):
915 921 self._resolve_classes()
916 922 super(Type, self).instance_init()
917 923
918 924 def _resolve_classes(self):
919 925 if isinstance(self.klass, py3compat.string_types):
920 926 self.klass = self._resolve_string(self.klass)
921 927 if isinstance(self.default_value, py3compat.string_types):
922 928 self.default_value = self._resolve_string(self.default_value)
923 929
924 930 def get_default_value(self):
925 931 return self.default_value
926 932
927 933
928 934 class DefaultValueGenerator(object):
929 935 """A class for generating new default value instances."""
930 936
931 937 def __init__(self, *args, **kw):
932 938 self.args = args
933 939 self.kw = kw
934 940
935 941 def generate(self, klass):
936 942 return klass(*self.args, **self.kw)
937 943
938 944
939 945 class Instance(ClassBasedTraitType):
940 946 """A trait whose value must be an instance of a specified class.
941 947
942 948 The value can also be an instance of a subclass of the specified class.
943 949
944 950 Subclasses can declare default classes by overriding the klass attribute
945 951 """
946 952
947 953 klass = None
948 954
949 955 def __init__(self, klass=None, args=None, kw=None,
950 956 allow_none=True, **metadata ):
951 957 """Construct an Instance trait.
952 958
953 959 This trait allows values that are instances of a particular
954 960 class or its subclasses. Our implementation is quite different
955 961 from that of enthough.traits as we don't allow instances to be used
956 962 for klass and we handle the ``args`` and ``kw`` arguments differently.
957 963
958 964 Parameters
959 965 ----------
960 966 klass : class, str
961 967 The class that forms the basis for the trait. Class names
962 968 can also be specified as strings, like 'foo.bar.Bar'.
963 969 args : tuple
964 970 Positional arguments for generating the default value.
965 971 kw : dict
966 972 Keyword arguments for generating the default value.
967 973 allow_none : bool [default True]
968 974 Indicates whether None is allowed as a value.
969 975
970 976 Notes
971 977 -----
972 978 If both ``args`` and ``kw`` are None, then the default value is None.
973 979 If ``args`` is a tuple and ``kw`` is a dict, then the default is
974 980 created as ``klass(*args, **kw)``. If exactly one of ``args`` or ``kw`` is
975 981 None, the None is replaced by ``()`` or ``{}``, respectively.
976 982 """
977 983 if klass is None:
978 984 klass = self.klass
979 985
980 986 if (klass is not None) and (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)):
981 987 self.klass = klass
982 988 else:
983 989 raise TraitError('The klass attribute must be a class'
984 990 ' not: %r' % klass)
985 991
986 992 # self.klass is a class, so handle default_value
987 993 if args is None and kw is None:
988 994 default_value = None
989 995 else:
990 996 if args is None:
991 997 # kw is not None
992 998 args = ()
993 999 elif kw is None:
994 1000 # args is not None
995 1001 kw = {}
996 1002
997 1003 if not isinstance(kw, dict):
998 1004 raise TraitError("The 'kw' argument must be a dict or None.")
999 1005 if not isinstance(args, tuple):
1000 1006 raise TraitError("The 'args' argument must be a tuple or None.")
1001 1007
1002 1008 default_value = DefaultValueGenerator(*args, **kw)
1003 1009
1004 1010 super(Instance, self).__init__(default_value, allow_none=allow_none, **metadata)
1005 1011
1006 1012 def validate(self, obj, value):
1007 1013 if isinstance(value, self.klass):
1008 1014 return value
1009 1015 else:
1010 1016 self.error(obj, value)
1011 1017
1012 1018 def info(self):
1013 1019 if isinstance(self.klass, py3compat.string_types):
1014 1020 klass = self.klass
1015 1021 else:
1016 1022 klass = self.klass.__name__
1017 1023 result = class_of(klass)
1018 1024 if self.allow_none:
1019 1025 return result + ' or None'
1020 1026
1021 1027 return result
1022 1028
1023 1029 def instance_init(self):
1024 1030 self._resolve_classes()
1025 1031 super(Instance, self).instance_init()
1026 1032
1027 1033 def _resolve_classes(self):
1028 1034 if isinstance(self.klass, py3compat.string_types):
1029 1035 self.klass = self._resolve_string(self.klass)
1030 1036
1031 1037 def get_default_value(self):
1032 1038 """Instantiate a default value instance.
1033 1039
1034 1040 This is called when the containing HasTraits classes'
1035 1041 :meth:`__new__` method is called to ensure that a unique instance
1036 1042 is created for each HasTraits instance.
1037 1043 """
1038 1044 dv = self.default_value
1039 1045 if isinstance(dv, DefaultValueGenerator):
1040 1046 return dv.generate(self.klass)
1041 1047 else:
1042 1048 return dv
1043 1049
1044 1050
1045 1051 class ForwardDeclaredMixin(object):
1046 1052 """
1047 1053 Mixin for forward-declared versions of Instance and Type.
1048 1054 """
1049 1055 def _resolve_string(self, string):
1050 1056 """
1051 1057 Find the specified class name by looking for it in the module in which
1052 1058 our this_class attribute was defined.
1053 1059 """
1054 1060 modname = self.this_class.__module__
1055 1061 return import_item('.'.join([modname, string]))
1056 1062
1057 1063
1058 1064 class ForwardDeclaredType(ForwardDeclaredMixin, Type):
1059 1065 """
1060 1066 Forward-declared version of Type.
1061 1067 """
1062 1068 pass
1063 1069
1064 1070
1065 1071 class ForwardDeclaredInstance(ForwardDeclaredMixin, Instance):
1066 1072 """
1067 1073 Forward-declared version of Instance.
1068 1074 """
1069 1075 pass
1070 1076
1071 1077
1072 1078 class This(ClassBasedTraitType):
1073 1079 """A trait for instances of the class containing this trait.
1074 1080
1075 1081 Because how how and when class bodies are executed, the ``This``
1076 1082 trait can only have a default value of None. This, and because we
1077 1083 always validate default values, ``allow_none`` is *always* true.
1078 1084 """
1079 1085
1080 1086 info_text = 'an instance of the same type as the receiver or None'
1081 1087
1082 1088 def __init__(self, **metadata):
1083 1089 super(This, self).__init__(None, **metadata)
1084 1090
1085 1091 def validate(self, obj, value):
1086 1092 # What if value is a superclass of obj.__class__? This is
1087 1093 # complicated if it was the superclass that defined the This
1088 1094 # trait.
1089 1095 if isinstance(value, self.this_class) or (value is None):
1090 1096 return value
1091 1097 else:
1092 1098 self.error(obj, value)
1093 1099
1094 1100
1095 1101 class Union(TraitType):
1096 1102 """A trait type representing a Union type."""
1097 1103
1098 1104 def __init__(self, trait_types, **metadata):
1099 1105 """Construct a Union trait.
1100 1106
1101 1107 This trait allows values that are allowed by at least one of the
1102 1108 specified trait types. A Union traitlet cannot have metadata on
1103 1109 its own, besides the metadata of the listed types.
1104 1110
1105 1111 Parameters
1106 1112 ----------
1107 1113 trait_types: sequence
1108 1114 The list of trait types of length at least 1.
1109 1115
1110 1116 Notes
1111 1117 -----
1112 1118 Union([Float(), Bool(), Int()]) attempts to validate the provided values
1113 1119 with the validation function of Float, then Bool, and finally Int.
1114 1120 """
1115 1121 self.trait_types = trait_types
1116 1122 self.info_text = " or ".join([tt.info_text for tt in self.trait_types])
1117 1123 self.default_value = self.trait_types[0].get_default_value()
1118 1124 super(Union, self).__init__(**metadata)
1119 1125
1120 1126 def instance_init(self):
1121 1127 for trait_type in self.trait_types:
1122 1128 trait_type.name = self.name
1123 1129 trait_type.this_class = self.this_class
1124 1130 trait_type.instance_init()
1125 1131 super(Union, self).instance_init()
1126 1132
1127 1133 def validate(self, obj, value):
1128 1134 for trait_type in self.trait_types:
1129 1135 try:
1130 1136 v = trait_type._validate(obj, value)
1131 1137 self._metadata = trait_type._metadata
1132 1138 return v
1133 1139 except TraitError:
1134 1140 continue
1135 1141 self.error(obj, value)
1136 1142
1137 1143 def __or__(self, other):
1138 1144 if isinstance(other, Union):
1139 1145 return Union(self.trait_types + other.trait_types)
1140 1146 else:
1141 1147 return Union(self.trait_types + [other])
1142 1148
1143 1149 #-----------------------------------------------------------------------------
1144 1150 # Basic TraitTypes implementations/subclasses
1145 1151 #-----------------------------------------------------------------------------
1146 1152
1147 1153
1148 1154 class Any(TraitType):
1149 1155 default_value = None
1150 1156 info_text = 'any value'
1151 1157
1152 1158
1153 1159 class Int(TraitType):
1154 1160 """An int trait."""
1155 1161
1156 1162 default_value = 0
1157 1163 info_text = 'an int'
1158 1164
1159 1165 def validate(self, obj, value):
1160 1166 if isinstance(value, int):
1161 1167 return value
1162 1168 self.error(obj, value)
1163 1169
1164 1170 class CInt(Int):
1165 1171 """A casting version of the int trait."""
1166 1172
1167 1173 def validate(self, obj, value):
1168 1174 try:
1169 1175 return int(value)
1170 1176 except:
1171 1177 self.error(obj, value)
1172 1178
1173 1179 if py3compat.PY3:
1174 1180 Long, CLong = Int, CInt
1175 1181 Integer = Int
1176 1182 else:
1177 1183 class Long(TraitType):
1178 1184 """A long integer trait."""
1179 1185
1180 1186 default_value = 0
1181 1187 info_text = 'a long'
1182 1188
1183 1189 def validate(self, obj, value):
1184 1190 if isinstance(value, long):
1185 1191 return value
1186 1192 if isinstance(value, int):
1187 1193 return long(value)
1188 1194 self.error(obj, value)
1189 1195
1190 1196
1191 1197 class CLong(Long):
1192 1198 """A casting version of the long integer trait."""
1193 1199
1194 1200 def validate(self, obj, value):
1195 1201 try:
1196 1202 return long(value)
1197 1203 except:
1198 1204 self.error(obj, value)
1199 1205
1200 1206 class Integer(TraitType):
1201 1207 """An integer trait.
1202 1208
1203 1209 Longs that are unnecessary (<= sys.maxint) are cast to ints."""
1204 1210
1205 1211 default_value = 0
1206 1212 info_text = 'an integer'
1207 1213
1208 1214 def validate(self, obj, value):
1209 1215 if isinstance(value, int):
1210 1216 return value
1211 1217 if isinstance(value, long):
1212 1218 # downcast longs that fit in int:
1213 1219 # note that int(n > sys.maxint) returns a long, so
1214 1220 # we don't need a condition on this cast
1215 1221 return int(value)
1216 1222 if sys.platform == "cli":
1217 1223 from System import Int64
1218 1224 if isinstance(value, Int64):
1219 1225 return int(value)
1220 1226 self.error(obj, value)
1221 1227
1222 1228
1223 1229 class Float(TraitType):
1224 1230 """A float trait."""
1225 1231
1226 1232 default_value = 0.0
1227 1233 info_text = 'a float'
1228 1234
1229 1235 def validate(self, obj, value):
1230 1236 if isinstance(value, float):
1231 1237 return value
1232 1238 if isinstance(value, int):
1233 1239 return float(value)
1234 1240 self.error(obj, value)
1235 1241
1236 1242
1237 1243 class CFloat(Float):
1238 1244 """A casting version of the float trait."""
1239 1245
1240 1246 def validate(self, obj, value):
1241 1247 try:
1242 1248 return float(value)
1243 1249 except:
1244 1250 self.error(obj, value)
1245 1251
1246 1252 class Complex(TraitType):
1247 1253 """A trait for complex numbers."""
1248 1254
1249 1255 default_value = 0.0 + 0.0j
1250 1256 info_text = 'a complex number'
1251 1257
1252 1258 def validate(self, obj, value):
1253 1259 if isinstance(value, complex):
1254 1260 return value
1255 1261 if isinstance(value, (float, int)):
1256 1262 return complex(value)
1257 1263 self.error(obj, value)
1258 1264
1259 1265
1260 1266 class CComplex(Complex):
1261 1267 """A casting version of the complex number trait."""
1262 1268
1263 1269 def validate (self, obj, value):
1264 1270 try:
1265 1271 return complex(value)
1266 1272 except:
1267 1273 self.error(obj, value)
1268 1274
1269 1275 # We should always be explicit about whether we're using bytes or unicode, both
1270 1276 # for Python 3 conversion and for reliable unicode behaviour on Python 2. So
1271 1277 # we don't have a Str type.
1272 1278 class Bytes(TraitType):
1273 1279 """A trait for byte strings."""
1274 1280
1275 1281 default_value = b''
1276 1282 info_text = 'a bytes object'
1277 1283
1278 1284 def validate(self, obj, value):
1279 1285 if isinstance(value, bytes):
1280 1286 return value
1281 1287 self.error(obj, value)
1282 1288
1283 1289
1284 1290 class CBytes(Bytes):
1285 1291 """A casting version of the byte string trait."""
1286 1292
1287 1293 def validate(self, obj, value):
1288 1294 try:
1289 1295 return bytes(value)
1290 1296 except:
1291 1297 self.error(obj, value)
1292 1298
1293 1299
1294 1300 class Unicode(TraitType):
1295 1301 """A trait for unicode strings."""
1296 1302
1297 1303 default_value = u''
1298 1304 info_text = 'a unicode string'
1299 1305
1300 1306 def validate(self, obj, value):
1301 1307 if isinstance(value, py3compat.unicode_type):
1302 1308 return value
1303 1309 if isinstance(value, bytes):
1304 1310 try:
1305 1311 return value.decode('ascii', 'strict')
1306 1312 except UnicodeDecodeError:
1307 1313 msg = "Could not decode {!r} for unicode trait '{}' of {} instance."
1308 1314 raise TraitError(msg.format(value, self.name, class_of(obj)))
1309 1315 self.error(obj, value)
1310 1316
1311 1317
1312 1318 class CUnicode(Unicode):
1313 1319 """A casting version of the unicode trait."""
1314 1320
1315 1321 def validate(self, obj, value):
1316 1322 try:
1317 1323 return py3compat.unicode_type(value)
1318 1324 except:
1319 1325 self.error(obj, value)
1320 1326
1321 1327
1322 1328 class ObjectName(TraitType):
1323 1329 """A string holding a valid object name in this version of Python.
1324 1330
1325 1331 This does not check that the name exists in any scope."""
1326 1332 info_text = "a valid object identifier in Python"
1327 1333
1328 1334 if py3compat.PY3:
1329 1335 # Python 3:
1330 1336 coerce_str = staticmethod(lambda _,s: s)
1331 1337
1332 1338 else:
1333 1339 # Python 2:
1334 1340 def coerce_str(self, obj, value):
1335 1341 "In Python 2, coerce ascii-only unicode to str"
1336 1342 if isinstance(value, unicode):
1337 1343 try:
1338 1344 return str(value)
1339 1345 except UnicodeEncodeError:
1340 1346 self.error(obj, value)
1341 1347 return value
1342 1348
1343 1349 def validate(self, obj, value):
1344 1350 value = self.coerce_str(obj, value)
1345 1351
1346 1352 if isinstance(value, string_types) and py3compat.isidentifier(value):
1347 1353 return value
1348 1354 self.error(obj, value)
1349 1355
1350 1356 class DottedObjectName(ObjectName):
1351 1357 """A string holding a valid dotted object name in Python, such as A.b3._c"""
1352 1358 def validate(self, obj, value):
1353 1359 value = self.coerce_str(obj, value)
1354 1360
1355 1361 if isinstance(value, string_types) and py3compat.isidentifier(value, dotted=True):
1356 1362 return value
1357 1363 self.error(obj, value)
1358 1364
1359 1365
1360 1366 class Bool(TraitType):
1361 1367 """A boolean (True, False) trait."""
1362 1368
1363 1369 default_value = False
1364 1370 info_text = 'a boolean'
1365 1371
1366 1372 def validate(self, obj, value):
1367 1373 if isinstance(value, bool):
1368 1374 return value
1369 1375 self.error(obj, value)
1370 1376
1371 1377
1372 1378 class CBool(Bool):
1373 1379 """A casting version of the boolean trait."""
1374 1380
1375 1381 def validate(self, obj, value):
1376 1382 try:
1377 1383 return bool(value)
1378 1384 except:
1379 1385 self.error(obj, value)
1380 1386
1381 1387
1382 1388 class Enum(TraitType):
1383 1389 """An enum that whose value must be in a given sequence."""
1384 1390
1385 1391 def __init__(self, values, default_value=None, **metadata):
1386 1392 self.values = values
1387 1393 super(Enum, self).__init__(default_value, **metadata)
1388 1394
1389 1395 def validate(self, obj, value):
1390 1396 if value in self.values:
1391 1397 return value
1392 1398 self.error(obj, value)
1393 1399
1394 1400 def info(self):
1395 1401 """ Returns a description of the trait."""
1396 1402 result = 'any of ' + repr(self.values)
1397 1403 if self.allow_none:
1398 1404 return result + ' or None'
1399 1405 return result
1400 1406
1401 1407 class CaselessStrEnum(Enum):
1402 1408 """An enum of strings that are caseless in validate."""
1403 1409
1404 1410 def validate(self, obj, value):
1405 1411 if not isinstance(value, py3compat.string_types):
1406 1412 self.error(obj, value)
1407 1413
1408 1414 for v in self.values:
1409 1415 if v.lower() == value.lower():
1410 1416 return v
1411 1417 self.error(obj, value)
1412 1418
1413 1419 class Container(Instance):
1414 1420 """An instance of a container (list, set, etc.)
1415 1421
1416 1422 To be subclassed by overriding klass.
1417 1423 """
1418 1424 klass = None
1419 1425 _cast_types = ()
1420 1426 _valid_defaults = SequenceTypes
1421 1427 _trait = None
1422 1428
1423 1429 def __init__(self, trait=None, default_value=None, allow_none=False,
1424 1430 **metadata):
1425 1431 """Create a container trait type from a list, set, or tuple.
1426 1432
1427 1433 The default value is created by doing ``List(default_value)``,
1428 1434 which creates a copy of the ``default_value``.
1429 1435
1430 1436 ``trait`` can be specified, which restricts the type of elements
1431 1437 in the container to that TraitType.
1432 1438
1433 1439 If only one arg is given and it is not a Trait, it is taken as
1434 1440 ``default_value``:
1435 1441
1436 1442 ``c = List([1,2,3])``
1437 1443
1438 1444 Parameters
1439 1445 ----------
1440 1446
1441 1447 trait : TraitType [ optional ]
1442 1448 the type for restricting the contents of the Container. If unspecified,
1443 1449 types are not checked.
1444 1450
1445 1451 default_value : SequenceType [ optional ]
1446 1452 The default value for the Trait. Must be list/tuple/set, and
1447 1453 will be cast to the container type.
1448 1454
1449 1455 allow_none : bool [ default False ]
1450 1456 Whether to allow the value to be None
1451 1457
1452 1458 **metadata : any
1453 1459 further keys for extensions to the Trait (e.g. config)
1454 1460
1455 1461 """
1456 1462 # allow List([values]):
1457 1463 if default_value is None and not is_trait(trait):
1458 1464 default_value = trait
1459 1465 trait = None
1460 1466
1461 1467 if default_value is None:
1462 1468 args = ()
1463 1469 elif isinstance(default_value, self._valid_defaults):
1464 1470 args = (default_value,)
1465 1471 else:
1466 1472 raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value))
1467 1473
1468 1474 if is_trait(trait):
1469 1475 self._trait = trait() if isinstance(trait, type) else trait
1470 1476 self._trait.name = 'element'
1471 1477 elif trait is not None:
1472 1478 raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait))
1473 1479
1474 1480 super(Container,self).__init__(klass=self.klass, args=args,
1475 1481 allow_none=allow_none, **metadata)
1476 1482
1477 1483 def element_error(self, obj, element, validator):
1478 1484 e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \
1479 1485 % (self.name, class_of(obj), validator.info(), repr_type(element))
1480 1486 raise TraitError(e)
1481 1487
1482 1488 def validate(self, obj, value):
1483 1489 if isinstance(value, self._cast_types):
1484 1490 value = self.klass(value)
1485 1491 value = super(Container, self).validate(obj, value)
1486 1492 if value is None:
1487 1493 return value
1488 1494
1489 1495 value = self.validate_elements(obj, value)
1490 1496
1491 1497 return value
1492 1498
1493 1499 def validate_elements(self, obj, value):
1494 1500 validated = []
1495 1501 if self._trait is None or isinstance(self._trait, Any):
1496 1502 return value
1497 1503 for v in value:
1498 1504 try:
1499 1505 v = self._trait._validate(obj, v)
1500 1506 except TraitError:
1501 1507 self.element_error(obj, v, self._trait)
1502 1508 else:
1503 1509 validated.append(v)
1504 1510 return self.klass(validated)
1505 1511
1506 1512 def instance_init(self):
1507 1513 if isinstance(self._trait, TraitType):
1508 1514 self._trait.this_class = self.this_class
1509 1515 self._trait.instance_init()
1510 1516 super(Container, self).instance_init()
1511 1517
1512 1518
1513 1519 class List(Container):
1514 1520 """An instance of a Python list."""
1515 1521 klass = list
1516 1522 _cast_types = (tuple,)
1517 1523
1518 1524 def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize, **metadata):
1519 1525 """Create a List trait type from a list, set, or tuple.
1520 1526
1521 1527 The default value is created by doing ``List(default_value)``,
1522 1528 which creates a copy of the ``default_value``.
1523 1529
1524 1530 ``trait`` can be specified, which restricts the type of elements
1525 1531 in the container to that TraitType.
1526 1532
1527 1533 If only one arg is given and it is not a Trait, it is taken as
1528 1534 ``default_value``:
1529 1535
1530 1536 ``c = List([1,2,3])``
1531 1537
1532 1538 Parameters
1533 1539 ----------
1534 1540
1535 1541 trait : TraitType [ optional ]
1536 1542 the type for restricting the contents of the Container. If unspecified,
1537 1543 types are not checked.
1538 1544
1539 1545 default_value : SequenceType [ optional ]
1540 1546 The default value for the Trait. Must be list/tuple/set, and
1541 1547 will be cast to the container type.
1542 1548
1543 1549 minlen : Int [ default 0 ]
1544 1550 The minimum length of the input list
1545 1551
1546 1552 maxlen : Int [ default sys.maxsize ]
1547 1553 The maximum length of the input list
1548 1554
1549 1555 allow_none : bool [ default False ]
1550 1556 Whether to allow the value to be None
1551 1557
1552 1558 **metadata : any
1553 1559 further keys for extensions to the Trait (e.g. config)
1554 1560
1555 1561 """
1556 1562 self._minlen = minlen
1557 1563 self._maxlen = maxlen
1558 1564 super(List, self).__init__(trait=trait, default_value=default_value,
1559 1565 **metadata)
1560 1566
1561 1567 def length_error(self, obj, value):
1562 1568 e = "The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified." \
1563 1569 % (self.name, class_of(obj), self._minlen, self._maxlen, value)
1564 1570 raise TraitError(e)
1565 1571
1566 1572 def validate_elements(self, obj, value):
1567 1573 length = len(value)
1568 1574 if length < self._minlen or length > self._maxlen:
1569 1575 self.length_error(obj, value)
1570 1576
1571 1577 return super(List, self).validate_elements(obj, value)
1572 1578
1573 1579 def validate(self, obj, value):
1574 1580 value = super(List, self).validate(obj, value)
1575 1581 value = self.validate_elements(obj, value)
1576 1582 return value
1577 1583
1578 1584
1579 1585 class Set(List):
1580 1586 """An instance of a Python set."""
1581 1587 klass = set
1582 1588 _cast_types = (tuple, list)
1583 1589
1584 1590
1585 1591 class Tuple(Container):
1586 1592 """An instance of a Python tuple."""
1587 1593 klass = tuple
1588 1594 _cast_types = (list,)
1589 1595
1590 1596 def __init__(self, *traits, **metadata):
1591 1597 """Tuple(*traits, default_value=None, **medatata)
1592 1598
1593 1599 Create a tuple from a list, set, or tuple.
1594 1600
1595 1601 Create a fixed-type tuple with Traits:
1596 1602
1597 1603 ``t = Tuple(Int, Str, CStr)``
1598 1604
1599 1605 would be length 3, with Int,Str,CStr for each element.
1600 1606
1601 1607 If only one arg is given and it is not a Trait, it is taken as
1602 1608 default_value:
1603 1609
1604 1610 ``t = Tuple((1,2,3))``
1605 1611
1606 1612 Otherwise, ``default_value`` *must* be specified by keyword.
1607 1613
1608 1614 Parameters
1609 1615 ----------
1610 1616
1611 1617 *traits : TraitTypes [ optional ]
1612 1618 the types for restricting the contents of the Tuple. If unspecified,
1613 1619 types are not checked. If specified, then each positional argument
1614 1620 corresponds to an element of the tuple. Tuples defined with traits
1615 1621 are of fixed length.
1616 1622
1617 1623 default_value : SequenceType [ optional ]
1618 1624 The default value for the Tuple. Must be list/tuple/set, and
1619 1625 will be cast to a tuple. If `traits` are specified, the
1620 1626 `default_value` must conform to the shape and type they specify.
1621 1627
1622 1628 allow_none : bool [ default False ]
1623 1629 Whether to allow the value to be None
1624 1630
1625 1631 **metadata : any
1626 1632 further keys for extensions to the Trait (e.g. config)
1627 1633
1628 1634 """
1629 1635 default_value = metadata.pop('default_value', None)
1630 1636 allow_none = metadata.pop('allow_none', True)
1631 1637
1632 1638 # allow Tuple((values,)):
1633 1639 if len(traits) == 1 and default_value is None and not is_trait(traits[0]):
1634 1640 default_value = traits[0]
1635 1641 traits = ()
1636 1642
1637 1643 if default_value is None:
1638 1644 args = ()
1639 1645 elif isinstance(default_value, self._valid_defaults):
1640 1646 args = (default_value,)
1641 1647 else:
1642 1648 raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value))
1643 1649
1644 1650 self._traits = []
1645 1651 for trait in traits:
1646 1652 t = trait() if isinstance(trait, type) else trait
1647 1653 t.name = 'element'
1648 1654 self._traits.append(t)
1649 1655
1650 1656 if self._traits and default_value is None:
1651 1657 # don't allow default to be an empty container if length is specified
1652 1658 args = None
1653 1659 super(Container,self).__init__(klass=self.klass, args=args, **metadata)
1654 1660
1655 1661 def validate_elements(self, obj, value):
1656 1662 if not self._traits:
1657 1663 # nothing to validate
1658 1664 return value
1659 1665 if len(value) != len(self._traits):
1660 1666 e = "The '%s' trait of %s instance requires %i elements, but a value of %s was specified." \
1661 1667 % (self.name, class_of(obj), len(self._traits), repr_type(value))
1662 1668 raise TraitError(e)
1663 1669
1664 1670 validated = []
1665 1671 for t, v in zip(self._traits, value):
1666 1672 try:
1667 1673 v = t._validate(obj, v)
1668 1674 except TraitError:
1669 1675 self.element_error(obj, v, t)
1670 1676 else:
1671 1677 validated.append(v)
1672 1678 return tuple(validated)
1673 1679
1674 1680 def instance_init(self):
1675 1681 for trait in self._traits:
1676 1682 if isinstance(trait, TraitType):
1677 1683 trait.this_class = self.this_class
1678 1684 trait.instance_init()
1679 1685 super(Container, self).instance_init()
1680 1686
1681 1687
1682 1688 class Dict(Instance):
1683 1689 """An instance of a Python dict."""
1684 1690 _trait = None
1685 1691
1686 1692 def __init__(self, trait=None, default_value=NoDefaultSpecified, allow_none=False, **metadata):
1687 1693 """Create a dict trait type from a dict.
1688 1694
1689 1695 The default value is created by doing ``dict(default_value)``,
1690 1696 which creates a copy of the ``default_value``.
1691 1697
1692 1698 trait : TraitType [ optional ]
1693 1699 the type for restricting the contents of the Container. If unspecified,
1694 1700 types are not checked.
1695 1701
1696 1702 default_value : SequenceType [ optional ]
1697 1703 The default value for the Dict. Must be dict, tuple, or None, and
1698 1704 will be cast to a dict if not None. If `trait` is specified, the
1699 1705 `default_value` must conform to the constraints it specifies.
1700 1706
1701 1707 allow_none : bool [ default False ]
1702 1708 Whether to allow the value to be None
1703 1709
1704 1710 """
1705 1711 if default_value is NoDefaultSpecified and trait is not None:
1706 1712 if not is_trait(trait):
1707 1713 default_value = trait
1708 1714 trait = None
1709 1715 if default_value is NoDefaultSpecified:
1710 1716 default_value = {}
1711 1717 if default_value is None:
1712 1718 args = None
1713 1719 elif isinstance(default_value, dict):
1714 1720 args = (default_value,)
1715 1721 elif isinstance(default_value, SequenceTypes):
1716 1722 args = (default_value,)
1717 1723 else:
1718 1724 raise TypeError('default value of Dict was %s' % default_value)
1719 1725
1720 1726 if is_trait(trait):
1721 1727 self._trait = trait() if isinstance(trait, type) else trait
1722 1728 self._trait.name = 'element'
1723 1729 elif trait is not None:
1724 1730 raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait))
1725 1731
1726 1732 super(Dict,self).__init__(klass=dict, args=args,
1727 1733 allow_none=allow_none, **metadata)
1728 1734
1729 1735 def element_error(self, obj, element, validator):
1730 1736 e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \
1731 1737 % (self.name, class_of(obj), validator.info(), repr_type(element))
1732 1738 raise TraitError(e)
1733 1739
1734 1740 def validate(self, obj, value):
1735 1741 value = super(Dict, self).validate(obj, value)
1736 1742 if value is None:
1737 1743 return value
1738 1744 value = self.validate_elements(obj, value)
1739 1745 return value
1740 1746
1741 1747 def validate_elements(self, obj, value):
1742 1748 if self._trait is None or isinstance(self._trait, Any):
1743 1749 return value
1744 1750 validated = {}
1745 1751 for key in value:
1746 1752 v = value[key]
1747 1753 try:
1748 1754 v = self._trait._validate(obj, v)
1749 1755 except TraitError:
1750 1756 self.element_error(obj, v, self._trait)
1751 1757 else:
1752 1758 validated[key] = v
1753 1759 return self.klass(validated)
1754 1760
1755 1761 def instance_init(self):
1756 1762 if isinstance(self._trait, TraitType):
1757 1763 self._trait.this_class = self.this_class
1758 1764 self._trait.instance_init()
1759 1765 super(Dict, self).instance_init()
1760 1766
1761 1767
1762 1768 class EventfulDict(Instance):
1763 1769 """An instance of an EventfulDict."""
1764 1770
1765 1771 def __init__(self, default_value={}, allow_none=False, **metadata):
1766 1772 """Create a EventfulDict trait type from a dict.
1767 1773
1768 1774 The default value is created by doing
1769 1775 ``eventful.EvenfulDict(default_value)``, which creates a copy of the
1770 1776 ``default_value``.
1771 1777 """
1772 1778 if default_value is None:
1773 1779 args = None
1774 1780 elif isinstance(default_value, dict):
1775 1781 args = (default_value,)
1776 1782 elif isinstance(default_value, SequenceTypes):
1777 1783 args = (default_value,)
1778 1784 else:
1779 1785 raise TypeError('default value of EventfulDict was %s' % default_value)
1780 1786
1781 1787 super(EventfulDict, self).__init__(klass=eventful.EventfulDict, args=args,
1782 1788 allow_none=allow_none, **metadata)
1783 1789
1784 1790
1785 1791 class EventfulList(Instance):
1786 1792 """An instance of an EventfulList."""
1787 1793
1788 1794 def __init__(self, default_value=None, allow_none=False, **metadata):
1789 1795 """Create a EventfulList trait type from a dict.
1790 1796
1791 1797 The default value is created by doing
1792 1798 ``eventful.EvenfulList(default_value)``, which creates a copy of the
1793 1799 ``default_value``.
1794 1800 """
1795 1801 if default_value is None:
1796 1802 args = ((),)
1797 1803 else:
1798 1804 args = (default_value,)
1799 1805
1800 1806 super(EventfulList, self).__init__(klass=eventful.EventfulList, args=args,
1801 1807 allow_none=allow_none, **metadata)
1802 1808
1803 1809
1804 1810 class TCPAddress(TraitType):
1805 1811 """A trait for an (ip, port) tuple.
1806 1812
1807 1813 This allows for both IPv4 IP addresses as well as hostnames.
1808 1814 """
1809 1815
1810 1816 default_value = ('127.0.0.1', 0)
1811 1817 info_text = 'an (ip, port) tuple'
1812 1818
1813 1819 def validate(self, obj, value):
1814 1820 if isinstance(value, tuple):
1815 1821 if len(value) == 2:
1816 1822 if isinstance(value[0], py3compat.string_types) and isinstance(value[1], int):
1817 1823 port = value[1]
1818 1824 if port >= 0 and port <= 65535:
1819 1825 return value
1820 1826 self.error(obj, value)
1821 1827
1822 1828 class CRegExp(TraitType):
1823 1829 """A casting compiled regular expression trait.
1824 1830
1825 1831 Accepts both strings and compiled regular expressions. The resulting
1826 1832 attribute will be a compiled regular expression."""
1827 1833
1828 1834 info_text = 'a regular expression'
1829 1835
1830 1836 def validate(self, obj, value):
1831 1837 try:
1832 1838 return re.compile(value)
1833 1839 except:
1834 1840 self.error(obj, value)
General Comments 0
You need to be logged in to leave comments. Login now