##// END OF EJS Templates
Add EventfulList and EventfulDict trait types.
Jonathan Frederic -
Show More
@@ -0,0 +1,289
1 """Contains eventful dict and list implementations."""
2
3 class EventfulDict(dict):
4 """Eventful dictionary.
5
6 This class inherits from the Python intrinsic dictionary class, dict. It
7 adds events to the get, set, and del actions and optionally allows you to
8 intercept and cancel these actions. The eventfulness isn't recursive. In
9 other words, if you add a dict as a child, the events of that dict won't be
10 listened to. If you find you need something recursive, listen to the `add`
11 and `set` methods, and then cancel `dict` values from being set, and instead
12 set `EventfulDict`s that wrap those `dict`s. Then you can wire the events
13 to the same handlers if necessary.
14
15 See the on_add, on_set, and on_del methods for registering an event
16 handler."""
17
18 def __init__(self, *args, **kwargs):
19 """Public constructor"""
20 self._add_callback = None
21 self._del_callback = None
22 self._set_callback = None
23 dict.__init__(self, *args, **kwargs)
24
25 def on_add(self, callback):
26 """Register a callback for when an item is added to the dict.
27
28 Allows the listener to detect when items are added to the dictionary and
29 optionally cancel the addition.
30
31 callback: callable or None
32 If you want to ignore the addition event, pass None as the callback.
33 The callback should have a signature of callback(key, value). The
34 callback should return a boolean True if the additon should be
35 canceled, False or None otherwise."""
36 self._add_callback = callback
37
38 def on_del(self, callback):
39 """Register a callback for when an item is deleted from the dict.
40
41 Allows the listener to detect when items are deleted from the dictionary
42 and optionally cancel the deletion.
43
44 callback: callable or None
45 If you want to ignore the deletion event, pass None as the callback.
46 The callback should have a signature of callback(key). The
47 callback should return a boolean True if the deletion should be
48 canceled, False or None otherwise."""
49 self._del_callback = callback
50
51 def on_set(self, callback):
52 """Register a callback for when an item is changed in the dict.
53
54 Allows the listener to detect when items are changed in the dictionary
55 and optionally cancel the change.
56
57 callback: callable or None
58 If you want to ignore the change event, pass None as the callback.
59 The callback should have a signature of callback(key, value). The
60 callback should return a boolean True if the change should be
61 canceled, False or None otherwise."""
62 self._set_callback = callback
63
64 def _can_add(self, key, value):
65 """Check if the item can be added to the dict."""
66 if callable(self._add_callback):
67 return not bool(self._add_callback(key, value))
68 else:
69 return True
70
71 def _can_del(self, key):
72 """Check if the item can be deleted from the dict."""
73 if callable(self._del_callback):
74 return not bool(self._del_callback(key))
75 else:
76 return True
77
78 def _can_set(self, key, value):
79 """Check if the item can be changed in the dict."""
80 if callable(self._set_callback):
81 return not bool(self._set_callback(key, value))
82 else:
83 return True
84
85 def pop(self, key):
86 """Returns the value of an item in the dictionary and then deletes the
87 item from the dictionary."""
88 if self._can_del(key):
89 return dict.pop(self, key)
90 else:
91 raise Exception('Cannot `pop`, deletion of key "{}" failed.'.format(key))
92
93 def popitem(self):
94 """Pop the next key/value pair from the dictionary."""
95 key = dict.keys(self)[0]
96 return key, self.pop(key)
97
98 def update(self, other_dict):
99 """Copy the key/value pairs from another dictionary into this dictionary,
100 overwriting any conflicting keys in this dictionary."""
101 for (key, value) in other_dict.items():
102 self[key] = value
103
104 def clear(self):
105 """Clear the dictionary."""
106 for key in list(self.keys()):
107 del self[key]
108
109 def __setitem__(self, key, value):
110 if (key in self and self._can_set(key, value)) or \
111 (key not in self and self._can_add(key, value)):
112 return dict.__setitem__(self, key, value)
113
114 def __delitem__(self, key):
115 if self._can_del(key):
116 return dict.__delitem__(self, key)
117
118
119 class EventfulList(list):
120 """Eventful list.
121
122 This class inherits from the Python intrinsic `list` class. It adds events
123 that allow you to listen for actions that modify the list. You can
124 optionally cancel the actions.
125
126 See the on_del, on_set, on_insert, on_sort, and on_reverse methods for
127 registering an event handler.
128
129 Some of the method docstrings were taken from the Python documentation at
130 https://docs.python.org/2/tutorial/datastructures.html"""
131
132 def __init__(self, *pargs, **kwargs):
133 """Public constructor"""
134 self._insert_callback = None
135 self._set_callback = None
136 self._del_callback = None
137 self._sort_callback = None
138 self._reverse_callback = None
139 list.__init__(self, *pargs, **kwargs)
140
141 def on_insert(self, callback):
142 """Register a callback for when an item is inserted into the list.
143
144 Allows the listener to detect when items are inserted into the list and
145 optionally cancel the insertion.
146
147 callback: callable or None
148 If you want to ignore the insertion event, pass None as the callback.
149 The callback should have a signature of callback(index, value). The
150 callback should return a boolean True if the insertion should be
151 canceled, False or None otherwise."""
152 self._insert_callback = callback
153
154 def on_del(self, callback):
155 """Register a callback for item deletion.
156
157 Allows the listener to detect when items are deleted from the list and
158 optionally cancel the deletion.
159
160 callback: callable or None
161 If you want to ignore the deletion event, pass None as the callback.
162 The callback should have a signature of callback(index). The
163 callback should return a boolean True if the deletion should be
164 canceled, False or None otherwise."""
165 self._del_callback = callback
166
167 def on_set(self, callback):
168 """Register a callback for items are set.
169
170 Allows the listener to detect when items are set and optionally cancel
171 the setting. Note, `set` is also called when one or more items are
172 added to the end of the list.
173
174 callback: callable or None
175 If you want to ignore the set event, pass None as the callback.
176 The callback should have a signature of callback(index, value). The
177 callback should return a boolean True if the set should be
178 canceled, False or None otherwise."""
179 self._set_callback = callback
180
181 def on_reverse(self, callback):
182 """Register a callback for list reversal.
183
184 callback: callable or None
185 If you want to ignore the reverse event, pass None as the callback.
186 The callback should have a signature of callback(). The
187 callback should return a boolean True if the reverse should be
188 canceled, False or None otherwise."""
189 self._reverse_callback = callback
190
191 def on_sort(self, callback):
192 """Register a callback for sortting of the list.
193
194 callback: callable or None
195 If you want to ignore the sort event, pass None as the callback.
196 The callback signature should match that of Python list's `.sort`
197 method or `callback(*pargs, **kwargs)` as a catch all. The callback
198 should return a boolean True if the reverse should be canceled,
199 False or None otherwise."""
200 self._sort_callback = callback
201
202 def _can_insert(self, index, value):
203 """Check if the item can be inserted."""
204 if callable(self._insert_callback):
205 return not bool(self._insert_callback(index, value))
206 else:
207 return True
208
209 def _can_del(self, index):
210 """Check if the item can be deleted."""
211 if callable(self._del_callback):
212 return not bool(self._del_callback(index))
213 else:
214 return True
215
216 def _can_set(self, index, value):
217 """Check if the item can be set."""
218 if callable(self._set_callback):
219 return not bool(self._set_callback(index, value))
220 else:
221 return True
222
223 def _can_reverse(self):
224 """Check if the list can be reversed."""
225 if callable(self._reverse_callback):
226 return not bool(self._reverse_callback())
227 else:
228 return True
229
230 def _can_sort(self, *pargs, **kwargs):
231 """Check if the list can be sorted."""
232 if callable(self._sort_callback):
233 return not bool(self._sort_callback(*pargs, **kwargs))
234 else:
235 return True
236
237 def append(self, x):
238 """Add an item to the end of the list."""
239 self[len(self):] = [x]
240
241 def extend(self, L):
242 """Extend the list by appending all the items in the given list."""
243 self[len(self):] = L
244
245 def remove(self, x):
246 """Remove the first item from the list whose value is x. It is an error
247 if there is no such item."""
248 del self[self.index(x)]
249
250 def pop(self, i=None):
251 """Remove the item at the given position in the list, and return it. If
252 no index is specified, a.pop() removes and returns the last item in the
253 list."""
254 if i is None:
255 i = len(self) - 1
256 val = self[i]
257 del self[i]
258 return val
259
260 def reverse(self):
261 """Reverse the elements of the list, in place."""
262 if self._can_reverse():
263 list.reverse(self)
264
265 def insert(self, index, value):
266 """Insert an item at a given position. The first argument is the index
267 of the element before which to insert, so a.insert(0, x) inserts at the
268 front of the list, and a.insert(len(a), x) is equivalent to
269 a.append(x)."""
270 if self._can_insert(index, value):
271 list.insert(self, index, value)
272
273 def sort(self, *pargs, **kwargs):
274 """Sort the items of the list in place (the arguments can be used for
275 sort customization, see Python's sorted() for their explanation)."""
276 if self._can_sort(*pargs, **kwargs):
277 list.sort(self, *pargs, **kwargs)
278
279 def __delitem__(self, index):
280 if self._can_del(index):
281 list.__delitem__(self, index)
282
283 def __setitem__(self, index, value):
284 if self._can_set(index, value):
285 list.__setitem__(self, index, value)
286
287 def __setslice__(self, start, end, value):
288 if self._can_set(slice(start, end), value):
289 list.__setslice__(self, start, end, value)
@@ -1,1174 +1,1253
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, CBytes, Dict,
20 20 Int, Long, Integer, Float, Complex, Bytes, Unicode, TraitError,
21 21 Undefined, Type, This, Instance, TCPAddress, List, Tuple,
22 ObjectName, DottedObjectName, CRegExp, link
22 ObjectName, DottedObjectName, CRegExp, link, EventfulList, EventfulDict
23 23 )
24 24 from IPython.utils import py3compat
25 25 from IPython.testing.decorators import skipif
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Helper classes for testing
29 29 #-----------------------------------------------------------------------------
30 30
31 31
32 32 class HasTraitsStub(HasTraits):
33 33
34 34 def _notify_trait(self, name, old, new):
35 35 self._notify_name = name
36 36 self._notify_old = old
37 37 self._notify_new = new
38 38
39 39
40 40 #-----------------------------------------------------------------------------
41 41 # Test classes
42 42 #-----------------------------------------------------------------------------
43 43
44 44
45 45 class TestTraitType(TestCase):
46 46
47 47 def test_get_undefined(self):
48 48 class A(HasTraits):
49 49 a = TraitType
50 50 a = A()
51 51 self.assertEqual(a.a, Undefined)
52 52
53 53 def test_set(self):
54 54 class A(HasTraitsStub):
55 55 a = TraitType
56 56
57 57 a = A()
58 58 a.a = 10
59 59 self.assertEqual(a.a, 10)
60 60 self.assertEqual(a._notify_name, 'a')
61 61 self.assertEqual(a._notify_old, Undefined)
62 62 self.assertEqual(a._notify_new, 10)
63 63
64 64 def test_validate(self):
65 65 class MyTT(TraitType):
66 66 def validate(self, inst, value):
67 67 return -1
68 68 class A(HasTraitsStub):
69 69 tt = MyTT
70 70
71 71 a = A()
72 72 a.tt = 10
73 73 self.assertEqual(a.tt, -1)
74 74
75 75 def test_default_validate(self):
76 76 class MyIntTT(TraitType):
77 77 def validate(self, obj, value):
78 78 if isinstance(value, int):
79 79 return value
80 80 self.error(obj, value)
81 81 class A(HasTraits):
82 82 tt = MyIntTT(10)
83 83 a = A()
84 84 self.assertEqual(a.tt, 10)
85 85
86 86 # Defaults are validated when the HasTraits is instantiated
87 87 class B(HasTraits):
88 88 tt = MyIntTT('bad default')
89 89 self.assertRaises(TraitError, B)
90 90
91 91 def test_is_valid_for(self):
92 92 class MyTT(TraitType):
93 93 def is_valid_for(self, value):
94 94 return True
95 95 class A(HasTraits):
96 96 tt = MyTT
97 97
98 98 a = A()
99 99 a.tt = 10
100 100 self.assertEqual(a.tt, 10)
101 101
102 102 def test_value_for(self):
103 103 class MyTT(TraitType):
104 104 def value_for(self, value):
105 105 return 20
106 106 class A(HasTraits):
107 107 tt = MyTT
108 108
109 109 a = A()
110 110 a.tt = 10
111 111 self.assertEqual(a.tt, 20)
112 112
113 113 def test_info(self):
114 114 class A(HasTraits):
115 115 tt = TraitType
116 116 a = A()
117 117 self.assertEqual(A.tt.info(), 'any value')
118 118
119 119 def test_error(self):
120 120 class A(HasTraits):
121 121 tt = TraitType
122 122 a = A()
123 123 self.assertRaises(TraitError, A.tt.error, a, 10)
124 124
125 125 def test_dynamic_initializer(self):
126 126 class A(HasTraits):
127 127 x = Int(10)
128 128 def _x_default(self):
129 129 return 11
130 130 class B(A):
131 131 x = Int(20)
132 132 class C(A):
133 133 def _x_default(self):
134 134 return 21
135 135
136 136 a = A()
137 137 self.assertEqual(a._trait_values, {})
138 138 self.assertEqual(list(a._trait_dyn_inits.keys()), ['x'])
139 139 self.assertEqual(a.x, 11)
140 140 self.assertEqual(a._trait_values, {'x': 11})
141 141 b = B()
142 142 self.assertEqual(b._trait_values, {'x': 20})
143 143 self.assertEqual(list(a._trait_dyn_inits.keys()), ['x'])
144 144 self.assertEqual(b.x, 20)
145 145 c = C()
146 146 self.assertEqual(c._trait_values, {})
147 147 self.assertEqual(list(a._trait_dyn_inits.keys()), ['x'])
148 148 self.assertEqual(c.x, 21)
149 149 self.assertEqual(c._trait_values, {'x': 21})
150 150 # Ensure that the base class remains unmolested when the _default
151 151 # initializer gets overridden in a subclass.
152 152 a = A()
153 153 c = C()
154 154 self.assertEqual(a._trait_values, {})
155 155 self.assertEqual(list(a._trait_dyn_inits.keys()), ['x'])
156 156 self.assertEqual(a.x, 11)
157 157 self.assertEqual(a._trait_values, {'x': 11})
158 158
159 159
160 160
161 161 class TestHasTraitsMeta(TestCase):
162 162
163 163 def test_metaclass(self):
164 164 self.assertEqual(type(HasTraits), MetaHasTraits)
165 165
166 166 class A(HasTraits):
167 167 a = Int
168 168
169 169 a = A()
170 170 self.assertEqual(type(a.__class__), MetaHasTraits)
171 171 self.assertEqual(a.a,0)
172 172 a.a = 10
173 173 self.assertEqual(a.a,10)
174 174
175 175 class B(HasTraits):
176 176 b = Int()
177 177
178 178 b = B()
179 179 self.assertEqual(b.b,0)
180 180 b.b = 10
181 181 self.assertEqual(b.b,10)
182 182
183 183 class C(HasTraits):
184 184 c = Int(30)
185 185
186 186 c = C()
187 187 self.assertEqual(c.c,30)
188 188 c.c = 10
189 189 self.assertEqual(c.c,10)
190 190
191 191 def test_this_class(self):
192 192 class A(HasTraits):
193 193 t = This()
194 194 tt = This()
195 195 class B(A):
196 196 tt = This()
197 197 ttt = This()
198 198 self.assertEqual(A.t.this_class, A)
199 199 self.assertEqual(B.t.this_class, A)
200 200 self.assertEqual(B.tt.this_class, B)
201 201 self.assertEqual(B.ttt.this_class, B)
202 202
203 203 class TestHasTraitsNotify(TestCase):
204 204
205 205 def setUp(self):
206 206 self._notify1 = []
207 207 self._notify2 = []
208 208
209 209 def notify1(self, name, old, new):
210 210 self._notify1.append((name, old, new))
211 211
212 212 def notify2(self, name, old, new):
213 213 self._notify2.append((name, old, new))
214 214
215 215 def test_notify_all(self):
216 216
217 217 class A(HasTraits):
218 218 a = Int
219 219 b = Float
220 220
221 221 a = A()
222 222 a.on_trait_change(self.notify1)
223 223 a.a = 0
224 224 self.assertEqual(len(self._notify1),0)
225 225 a.b = 0.0
226 226 self.assertEqual(len(self._notify1),0)
227 227 a.a = 10
228 228 self.assertTrue(('a',0,10) in self._notify1)
229 229 a.b = 10.0
230 230 self.assertTrue(('b',0.0,10.0) in self._notify1)
231 231 self.assertRaises(TraitError,setattr,a,'a','bad string')
232 232 self.assertRaises(TraitError,setattr,a,'b','bad string')
233 233 self._notify1 = []
234 234 a.on_trait_change(self.notify1,remove=True)
235 235 a.a = 20
236 236 a.b = 20.0
237 237 self.assertEqual(len(self._notify1),0)
238 238
239 239 def test_notify_one(self):
240 240
241 241 class A(HasTraits):
242 242 a = Int
243 243 b = Float
244 244
245 245 a = A()
246 246 a.on_trait_change(self.notify1, 'a')
247 247 a.a = 0
248 248 self.assertEqual(len(self._notify1),0)
249 249 a.a = 10
250 250 self.assertTrue(('a',0,10) in self._notify1)
251 251 self.assertRaises(TraitError,setattr,a,'a','bad string')
252 252
253 253 def test_subclass(self):
254 254
255 255 class A(HasTraits):
256 256 a = Int
257 257
258 258 class B(A):
259 259 b = Float
260 260
261 261 b = B()
262 262 self.assertEqual(b.a,0)
263 263 self.assertEqual(b.b,0.0)
264 264 b.a = 100
265 265 b.b = 100.0
266 266 self.assertEqual(b.a,100)
267 267 self.assertEqual(b.b,100.0)
268 268
269 269 def test_notify_subclass(self):
270 270
271 271 class A(HasTraits):
272 272 a = Int
273 273
274 274 class B(A):
275 275 b = Float
276 276
277 277 b = B()
278 278 b.on_trait_change(self.notify1, 'a')
279 279 b.on_trait_change(self.notify2, 'b')
280 280 b.a = 0
281 281 b.b = 0.0
282 282 self.assertEqual(len(self._notify1),0)
283 283 self.assertEqual(len(self._notify2),0)
284 284 b.a = 10
285 285 b.b = 10.0
286 286 self.assertTrue(('a',0,10) in self._notify1)
287 287 self.assertTrue(('b',0.0,10.0) in self._notify2)
288 288
289 289 def test_static_notify(self):
290 290
291 291 class A(HasTraits):
292 292 a = Int
293 293 _notify1 = []
294 294 def _a_changed(self, name, old, new):
295 295 self._notify1.append((name, old, new))
296 296
297 297 a = A()
298 298 a.a = 0
299 299 # This is broken!!!
300 300 self.assertEqual(len(a._notify1),0)
301 301 a.a = 10
302 302 self.assertTrue(('a',0,10) in a._notify1)
303 303
304 304 class B(A):
305 305 b = Float
306 306 _notify2 = []
307 307 def _b_changed(self, name, old, new):
308 308 self._notify2.append((name, old, new))
309 309
310 310 b = B()
311 311 b.a = 10
312 312 b.b = 10.0
313 313 self.assertTrue(('a',0,10) in b._notify1)
314 314 self.assertTrue(('b',0.0,10.0) in b._notify2)
315 315
316 316 def test_notify_args(self):
317 317
318 318 def callback0():
319 319 self.cb = ()
320 320 def callback1(name):
321 321 self.cb = (name,)
322 322 def callback2(name, new):
323 323 self.cb = (name, new)
324 324 def callback3(name, old, new):
325 325 self.cb = (name, old, new)
326 326
327 327 class A(HasTraits):
328 328 a = Int
329 329
330 330 a = A()
331 331 a.on_trait_change(callback0, 'a')
332 332 a.a = 10
333 333 self.assertEqual(self.cb,())
334 334 a.on_trait_change(callback0, 'a', remove=True)
335 335
336 336 a.on_trait_change(callback1, 'a')
337 337 a.a = 100
338 338 self.assertEqual(self.cb,('a',))
339 339 a.on_trait_change(callback1, 'a', remove=True)
340 340
341 341 a.on_trait_change(callback2, 'a')
342 342 a.a = 1000
343 343 self.assertEqual(self.cb,('a',1000))
344 344 a.on_trait_change(callback2, 'a', remove=True)
345 345
346 346 a.on_trait_change(callback3, 'a')
347 347 a.a = 10000
348 348 self.assertEqual(self.cb,('a',1000,10000))
349 349 a.on_trait_change(callback3, 'a', remove=True)
350 350
351 351 self.assertEqual(len(a._trait_notifiers['a']),0)
352 352
353 353 def test_notify_only_once(self):
354 354
355 355 class A(HasTraits):
356 356 listen_to = ['a']
357 357
358 358 a = Int(0)
359 359 b = 0
360 360
361 361 def __init__(self, **kwargs):
362 362 super(A, self).__init__(**kwargs)
363 363 self.on_trait_change(self.listener1, ['a'])
364 364
365 365 def listener1(self, name, old, new):
366 366 self.b += 1
367 367
368 368 class B(A):
369 369
370 370 c = 0
371 371 d = 0
372 372
373 373 def __init__(self, **kwargs):
374 374 super(B, self).__init__(**kwargs)
375 375 self.on_trait_change(self.listener2)
376 376
377 377 def listener2(self, name, old, new):
378 378 self.c += 1
379 379
380 380 def _a_changed(self, name, old, new):
381 381 self.d += 1
382 382
383 383 b = B()
384 384 b.a += 1
385 385 self.assertEqual(b.b, b.c)
386 386 self.assertEqual(b.b, b.d)
387 387 b.a += 1
388 388 self.assertEqual(b.b, b.c)
389 389 self.assertEqual(b.b, b.d)
390 390
391 391
392 392 class TestHasTraits(TestCase):
393 393
394 394 def test_trait_names(self):
395 395 class A(HasTraits):
396 396 i = Int
397 397 f = Float
398 398 a = A()
399 399 self.assertEqual(sorted(a.trait_names()),['f','i'])
400 400 self.assertEqual(sorted(A.class_trait_names()),['f','i'])
401 401
402 402 def test_trait_metadata(self):
403 403 class A(HasTraits):
404 404 i = Int(config_key='MY_VALUE')
405 405 a = A()
406 406 self.assertEqual(a.trait_metadata('i','config_key'), 'MY_VALUE')
407 407
408 408 def test_traits(self):
409 409 class A(HasTraits):
410 410 i = Int
411 411 f = Float
412 412 a = A()
413 413 self.assertEqual(a.traits(), dict(i=A.i, f=A.f))
414 414 self.assertEqual(A.class_traits(), dict(i=A.i, f=A.f))
415 415
416 416 def test_traits_metadata(self):
417 417 class A(HasTraits):
418 418 i = Int(config_key='VALUE1', other_thing='VALUE2')
419 419 f = Float(config_key='VALUE3', other_thing='VALUE2')
420 420 j = Int(0)
421 421 a = A()
422 422 self.assertEqual(a.traits(), dict(i=A.i, f=A.f, j=A.j))
423 423 traits = a.traits(config_key='VALUE1', other_thing='VALUE2')
424 424 self.assertEqual(traits, dict(i=A.i))
425 425
426 426 # This passes, but it shouldn't because I am replicating a bug in
427 427 # traits.
428 428 traits = a.traits(config_key=lambda v: True)
429 429 self.assertEqual(traits, dict(i=A.i, f=A.f, j=A.j))
430 430
431 431 def test_init(self):
432 432 class A(HasTraits):
433 433 i = Int()
434 434 x = Float()
435 435 a = A(i=1, x=10.0)
436 436 self.assertEqual(a.i, 1)
437 437 self.assertEqual(a.x, 10.0)
438 438
439 439 def test_positional_args(self):
440 440 class A(HasTraits):
441 441 i = Int(0)
442 442 def __init__(self, i):
443 443 super(A, self).__init__()
444 444 self.i = i
445 445
446 446 a = A(5)
447 447 self.assertEqual(a.i, 5)
448 448 # should raise TypeError if no positional arg given
449 449 self.assertRaises(TypeError, A)
450 450
451 451 #-----------------------------------------------------------------------------
452 452 # Tests for specific trait types
453 453 #-----------------------------------------------------------------------------
454 454
455 455
456 456 class TestType(TestCase):
457 457
458 458 def test_default(self):
459 459
460 460 class B(object): pass
461 461 class A(HasTraits):
462 462 klass = Type
463 463
464 464 a = A()
465 465 self.assertEqual(a.klass, None)
466 466
467 467 a.klass = B
468 468 self.assertEqual(a.klass, B)
469 469 self.assertRaises(TraitError, setattr, a, 'klass', 10)
470 470
471 471 def test_value(self):
472 472
473 473 class B(object): pass
474 474 class C(object): pass
475 475 class A(HasTraits):
476 476 klass = Type(B)
477 477
478 478 a = A()
479 479 self.assertEqual(a.klass, B)
480 480 self.assertRaises(TraitError, setattr, a, 'klass', C)
481 481 self.assertRaises(TraitError, setattr, a, 'klass', object)
482 482 a.klass = B
483 483
484 484 def test_allow_none(self):
485 485
486 486 class B(object): pass
487 487 class C(B): pass
488 488 class A(HasTraits):
489 489 klass = Type(B, allow_none=False)
490 490
491 491 a = A()
492 492 self.assertEqual(a.klass, B)
493 493 self.assertRaises(TraitError, setattr, a, 'klass', None)
494 494 a.klass = C
495 495 self.assertEqual(a.klass, C)
496 496
497 497 def test_validate_klass(self):
498 498
499 499 class A(HasTraits):
500 500 klass = Type('no strings allowed')
501 501
502 502 self.assertRaises(ImportError, A)
503 503
504 504 class A(HasTraits):
505 505 klass = Type('rub.adub.Duck')
506 506
507 507 self.assertRaises(ImportError, A)
508 508
509 509 def test_validate_default(self):
510 510
511 511 class B(object): pass
512 512 class A(HasTraits):
513 513 klass = Type('bad default', B)
514 514
515 515 self.assertRaises(ImportError, A)
516 516
517 517 class C(HasTraits):
518 518 klass = Type(None, B, allow_none=False)
519 519
520 520 self.assertRaises(TraitError, C)
521 521
522 522 def test_str_klass(self):
523 523
524 524 class A(HasTraits):
525 525 klass = Type('IPython.utils.ipstruct.Struct')
526 526
527 527 from IPython.utils.ipstruct import Struct
528 528 a = A()
529 529 a.klass = Struct
530 530 self.assertEqual(a.klass, Struct)
531 531
532 532 self.assertRaises(TraitError, setattr, a, 'klass', 10)
533 533
534 534 def test_set_str_klass(self):
535 535
536 536 class A(HasTraits):
537 537 klass = Type()
538 538
539 539 a = A(klass='IPython.utils.ipstruct.Struct')
540 540 from IPython.utils.ipstruct import Struct
541 541 self.assertEqual(a.klass, Struct)
542 542
543 543 class TestInstance(TestCase):
544 544
545 545 def test_basic(self):
546 546 class Foo(object): pass
547 547 class Bar(Foo): pass
548 548 class Bah(object): pass
549 549
550 550 class A(HasTraits):
551 551 inst = Instance(Foo)
552 552
553 553 a = A()
554 554 self.assertTrue(a.inst is None)
555 555 a.inst = Foo()
556 556 self.assertTrue(isinstance(a.inst, Foo))
557 557 a.inst = Bar()
558 558 self.assertTrue(isinstance(a.inst, Foo))
559 559 self.assertRaises(TraitError, setattr, a, 'inst', Foo)
560 560 self.assertRaises(TraitError, setattr, a, 'inst', Bar)
561 561 self.assertRaises(TraitError, setattr, a, 'inst', Bah())
562 562
563 563 def test_default_klass(self):
564 564 class Foo(object): pass
565 565 class Bar(Foo): pass
566 566 class Bah(object): pass
567 567
568 568 class FooInstance(Instance):
569 569 klass = Foo
570 570
571 571 class A(HasTraits):
572 572 inst = FooInstance()
573 573
574 574 a = A()
575 575 self.assertTrue(a.inst is None)
576 576 a.inst = Foo()
577 577 self.assertTrue(isinstance(a.inst, Foo))
578 578 a.inst = Bar()
579 579 self.assertTrue(isinstance(a.inst, Foo))
580 580 self.assertRaises(TraitError, setattr, a, 'inst', Foo)
581 581 self.assertRaises(TraitError, setattr, a, 'inst', Bar)
582 582 self.assertRaises(TraitError, setattr, a, 'inst', Bah())
583 583
584 584 def test_unique_default_value(self):
585 585 class Foo(object): pass
586 586 class A(HasTraits):
587 587 inst = Instance(Foo,(),{})
588 588
589 589 a = A()
590 590 b = A()
591 591 self.assertTrue(a.inst is not b.inst)
592 592
593 593 def test_args_kw(self):
594 594 class Foo(object):
595 595 def __init__(self, c): self.c = c
596 596 class Bar(object): pass
597 597 class Bah(object):
598 598 def __init__(self, c, d):
599 599 self.c = c; self.d = d
600 600
601 601 class A(HasTraits):
602 602 inst = Instance(Foo, (10,))
603 603 a = A()
604 604 self.assertEqual(a.inst.c, 10)
605 605
606 606 class B(HasTraits):
607 607 inst = Instance(Bah, args=(10,), kw=dict(d=20))
608 608 b = B()
609 609 self.assertEqual(b.inst.c, 10)
610 610 self.assertEqual(b.inst.d, 20)
611 611
612 612 class C(HasTraits):
613 613 inst = Instance(Foo)
614 614 c = C()
615 615 self.assertTrue(c.inst is None)
616 616
617 617 def test_bad_default(self):
618 618 class Foo(object): pass
619 619
620 620 class A(HasTraits):
621 621 inst = Instance(Foo, allow_none=False)
622 622
623 623 self.assertRaises(TraitError, A)
624 624
625 625 def test_instance(self):
626 626 class Foo(object): pass
627 627
628 628 def inner():
629 629 class A(HasTraits):
630 630 inst = Instance(Foo())
631 631
632 632 self.assertRaises(TraitError, inner)
633 633
634 634
635 635 class TestThis(TestCase):
636 636
637 637 def test_this_class(self):
638 638 class Foo(HasTraits):
639 639 this = This
640 640
641 641 f = Foo()
642 642 self.assertEqual(f.this, None)
643 643 g = Foo()
644 644 f.this = g
645 645 self.assertEqual(f.this, g)
646 646 self.assertRaises(TraitError, setattr, f, 'this', 10)
647 647
648 648 def test_this_inst(self):
649 649 class Foo(HasTraits):
650 650 this = This()
651 651
652 652 f = Foo()
653 653 f.this = Foo()
654 654 self.assertTrue(isinstance(f.this, Foo))
655 655
656 656 def test_subclass(self):
657 657 class Foo(HasTraits):
658 658 t = This()
659 659 class Bar(Foo):
660 660 pass
661 661 f = Foo()
662 662 b = Bar()
663 663 f.t = b
664 664 b.t = f
665 665 self.assertEqual(f.t, b)
666 666 self.assertEqual(b.t, f)
667 667
668 668 def test_subclass_override(self):
669 669 class Foo(HasTraits):
670 670 t = This()
671 671 class Bar(Foo):
672 672 t = This()
673 673 f = Foo()
674 674 b = Bar()
675 675 f.t = b
676 676 self.assertEqual(f.t, b)
677 677 self.assertRaises(TraitError, setattr, b, 't', f)
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
742 742 class IntTrait(HasTraits):
743 743
744 744 value = Int(99)
745 745
746 746 class TestInt(TraitTestBase):
747 747
748 748 obj = IntTrait()
749 749 _default_value = 99
750 750 _good_values = [10, -10]
751 751 _bad_values = ['ten', u'ten', [10], {'ten': 10},(10,), None, 1j,
752 752 10.1, -10.1, '10L', '-10L', '10.1', '-10.1', u'10L',
753 753 u'-10L', u'10.1', u'-10.1', '10', '-10', u'10', u'-10']
754 754 if not py3compat.PY3:
755 755 _bad_values.extend([long(10), long(-10), 10*sys.maxint, -10*sys.maxint])
756 756
757 757
758 758 class LongTrait(HasTraits):
759 759
760 760 value = Long(99 if py3compat.PY3 else long(99))
761 761
762 762 class TestLong(TraitTestBase):
763 763
764 764 obj = LongTrait()
765 765
766 766 _default_value = 99 if py3compat.PY3 else long(99)
767 767 _good_values = [10, -10]
768 768 _bad_values = ['ten', u'ten', [10], {'ten': 10},(10,),
769 769 None, 1j, 10.1, -10.1, '10', '-10', '10L', '-10L', '10.1',
770 770 '-10.1', u'10', u'-10', u'10L', u'-10L', u'10.1',
771 771 u'-10.1']
772 772 if not py3compat.PY3:
773 773 # maxint undefined on py3, because int == long
774 774 _good_values.extend([long(10), long(-10), 10*sys.maxint, -10*sys.maxint])
775 775 _bad_values.extend([[long(10)], (long(10),)])
776 776
777 777 @skipif(py3compat.PY3, "not relevant on py3")
778 778 def test_cast_small(self):
779 779 """Long casts ints to long"""
780 780 self.obj.value = 10
781 781 self.assertEqual(type(self.obj.value), long)
782 782
783 783
784 784 class IntegerTrait(HasTraits):
785 785 value = Integer(1)
786 786
787 787 class TestInteger(TestLong):
788 788 obj = IntegerTrait()
789 789 _default_value = 1
790 790
791 791 def coerce(self, n):
792 792 return int(n)
793 793
794 794 @skipif(py3compat.PY3, "not relevant on py3")
795 795 def test_cast_small(self):
796 796 """Integer casts small longs to int"""
797 797 if py3compat.PY3:
798 798 raise SkipTest("not relevant on py3")
799 799
800 800 self.obj.value = long(100)
801 801 self.assertEqual(type(self.obj.value), int)
802 802
803 803
804 804 class FloatTrait(HasTraits):
805 805
806 806 value = Float(99.0)
807 807
808 808 class TestFloat(TraitTestBase):
809 809
810 810 obj = FloatTrait()
811 811
812 812 _default_value = 99.0
813 813 _good_values = [10, -10, 10.1, -10.1]
814 814 _bad_values = ['ten', u'ten', [10], {'ten': 10},(10,), None,
815 815 1j, '10', '-10', '10L', '-10L', '10.1', '-10.1', u'10',
816 816 u'-10', u'10L', u'-10L', u'10.1', u'-10.1']
817 817 if not py3compat.PY3:
818 818 _bad_values.extend([long(10), long(-10)])
819 819
820 820
821 821 class ComplexTrait(HasTraits):
822 822
823 823 value = Complex(99.0-99.0j)
824 824
825 825 class TestComplex(TraitTestBase):
826 826
827 827 obj = ComplexTrait()
828 828
829 829 _default_value = 99.0-99.0j
830 830 _good_values = [10, -10, 10.1, -10.1, 10j, 10+10j, 10-10j,
831 831 10.1j, 10.1+10.1j, 10.1-10.1j]
832 832 _bad_values = [u'10L', u'-10L', 'ten', [10], {'ten': 10},(10,), None]
833 833 if not py3compat.PY3:
834 834 _bad_values.extend([long(10), long(-10)])
835 835
836 836
837 837 class BytesTrait(HasTraits):
838 838
839 839 value = Bytes(b'string')
840 840
841 841 class TestBytes(TraitTestBase):
842 842
843 843 obj = BytesTrait()
844 844
845 845 _default_value = b'string'
846 846 _good_values = [b'10', b'-10', b'10L',
847 847 b'-10L', b'10.1', b'-10.1', b'string']
848 848 _bad_values = [10, -10, 10.1, -10.1, 1j, [10],
849 849 ['ten'],{'ten': 10},(10,), None, u'string']
850 850 if not py3compat.PY3:
851 851 _bad_values.extend([long(10), long(-10)])
852 852
853 853
854 854 class UnicodeTrait(HasTraits):
855 855
856 856 value = Unicode(u'unicode')
857 857
858 858 class TestUnicode(TraitTestBase):
859 859
860 860 obj = UnicodeTrait()
861 861
862 862 _default_value = u'unicode'
863 863 _good_values = ['10', '-10', '10L', '-10L', '10.1',
864 864 '-10.1', '', u'', 'string', u'string', u"€"]
865 865 _bad_values = [10, -10, 10.1, -10.1, 1j,
866 866 [10], ['ten'], [u'ten'], {'ten': 10},(10,), None]
867 867 if not py3compat.PY3:
868 868 _bad_values.extend([long(10), long(-10)])
869 869
870 870
871 871 class ObjectNameTrait(HasTraits):
872 872 value = ObjectName("abc")
873 873
874 874 class TestObjectName(TraitTestBase):
875 875 obj = ObjectNameTrait()
876 876
877 877 _default_value = "abc"
878 878 _good_values = ["a", "gh", "g9", "g_", "_G", u"a345_"]
879 879 _bad_values = [1, "", u"€", "9g", "!", "#abc", "aj@", "a.b", "a()", "a[0]",
880 880 None, object(), object]
881 881 if sys.version_info[0] < 3:
882 882 _bad_values.append(u"þ")
883 883 else:
884 884 _good_values.append(u"þ") # þ=1 is valid in Python 3 (PEP 3131).
885 885
886 886
887 887 class DottedObjectNameTrait(HasTraits):
888 888 value = DottedObjectName("a.b")
889 889
890 890 class TestDottedObjectName(TraitTestBase):
891 891 obj = DottedObjectNameTrait()
892 892
893 893 _default_value = "a.b"
894 894 _good_values = ["A", "y.t", "y765.__repr__", "os.path.join", u"os.path.join"]
895 895 _bad_values = [1, u"abc.€", "_.@", ".", ".abc", "abc.", ".abc.", None]
896 896 if sys.version_info[0] < 3:
897 897 _bad_values.append(u"t.þ")
898 898 else:
899 899 _good_values.append(u"t.þ")
900 900
901 901
902 902 class TCPAddressTrait(HasTraits):
903 903
904 904 value = TCPAddress()
905 905
906 906 class TestTCPAddress(TraitTestBase):
907 907
908 908 obj = TCPAddressTrait()
909 909
910 910 _default_value = ('127.0.0.1',0)
911 911 _good_values = [('localhost',0),('192.168.0.1',1000),('www.google.com',80)]
912 912 _bad_values = [(0,0),('localhost',10.0),('localhost',-1), None]
913 913
914 914 class ListTrait(HasTraits):
915 915
916 916 value = List(Int)
917 917
918 918 class TestList(TraitTestBase):
919 919
920 920 obj = ListTrait()
921 921
922 922 _default_value = []
923 923 _good_values = [[], [1], list(range(10)), (1,2)]
924 924 _bad_values = [10, [1,'a'], 'a']
925 925
926 926 def coerce(self, value):
927 927 if value is not None:
928 928 value = list(value)
929 929 return value
930 930
931 931 class Foo(object):
932 932 pass
933 933
934 934 class InstanceListTrait(HasTraits):
935 935
936 936 value = List(Instance(__name__+'.Foo'))
937 937
938 938 class TestInstanceList(TraitTestBase):
939 939
940 940 obj = InstanceListTrait()
941 941
942 942 def test_klass(self):
943 943 """Test that the instance klass is properly assigned."""
944 944 self.assertIs(self.obj.traits()['value']._trait.klass, Foo)
945 945
946 946 _default_value = []
947 947 _good_values = [[Foo(), Foo(), None], None]
948 948 _bad_values = [['1', 2,], '1', [Foo]]
949 949
950 950 class LenListTrait(HasTraits):
951 951
952 952 value = List(Int, [0], minlen=1, maxlen=2)
953 953
954 954 class TestLenList(TraitTestBase):
955 955
956 956 obj = LenListTrait()
957 957
958 958 _default_value = [0]
959 959 _good_values = [[1], [1,2], (1,2)]
960 960 _bad_values = [10, [1,'a'], 'a', [], list(range(3))]
961 961
962 962 def coerce(self, value):
963 963 if value is not None:
964 964 value = list(value)
965 965 return value
966 966
967 967 class TupleTrait(HasTraits):
968 968
969 969 value = Tuple(Int(allow_none=True))
970 970
971 971 class TestTupleTrait(TraitTestBase):
972 972
973 973 obj = TupleTrait()
974 974
975 975 _default_value = None
976 976 _good_values = [(1,), None, (0,), [1], (None,)]
977 977 _bad_values = [10, (1,2), ('a'), ()]
978 978
979 979 def coerce(self, value):
980 980 if value is not None:
981 981 value = tuple(value)
982 982 return value
983 983
984 984 def test_invalid_args(self):
985 985 self.assertRaises(TypeError, Tuple, 5)
986 986 self.assertRaises(TypeError, Tuple, default_value='hello')
987 987 t = Tuple(Int, CBytes, default_value=(1,5))
988 988
989 989 class LooseTupleTrait(HasTraits):
990 990
991 991 value = Tuple((1,2,3))
992 992
993 993 class TestLooseTupleTrait(TraitTestBase):
994 994
995 995 obj = LooseTupleTrait()
996 996
997 997 _default_value = (1,2,3)
998 998 _good_values = [(1,), None, [1], (0,), tuple(range(5)), tuple('hello'), ('a',5), ()]
999 999 _bad_values = [10, 'hello', {}]
1000 1000
1001 1001 def coerce(self, value):
1002 1002 if value is not None:
1003 1003 value = tuple(value)
1004 1004 return value
1005 1005
1006 1006 def test_invalid_args(self):
1007 1007 self.assertRaises(TypeError, Tuple, 5)
1008 1008 self.assertRaises(TypeError, Tuple, default_value='hello')
1009 1009 t = Tuple(Int, CBytes, default_value=(1,5))
1010 1010
1011 1011
1012 1012 class MultiTupleTrait(HasTraits):
1013 1013
1014 1014 value = Tuple(Int, Bytes, default_value=[99,b'bottles'])
1015 1015
1016 1016 class TestMultiTuple(TraitTestBase):
1017 1017
1018 1018 obj = MultiTupleTrait()
1019 1019
1020 1020 _default_value = (99,b'bottles')
1021 1021 _good_values = [(1,b'a'), (2,b'b')]
1022 1022 _bad_values = ((),10, b'a', (1,b'a',3), (b'a',1), (1, u'a'))
1023 1023
1024 1024 class CRegExpTrait(HasTraits):
1025 1025
1026 1026 value = CRegExp(r'')
1027 1027
1028 1028 class TestCRegExp(TraitTestBase):
1029 1029
1030 1030 def coerce(self, value):
1031 1031 return re.compile(value)
1032 1032
1033 1033 obj = CRegExpTrait()
1034 1034
1035 1035 _default_value = re.compile(r'')
1036 1036 _good_values = [r'\d+', re.compile(r'\d+')]
1037 _bad_values = [r'(', None, ()]
1037 _bad_values = ['(', None, ()]
1038 1038
1039 1039 class DictTrait(HasTraits):
1040 1040 value = Dict()
1041 1041
1042 1042 def test_dict_assignment():
1043 1043 d = dict()
1044 1044 c = DictTrait()
1045 1045 c.value = d
1046 1046 d['a'] = 5
1047 1047 nt.assert_equal(d, c.value)
1048 1048 nt.assert_true(c.value is d)
1049 1049
1050 1050 class TestLink(TestCase):
1051 1051 def test_connect_same(self):
1052 1052 """Verify two traitlets of the same type can be linked together using link."""
1053 1053
1054 1054 # Create two simple classes with Int traitlets.
1055 1055 class A(HasTraits):
1056 1056 value = Int()
1057 1057 a = A(value=9)
1058 1058 b = A(value=8)
1059 1059
1060 1060 # Conenct the two classes.
1061 1061 c = link((a, 'value'), (b, 'value'))
1062 1062
1063 1063 # Make sure the values are the same at the point of linking.
1064 1064 self.assertEqual(a.value, b.value)
1065 1065
1066 1066 # Change one of the values to make sure they stay in sync.
1067 1067 a.value = 5
1068 1068 self.assertEqual(a.value, b.value)
1069 1069 b.value = 6
1070 1070 self.assertEqual(a.value, b.value)
1071 1071
1072 1072 def test_link_different(self):
1073 1073 """Verify two traitlets of different types can be linked together using link."""
1074 1074
1075 1075 # Create two simple classes with Int traitlets.
1076 1076 class A(HasTraits):
1077 1077 value = Int()
1078 1078 class B(HasTraits):
1079 1079 count = Int()
1080 1080 a = A(value=9)
1081 1081 b = B(count=8)
1082 1082
1083 1083 # Conenct the two classes.
1084 1084 c = link((a, 'value'), (b, 'count'))
1085 1085
1086 1086 # Make sure the values are the same at the point of linking.
1087 1087 self.assertEqual(a.value, b.count)
1088 1088
1089 1089 # Change one of the values to make sure they stay in sync.
1090 1090 a.value = 5
1091 1091 self.assertEqual(a.value, b.count)
1092 1092 b.count = 4
1093 1093 self.assertEqual(a.value, b.count)
1094 1094
1095 1095 def test_unlink(self):
1096 1096 """Verify two linked traitlets can be unlinked."""
1097 1097
1098 1098 # Create two simple classes with Int traitlets.
1099 1099 class A(HasTraits):
1100 1100 value = Int()
1101 1101 a = A(value=9)
1102 1102 b = A(value=8)
1103 1103
1104 1104 # Connect the two classes.
1105 1105 c = link((a, 'value'), (b, 'value'))
1106 1106 a.value = 4
1107 1107 c.unlink()
1108 1108
1109 1109 # Change one of the values to make sure they don't stay in sync.
1110 1110 a.value = 5
1111 1111 self.assertNotEqual(a.value, b.value)
1112 1112
1113 1113 def test_callbacks(self):
1114 1114 """Verify two linked traitlets have their callbacks called once."""
1115 1115
1116 1116 # Create two simple classes with Int traitlets.
1117 1117 class A(HasTraits):
1118 1118 value = Int()
1119 1119 class B(HasTraits):
1120 1120 count = Int()
1121 1121 a = A(value=9)
1122 1122 b = B(count=8)
1123 1123
1124 1124 # Register callbacks that count.
1125 1125 callback_count = []
1126 1126 def a_callback(name, old, new):
1127 1127 callback_count.append('a')
1128 1128 a.on_trait_change(a_callback, 'value')
1129 1129 def b_callback(name, old, new):
1130 1130 callback_count.append('b')
1131 1131 b.on_trait_change(b_callback, 'count')
1132 1132
1133 1133 # Connect the two classes.
1134 1134 c = link((a, 'value'), (b, 'count'))
1135 1135
1136 1136 # Make sure b's count was set to a's value once.
1137 1137 self.assertEqual(''.join(callback_count), 'b')
1138 1138 del callback_count[:]
1139 1139
1140 1140 # Make sure a's value was set to b's count once.
1141 1141 b.count = 5
1142 1142 self.assertEqual(''.join(callback_count), 'ba')
1143 1143 del callback_count[:]
1144 1144
1145 1145 # Make sure b's count was set to a's value once.
1146 1146 a.value = 4
1147 1147 self.assertEqual(''.join(callback_count), 'ab')
1148 1148 del callback_count[:]
1149 1149
1150 1150 class Pickleable(HasTraits):
1151 1151 i = Int()
1152 1152 j = Int()
1153 1153
1154 1154 def _i_default(self):
1155 1155 return 1
1156 1156
1157 1157 def _i_changed(self, name, old, new):
1158 1158 self.j = new
1159 1159
1160 1160 def test_pickle_hastraits():
1161 1161 c = Pickleable()
1162 1162 for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
1163 1163 p = pickle.dumps(c, protocol)
1164 1164 c2 = pickle.loads(p)
1165 1165 nt.assert_equal(c2.i, c.i)
1166 1166 nt.assert_equal(c2.j, c.j)
1167 1167
1168 1168 c.i = 5
1169 1169 for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
1170 1170 p = pickle.dumps(c, protocol)
1171 1171 c2 = pickle.loads(p)
1172 1172 nt.assert_equal(c2.i, c.i)
1173 1173 nt.assert_equal(c2.j, c.j)
1174
1174
1175 class TestEventful(TestCase):
1176
1177 def test_list(self):
1178 """Does the EventfulList work?"""
1179 event_cache = []
1180
1181 class A(HasTraits):
1182 x = EventfulList([c for c in 'abc'])
1183 a = A()
1184
1185 def handle_insert(index, value):
1186 event_cache.append('insert')
1187 def handle_del(index):
1188 event_cache.append('del')
1189 def handle_set(index, value):
1190 event_cache.append('set')
1191 def handle_reverse():
1192 event_cache.append('reverse')
1193 def handle_sort(*pargs, **kwargs):
1194 event_cache.append('sort')
1195 a.x.on_insert(handle_insert)
1196 a.x.on_del(handle_del)
1197 a.x.on_set(handle_set)
1198 a.x.on_reverse(handle_reverse)
1199 a.x.on_sort(handle_sort)
1200
1201 a.x.remove('c')
1202 # ab
1203 a.x.insert(0, 'z')
1204 # zab
1205 del a.x[1]
1206 # zb
1207 a.x.reverse()
1208 # bz
1209 a.x[1] = 'o'
1210 # bo
1211 a.x.append('a')
1212 # boa
1213 a.x.sort()
1214 # abo
1215
1216 # Were the correct events captured?
1217 self.assertEqual(event_cache, ['del', 'insert', 'del', 'reverse', 'set', 'set', 'sort'])
1218
1219 # Is the output correct?
1220 self.assertEqual(a.x, [c for c in 'abo'])
1221
1222 def test_dict(self):
1223 """Does the EventfulDict work?"""
1224 event_cache = []
1225
1226 class A(HasTraits):
1227 x = EventfulDict({c: c for c in 'abc'})
1228 a = A()
1229
1230 def handle_add(key, value):
1231 event_cache.append('add')
1232 def handle_del(key):
1233 event_cache.append('del')
1234 def handle_set(key, value):
1235 event_cache.append('set')
1236 a.x.on_add(handle_add)
1237 a.x.on_del(handle_del)
1238 a.x.on_set(handle_set)
1239
1240 del a.x['c']
1241 # ab
1242 a.x['z'] = 1
1243 # abz
1244 a.x['z'] = 'z'
1245 # abz
1246 a.x.pop('a')
1247 # bz
1248
1249 # Were the correct events captured?
1250 self.assertEqual(event_cache, ['del', 'add', 'set', 'del'])
1251
1252 # Is the output correct?
1253 self.assertEqual(a.x, {c: c for c in 'bz'})
@@ -1,1523 +1,1567
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
55 55 from .importstring import import_item
56 56 from IPython.utils import py3compat
57 from IPython.utils import eventful
57 58 from IPython.utils.py3compat import iteritems
58 59 from IPython.testing.skipdoctest import skip_doctest
59 60
60 61 SequenceTypes = (list, tuple, set, frozenset)
61 62
62 63 #-----------------------------------------------------------------------------
63 64 # Basic classes
64 65 #-----------------------------------------------------------------------------
65 66
66 67
67 68 class NoDefaultSpecified ( object ): pass
68 69 NoDefaultSpecified = NoDefaultSpecified()
69 70
70 71
71 72 class Undefined ( object ): pass
72 73 Undefined = Undefined()
73 74
74 75 class TraitError(Exception):
75 76 pass
76 77
77 78 #-----------------------------------------------------------------------------
78 79 # Utilities
79 80 #-----------------------------------------------------------------------------
80 81
81 82
82 83 def class_of ( object ):
83 84 """ Returns a string containing the class name of an object with the
84 85 correct indefinite article ('a' or 'an') preceding it (e.g., 'an Image',
85 86 'a PlotValue').
86 87 """
87 88 if isinstance( object, py3compat.string_types ):
88 89 return add_article( object )
89 90
90 91 return add_article( object.__class__.__name__ )
91 92
92 93
93 94 def add_article ( name ):
94 95 """ Returns a string containing the correct indefinite article ('a' or 'an')
95 96 prefixed to the specified string.
96 97 """
97 98 if name[:1].lower() in 'aeiou':
98 99 return 'an ' + name
99 100
100 101 return 'a ' + name
101 102
102 103
103 104 def repr_type(obj):
104 105 """ Return a string representation of a value and its type for readable
105 106 error messages.
106 107 """
107 108 the_type = type(obj)
108 109 if (not py3compat.PY3) and the_type is InstanceType:
109 110 # Old-style class.
110 111 the_type = obj.__class__
111 112 msg = '%r %r' % (obj, the_type)
112 113 return msg
113 114
114 115
115 116 def is_trait(t):
116 117 """ Returns whether the given value is an instance or subclass of TraitType.
117 118 """
118 119 return (isinstance(t, TraitType) or
119 120 (isinstance(t, type) and issubclass(t, TraitType)))
120 121
121 122
122 123 def parse_notifier_name(name):
123 124 """Convert the name argument to a list of names.
124 125
125 126 Examples
126 127 --------
127 128
128 129 >>> parse_notifier_name('a')
129 130 ['a']
130 131 >>> parse_notifier_name(['a','b'])
131 132 ['a', 'b']
132 133 >>> parse_notifier_name(None)
133 134 ['anytrait']
134 135 """
135 136 if isinstance(name, str):
136 137 return [name]
137 138 elif name is None:
138 139 return ['anytrait']
139 140 elif isinstance(name, (list, tuple)):
140 141 for n in name:
141 142 assert isinstance(n, str), "names must be strings"
142 143 return name
143 144
144 145
145 146 class _SimpleTest:
146 147 def __init__ ( self, value ): self.value = value
147 148 def __call__ ( self, test ):
148 149 return test == self.value
149 150 def __repr__(self):
150 151 return "<SimpleTest(%r)" % self.value
151 152 def __str__(self):
152 153 return self.__repr__()
153 154
154 155
155 156 def getmembers(object, predicate=None):
156 157 """A safe version of inspect.getmembers that handles missing attributes.
157 158
158 159 This is useful when there are descriptor based attributes that for
159 160 some reason raise AttributeError even though they exist. This happens
160 161 in zope.inteface with the __provides__ attribute.
161 162 """
162 163 results = []
163 164 for key in dir(object):
164 165 try:
165 166 value = getattr(object, key)
166 167 except AttributeError:
167 168 pass
168 169 else:
169 170 if not predicate or predicate(value):
170 171 results.append((key, value))
171 172 results.sort()
172 173 return results
173 174
174 175 @skip_doctest
175 176 class link(object):
176 177 """Link traits from different objects together so they remain in sync.
177 178
178 179 Parameters
179 180 ----------
180 181 obj : pairs of objects/attributes
181 182
182 183 Examples
183 184 --------
184 185
185 186 >>> c = link((obj1, 'value'), (obj2, 'value'), (obj3, 'value'))
186 187 >>> obj1.value = 5 # updates other objects as well
187 188 """
188 189 updating = False
189 190 def __init__(self, *args):
190 191 if len(args) < 2:
191 192 raise TypeError('At least two traitlets must be provided.')
192 193
193 194 self.objects = {}
194 195 initial = getattr(args[0][0], args[0][1])
195 196 for obj,attr in args:
196 197 if getattr(obj, attr) != initial:
197 198 setattr(obj, attr, initial)
198 199
199 200 callback = self._make_closure(obj,attr)
200 201 obj.on_trait_change(callback, attr)
201 202 self.objects[(obj,attr)] = callback
202 203
203 204 @contextlib.contextmanager
204 205 def _busy_updating(self):
205 206 self.updating = True
206 207 try:
207 208 yield
208 209 finally:
209 210 self.updating = False
210 211
211 212 def _make_closure(self, sending_obj, sending_attr):
212 213 def update(name, old, new):
213 214 self._update(sending_obj, sending_attr, new)
214 215 return update
215 216
216 217 def _update(self, sending_obj, sending_attr, new):
217 218 if self.updating:
218 219 return
219 220 with self._busy_updating():
220 221 for obj,attr in self.objects.keys():
221 222 if obj is not sending_obj or attr != sending_attr:
222 223 setattr(obj, attr, new)
223 224
224 225 def unlink(self):
225 226 for key, callback in self.objects.items():
226 227 (obj,attr) = key
227 228 obj.on_trait_change(callback, attr, remove=True)
228 229
229 230 #-----------------------------------------------------------------------------
230 231 # Base TraitType for all traits
231 232 #-----------------------------------------------------------------------------
232 233
233 234
234 235 class TraitType(object):
235 236 """A base class for all trait descriptors.
236 237
237 238 Notes
238 239 -----
239 240 Our implementation of traits is based on Python's descriptor
240 241 prototol. This class is the base class for all such descriptors. The
241 242 only magic we use is a custom metaclass for the main :class:`HasTraits`
242 243 class that does the following:
243 244
244 245 1. Sets the :attr:`name` attribute of every :class:`TraitType`
245 246 instance in the class dict to the name of the attribute.
246 247 2. Sets the :attr:`this_class` attribute of every :class:`TraitType`
247 248 instance in the class dict to the *class* that declared the trait.
248 249 This is used by the :class:`This` trait to allow subclasses to
249 250 accept superclasses for :class:`This` values.
250 251 """
251 252
252 253
253 254 metadata = {}
254 255 default_value = Undefined
255 256 allow_none = False
256 257 info_text = 'any value'
257 258
258 259 def __init__(self, default_value=NoDefaultSpecified, allow_none=None, **metadata):
259 260 """Create a TraitType.
260 261 """
261 262 if default_value is not NoDefaultSpecified:
262 263 self.default_value = default_value
263 264 if allow_none is not None:
264 265 self.allow_none = allow_none
265 266
266 267 if len(metadata) > 0:
267 268 if len(self.metadata) > 0:
268 269 self._metadata = self.metadata.copy()
269 270 self._metadata.update(metadata)
270 271 else:
271 272 self._metadata = metadata
272 273 else:
273 274 self._metadata = self.metadata
274 275
275 276 self.init()
276 277
277 278 def init(self):
278 279 pass
279 280
280 281 def get_default_value(self):
281 282 """Create a new instance of the default value."""
282 283 return self.default_value
283 284
284 285 def instance_init(self, obj):
285 286 """This is called by :meth:`HasTraits.__new__` to finish init'ing.
286 287
287 288 Some stages of initialization must be delayed until the parent
288 289 :class:`HasTraits` instance has been created. This method is
289 290 called in :meth:`HasTraits.__new__` after the instance has been
290 291 created.
291 292
292 293 This method trigger the creation and validation of default values
293 294 and also things like the resolution of str given class names in
294 295 :class:`Type` and :class`Instance`.
295 296
296 297 Parameters
297 298 ----------
298 299 obj : :class:`HasTraits` instance
299 300 The parent :class:`HasTraits` instance that has just been
300 301 created.
301 302 """
302 303 self.set_default_value(obj)
303 304
304 305 def set_default_value(self, obj):
305 306 """Set the default value on a per instance basis.
306 307
307 308 This method is called by :meth:`instance_init` to create and
308 309 validate the default value. The creation and validation of
309 310 default values must be delayed until the parent :class:`HasTraits`
310 311 class has been instantiated.
311 312 """
312 313 # Check for a deferred initializer defined in the same class as the
313 314 # trait declaration or above.
314 315 mro = type(obj).mro()
315 316 meth_name = '_%s_default' % self.name
316 317 for cls in mro[:mro.index(self.this_class)+1]:
317 318 if meth_name in cls.__dict__:
318 319 break
319 320 else:
320 321 # We didn't find one. Do static initialization.
321 322 dv = self.get_default_value()
322 323 newdv = self._validate(obj, dv)
323 324 obj._trait_values[self.name] = newdv
324 325 return
325 326 # Complete the dynamic initialization.
326 327 obj._trait_dyn_inits[self.name] = meth_name
327 328
328 329 def __get__(self, obj, cls=None):
329 330 """Get the value of the trait by self.name for the instance.
330 331
331 332 Default values are instantiated when :meth:`HasTraits.__new__`
332 333 is called. Thus by the time this method gets called either the
333 334 default value or a user defined value (they called :meth:`__set__`)
334 335 is in the :class:`HasTraits` instance.
335 336 """
336 337 if obj is None:
337 338 return self
338 339 else:
339 340 try:
340 341 value = obj._trait_values[self.name]
341 342 except KeyError:
342 343 # Check for a dynamic initializer.
343 344 if self.name in obj._trait_dyn_inits:
344 345 method = getattr(obj, obj._trait_dyn_inits[self.name])
345 346 value = method()
346 347 # FIXME: Do we really validate here?
347 348 value = self._validate(obj, value)
348 349 obj._trait_values[self.name] = value
349 350 return value
350 351 else:
351 352 raise TraitError('Unexpected error in TraitType: '
352 353 'both default value and dynamic initializer are '
353 354 'absent.')
354 355 except Exception:
355 356 # HasTraits should call set_default_value to populate
356 357 # this. So this should never be reached.
357 358 raise TraitError('Unexpected error in TraitType: '
358 359 'default value not set properly')
359 360 else:
360 361 return value
361 362
362 363 def __set__(self, obj, value):
363 364 new_value = self._validate(obj, value)
364 365 old_value = self.__get__(obj)
365 366 obj._trait_values[self.name] = new_value
366 367 try:
367 368 silent = bool(old_value == new_value)
368 369 except:
369 370 # if there is an error in comparing, default to notify
370 371 silent = False
371 372 if silent is not True:
372 373 # we explicitly compare silent to True just in case the equality
373 374 # comparison above returns something other than True/False
374 375 obj._notify_trait(self.name, old_value, new_value)
375 376
376 377 def _validate(self, obj, value):
377 378 if value is None and self.allow_none:
378 379 return value
379 380 if hasattr(self, 'validate'):
380 381 return self.validate(obj, value)
381 382 elif hasattr(self, 'is_valid_for'):
382 383 valid = self.is_valid_for(value)
383 384 if valid:
384 385 return value
385 386 else:
386 387 raise TraitError('invalid value for type: %r' % value)
387 388 elif hasattr(self, 'value_for'):
388 389 return self.value_for(value)
389 390 else:
390 391 return value
391 392
392 393 def info(self):
393 394 return self.info_text
394 395
395 396 def error(self, obj, value):
396 397 if obj is not None:
397 398 e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
398 399 % (self.name, class_of(obj),
399 400 self.info(), repr_type(value))
400 401 else:
401 402 e = "The '%s' trait must be %s, but a value of %r was specified." \
402 403 % (self.name, self.info(), repr_type(value))
403 404 raise TraitError(e)
404 405
405 406 def get_metadata(self, key):
406 407 return getattr(self, '_metadata', {}).get(key, None)
407 408
408 409 def set_metadata(self, key, value):
409 410 getattr(self, '_metadata', {})[key] = value
410 411
411 412
412 413 #-----------------------------------------------------------------------------
413 414 # The HasTraits implementation
414 415 #-----------------------------------------------------------------------------
415 416
416 417
417 418 class MetaHasTraits(type):
418 419 """A metaclass for HasTraits.
419 420
420 421 This metaclass makes sure that any TraitType class attributes are
421 422 instantiated and sets their name attribute.
422 423 """
423 424
424 425 def __new__(mcls, name, bases, classdict):
425 426 """Create the HasTraits class.
426 427
427 428 This instantiates all TraitTypes in the class dict and sets their
428 429 :attr:`name` attribute.
429 430 """
430 431 # print "MetaHasTraitlets (mcls, name): ", mcls, name
431 432 # print "MetaHasTraitlets (bases): ", bases
432 433 # print "MetaHasTraitlets (classdict): ", classdict
433 434 for k,v in iteritems(classdict):
434 435 if isinstance(v, TraitType):
435 436 v.name = k
436 437 elif inspect.isclass(v):
437 438 if issubclass(v, TraitType):
438 439 vinst = v()
439 440 vinst.name = k
440 441 classdict[k] = vinst
441 442 return super(MetaHasTraits, mcls).__new__(mcls, name, bases, classdict)
442 443
443 444 def __init__(cls, name, bases, classdict):
444 445 """Finish initializing the HasTraits class.
445 446
446 447 This sets the :attr:`this_class` attribute of each TraitType in the
447 448 class dict to the newly created class ``cls``.
448 449 """
449 450 for k, v in iteritems(classdict):
450 451 if isinstance(v, TraitType):
451 452 v.this_class = cls
452 453 super(MetaHasTraits, cls).__init__(name, bases, classdict)
453 454
454 455 class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)):
455 456
456 457 def __new__(cls, *args, **kw):
457 458 # This is needed because object.__new__ only accepts
458 459 # the cls argument.
459 460 new_meth = super(HasTraits, cls).__new__
460 461 if new_meth is object.__new__:
461 462 inst = new_meth(cls)
462 463 else:
463 464 inst = new_meth(cls, **kw)
464 465 inst._trait_values = {}
465 466 inst._trait_notifiers = {}
466 467 inst._trait_dyn_inits = {}
467 468 # Here we tell all the TraitType instances to set their default
468 469 # values on the instance.
469 470 for key in dir(cls):
470 471 # Some descriptors raise AttributeError like zope.interface's
471 472 # __provides__ attributes even though they exist. This causes
472 473 # AttributeErrors even though they are listed in dir(cls).
473 474 try:
474 475 value = getattr(cls, key)
475 476 except AttributeError:
476 477 pass
477 478 else:
478 479 if isinstance(value, TraitType):
479 480 value.instance_init(inst)
480 481
481 482 return inst
482 483
483 484 def __init__(self, *args, **kw):
484 485 # Allow trait values to be set using keyword arguments.
485 486 # We need to use setattr for this to trigger validation and
486 487 # notifications.
487 488 for key, value in iteritems(kw):
488 489 setattr(self, key, value)
489 490
490 491 def _notify_trait(self, name, old_value, new_value):
491 492
492 493 # First dynamic ones
493 494 callables = []
494 495 callables.extend(self._trait_notifiers.get(name,[]))
495 496 callables.extend(self._trait_notifiers.get('anytrait',[]))
496 497
497 498 # Now static ones
498 499 try:
499 500 cb = getattr(self, '_%s_changed' % name)
500 501 except:
501 502 pass
502 503 else:
503 504 callables.append(cb)
504 505
505 506 # Call them all now
506 507 for c in callables:
507 508 # Traits catches and logs errors here. I allow them to raise
508 509 if callable(c):
509 510 argspec = inspect.getargspec(c)
510 511 nargs = len(argspec[0])
511 512 # Bound methods have an additional 'self' argument
512 513 # I don't know how to treat unbound methods, but they
513 514 # can't really be used for callbacks.
514 515 if isinstance(c, types.MethodType):
515 516 offset = -1
516 517 else:
517 518 offset = 0
518 519 if nargs + offset == 0:
519 520 c()
520 521 elif nargs + offset == 1:
521 522 c(name)
522 523 elif nargs + offset == 2:
523 524 c(name, new_value)
524 525 elif nargs + offset == 3:
525 526 c(name, old_value, new_value)
526 527 else:
527 528 raise TraitError('a trait changed callback '
528 529 'must have 0-3 arguments.')
529 530 else:
530 531 raise TraitError('a trait changed callback '
531 532 'must be callable.')
532 533
533 534
534 535 def _add_notifiers(self, handler, name):
535 536 if name not in self._trait_notifiers:
536 537 nlist = []
537 538 self._trait_notifiers[name] = nlist
538 539 else:
539 540 nlist = self._trait_notifiers[name]
540 541 if handler not in nlist:
541 542 nlist.append(handler)
542 543
543 544 def _remove_notifiers(self, handler, name):
544 545 if name in self._trait_notifiers:
545 546 nlist = self._trait_notifiers[name]
546 547 try:
547 548 index = nlist.index(handler)
548 549 except ValueError:
549 550 pass
550 551 else:
551 552 del nlist[index]
552 553
553 554 def on_trait_change(self, handler, name=None, remove=False):
554 555 """Setup a handler to be called when a trait changes.
555 556
556 557 This is used to setup dynamic notifications of trait changes.
557 558
558 559 Static handlers can be created by creating methods on a HasTraits
559 560 subclass with the naming convention '_[traitname]_changed'. Thus,
560 561 to create static handler for the trait 'a', create the method
561 562 _a_changed(self, name, old, new) (fewer arguments can be used, see
562 563 below).
563 564
564 565 Parameters
565 566 ----------
566 567 handler : callable
567 568 A callable that is called when a trait changes. Its
568 569 signature can be handler(), handler(name), handler(name, new)
569 570 or handler(name, old, new).
570 571 name : list, str, None
571 572 If None, the handler will apply to all traits. If a list
572 573 of str, handler will apply to all names in the list. If a
573 574 str, the handler will apply just to that name.
574 575 remove : bool
575 576 If False (the default), then install the handler. If True
576 577 then unintall it.
577 578 """
578 579 if remove:
579 580 names = parse_notifier_name(name)
580 581 for n in names:
581 582 self._remove_notifiers(handler, n)
582 583 else:
583 584 names = parse_notifier_name(name)
584 585 for n in names:
585 586 self._add_notifiers(handler, n)
586 587
587 588 @classmethod
588 589 def class_trait_names(cls, **metadata):
589 590 """Get a list of all the names of this class' traits.
590 591
591 592 This method is just like the :meth:`trait_names` method,
592 593 but is unbound.
593 594 """
594 595 return cls.class_traits(**metadata).keys()
595 596
596 597 @classmethod
597 598 def class_traits(cls, **metadata):
598 599 """Get a `dict` of all the traits of this class. The dictionary
599 600 is keyed on the name and the values are the TraitType objects.
600 601
601 602 This method is just like the :meth:`traits` method, but is unbound.
602 603
603 604 The TraitTypes returned don't know anything about the values
604 605 that the various HasTrait's instances are holding.
605 606
606 607 The metadata kwargs allow functions to be passed in which
607 608 filter traits based on metadata values. The functions should
608 609 take a single value as an argument and return a boolean. If
609 610 any function returns False, then the trait is not included in
610 611 the output. This does not allow for any simple way of
611 612 testing that a metadata name exists and has any
612 613 value because get_metadata returns None if a metadata key
613 614 doesn't exist.
614 615 """
615 616 traits = dict([memb for memb in getmembers(cls) if
616 617 isinstance(memb[1], TraitType)])
617 618
618 619 if len(metadata) == 0:
619 620 return traits
620 621
621 622 for meta_name, meta_eval in metadata.items():
622 623 if type(meta_eval) is not FunctionType:
623 624 metadata[meta_name] = _SimpleTest(meta_eval)
624 625
625 626 result = {}
626 627 for name, trait in traits.items():
627 628 for meta_name, meta_eval in metadata.items():
628 629 if not meta_eval(trait.get_metadata(meta_name)):
629 630 break
630 631 else:
631 632 result[name] = trait
632 633
633 634 return result
634 635
635 636 def trait_names(self, **metadata):
636 637 """Get a list of all the names of this class' traits."""
637 638 return self.traits(**metadata).keys()
638 639
639 640 def traits(self, **metadata):
640 641 """Get a `dict` of all the traits of this class. The dictionary
641 642 is keyed on the name and the values are the TraitType objects.
642 643
643 644 The TraitTypes returned don't know anything about the values
644 645 that the various HasTrait's instances are holding.
645 646
646 647 The metadata kwargs allow functions to be passed in which
647 648 filter traits based on metadata values. The functions should
648 649 take a single value as an argument and return a boolean. If
649 650 any function returns False, then the trait is not included in
650 651 the output. This does not allow for any simple way of
651 652 testing that a metadata name exists and has any
652 653 value because get_metadata returns None if a metadata key
653 654 doesn't exist.
654 655 """
655 656 traits = dict([memb for memb in getmembers(self.__class__) if
656 657 isinstance(memb[1], TraitType)])
657 658
658 659 if len(metadata) == 0:
659 660 return traits
660 661
661 662 for meta_name, meta_eval in metadata.items():
662 663 if type(meta_eval) is not FunctionType:
663 664 metadata[meta_name] = _SimpleTest(meta_eval)
664 665
665 666 result = {}
666 667 for name, trait in traits.items():
667 668 for meta_name, meta_eval in metadata.items():
668 669 if not meta_eval(trait.get_metadata(meta_name)):
669 670 break
670 671 else:
671 672 result[name] = trait
672 673
673 674 return result
674 675
675 676 def trait_metadata(self, traitname, key):
676 677 """Get metadata values for trait by key."""
677 678 try:
678 679 trait = getattr(self.__class__, traitname)
679 680 except AttributeError:
680 681 raise TraitError("Class %s does not have a trait named %s" %
681 682 (self.__class__.__name__, traitname))
682 683 else:
683 684 return trait.get_metadata(key)
684 685
685 686 #-----------------------------------------------------------------------------
686 687 # Actual TraitTypes implementations/subclasses
687 688 #-----------------------------------------------------------------------------
688 689
689 690 #-----------------------------------------------------------------------------
690 691 # TraitTypes subclasses for handling classes and instances of classes
691 692 #-----------------------------------------------------------------------------
692 693
693 694
694 695 class ClassBasedTraitType(TraitType):
695 696 """A trait with error reporting for Type, Instance and This."""
696 697
697 698 def error(self, obj, value):
698 699 kind = type(value)
699 700 if (not py3compat.PY3) and kind is InstanceType:
700 701 msg = 'class %s' % value.__class__.__name__
701 702 else:
702 703 msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) )
703 704
704 705 if obj is not None:
705 706 e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
706 707 % (self.name, class_of(obj),
707 708 self.info(), msg)
708 709 else:
709 710 e = "The '%s' trait must be %s, but a value of %r was specified." \
710 711 % (self.name, self.info(), msg)
711 712
712 713 raise TraitError(e)
713 714
714 715
715 716 class Type(ClassBasedTraitType):
716 717 """A trait whose value must be a subclass of a specified class."""
717 718
718 719 def __init__ (self, default_value=None, klass=None, allow_none=True, **metadata ):
719 720 """Construct a Type trait
720 721
721 722 A Type trait specifies that its values must be subclasses of
722 723 a particular class.
723 724
724 725 If only ``default_value`` is given, it is used for the ``klass`` as
725 726 well.
726 727
727 728 Parameters
728 729 ----------
729 730 default_value : class, str or None
730 731 The default value must be a subclass of klass. If an str,
731 732 the str must be a fully specified class name, like 'foo.bar.Bah'.
732 733 The string is resolved into real class, when the parent
733 734 :class:`HasTraits` class is instantiated.
734 735 klass : class, str, None
735 736 Values of this trait must be a subclass of klass. The klass
736 737 may be specified in a string like: 'foo.bar.MyClass'.
737 738 The string is resolved into real class, when the parent
738 739 :class:`HasTraits` class is instantiated.
739 740 allow_none : boolean
740 741 Indicates whether None is allowed as an assignable value. Even if
741 742 ``False``, the default value may be ``None``.
742 743 """
743 744 if default_value is None:
744 745 if klass is None:
745 746 klass = object
746 747 elif klass is None:
747 748 klass = default_value
748 749
749 750 if not (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)):
750 751 raise TraitError("A Type trait must specify a class.")
751 752
752 753 self.klass = klass
753 754
754 755 super(Type, self).__init__(default_value, allow_none=allow_none, **metadata)
755 756
756 757 def validate(self, obj, value):
757 758 """Validates that the value is a valid object instance."""
758 759 if isinstance(value, py3compat.string_types):
759 760 try:
760 761 value = import_item(value)
761 762 except ImportError:
762 763 raise TraitError("The '%s' trait of %s instance must be a type, but "
763 764 "%r could not be imported" % (self.name, obj, value))
764 765 try:
765 766 if issubclass(value, self.klass):
766 767 return value
767 768 except:
768 769 pass
769 770
770 771 self.error(obj, value)
771 772
772 773 def info(self):
773 774 """ Returns a description of the trait."""
774 775 if isinstance(self.klass, py3compat.string_types):
775 776 klass = self.klass
776 777 else:
777 778 klass = self.klass.__name__
778 779 result = 'a subclass of ' + klass
779 780 if self.allow_none:
780 781 return result + ' or None'
781 782 return result
782 783
783 784 def instance_init(self, obj):
784 785 self._resolve_classes()
785 786 super(Type, self).instance_init(obj)
786 787
787 788 def _resolve_classes(self):
788 789 if isinstance(self.klass, py3compat.string_types):
789 790 self.klass = import_item(self.klass)
790 791 if isinstance(self.default_value, py3compat.string_types):
791 792 self.default_value = import_item(self.default_value)
792 793
793 794 def get_default_value(self):
794 795 return self.default_value
795 796
796 797
797 798 class DefaultValueGenerator(object):
798 799 """A class for generating new default value instances."""
799 800
800 801 def __init__(self, *args, **kw):
801 802 self.args = args
802 803 self.kw = kw
803 804
804 805 def generate(self, klass):
805 806 return klass(*self.args, **self.kw)
806 807
807 808
808 809 class Instance(ClassBasedTraitType):
809 810 """A trait whose value must be an instance of a specified class.
810 811
811 812 The value can also be an instance of a subclass of the specified class.
812 813
813 814 Subclasses can declare default classes by overriding the klass attribute
814 815 """
815 816
816 817 klass = None
817 818
818 819 def __init__(self, klass=None, args=None, kw=None,
819 820 allow_none=True, **metadata ):
820 821 """Construct an Instance trait.
821 822
822 823 This trait allows values that are instances of a particular
823 824 class or its sublclasses. Our implementation is quite different
824 825 from that of enthough.traits as we don't allow instances to be used
825 826 for klass and we handle the ``args`` and ``kw`` arguments differently.
826 827
827 828 Parameters
828 829 ----------
829 830 klass : class, str
830 831 The class that forms the basis for the trait. Class names
831 832 can also be specified as strings, like 'foo.bar.Bar'.
832 833 args : tuple
833 834 Positional arguments for generating the default value.
834 835 kw : dict
835 836 Keyword arguments for generating the default value.
836 837 allow_none : bool
837 838 Indicates whether None is allowed as a value.
838 839
839 840 Notes
840 841 -----
841 842 If both ``args`` and ``kw`` are None, then the default value is None.
842 843 If ``args`` is a tuple and ``kw`` is a dict, then the default is
843 844 created as ``klass(*args, **kw)``. If exactly one of ``args`` or ``kw`` is
844 845 None, the None is replaced by ``()`` or ``{}``, respectively.
845 846 """
846 847 if klass is None:
847 848 klass = self.klass
848 849
849 850 if (klass is not None) and (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)):
850 851 self.klass = klass
851 852 else:
852 853 raise TraitError('The klass attribute must be a class'
853 854 ' not: %r' % klass)
854 855
855 856 # self.klass is a class, so handle default_value
856 857 if args is None and kw is None:
857 858 default_value = None
858 859 else:
859 860 if args is None:
860 861 # kw is not None
861 862 args = ()
862 863 elif kw is None:
863 864 # args is not None
864 865 kw = {}
865 866
866 867 if not isinstance(kw, dict):
867 868 raise TraitError("The 'kw' argument must be a dict or None.")
868 869 if not isinstance(args, tuple):
869 870 raise TraitError("The 'args' argument must be a tuple or None.")
870 871
871 872 default_value = DefaultValueGenerator(*args, **kw)
872 873
873 874 super(Instance, self).__init__(default_value, allow_none=allow_none, **metadata)
874 875
875 876 def validate(self, obj, value):
876 877 if isinstance(value, self.klass):
877 878 return value
878 879 else:
879 880 self.error(obj, value)
880 881
881 882 def info(self):
882 883 if isinstance(self.klass, py3compat.string_types):
883 884 klass = self.klass
884 885 else:
885 886 klass = self.klass.__name__
886 887 result = class_of(klass)
887 888 if self.allow_none:
888 889 return result + ' or None'
889 890
890 891 return result
891 892
892 893 def instance_init(self, obj):
893 894 self._resolve_classes()
894 895 super(Instance, self).instance_init(obj)
895 896
896 897 def _resolve_classes(self):
897 898 if isinstance(self.klass, py3compat.string_types):
898 899 self.klass = import_item(self.klass)
899 900
900 901 def get_default_value(self):
901 902 """Instantiate a default value instance.
902 903
903 904 This is called when the containing HasTraits classes'
904 905 :meth:`__new__` method is called to ensure that a unique instance
905 906 is created for each HasTraits instance.
906 907 """
907 908 dv = self.default_value
908 909 if isinstance(dv, DefaultValueGenerator):
909 910 return dv.generate(self.klass)
910 911 else:
911 912 return dv
912 913
913 914
914 915 class This(ClassBasedTraitType):
915 916 """A trait for instances of the class containing this trait.
916 917
917 918 Because how how and when class bodies are executed, the ``This``
918 919 trait can only have a default value of None. This, and because we
919 920 always validate default values, ``allow_none`` is *always* true.
920 921 """
921 922
922 923 info_text = 'an instance of the same type as the receiver or None'
923 924
924 925 def __init__(self, **metadata):
925 926 super(This, self).__init__(None, **metadata)
926 927
927 928 def validate(self, obj, value):
928 929 # What if value is a superclass of obj.__class__? This is
929 930 # complicated if it was the superclass that defined the This
930 931 # trait.
931 932 if isinstance(value, self.this_class) or (value is None):
932 933 return value
933 934 else:
934 935 self.error(obj, value)
935 936
936 937
937 938 #-----------------------------------------------------------------------------
938 939 # Basic TraitTypes implementations/subclasses
939 940 #-----------------------------------------------------------------------------
940 941
941 942
942 943 class Any(TraitType):
943 944 default_value = None
944 945 info_text = 'any value'
945 946
946 947
947 948 class Int(TraitType):
948 949 """An int trait."""
949 950
950 951 default_value = 0
951 952 info_text = 'an int'
952 953
953 954 def validate(self, obj, value):
954 955 if isinstance(value, int):
955 956 return value
956 957 self.error(obj, value)
957 958
958 959 class CInt(Int):
959 960 """A casting version of the int trait."""
960 961
961 962 def validate(self, obj, value):
962 963 try:
963 964 return int(value)
964 965 except:
965 966 self.error(obj, value)
966 967
967 968 if py3compat.PY3:
968 969 Long, CLong = Int, CInt
969 970 Integer = Int
970 971 else:
971 972 class Long(TraitType):
972 973 """A long integer trait."""
973 974
974 975 default_value = 0
975 976 info_text = 'a long'
976 977
977 978 def validate(self, obj, value):
978 979 if isinstance(value, long):
979 980 return value
980 981 if isinstance(value, int):
981 982 return long(value)
982 983 self.error(obj, value)
983 984
984 985
985 986 class CLong(Long):
986 987 """A casting version of the long integer trait."""
987 988
988 989 def validate(self, obj, value):
989 990 try:
990 991 return long(value)
991 992 except:
992 993 self.error(obj, value)
993 994
994 995 class Integer(TraitType):
995 996 """An integer trait.
996 997
997 998 Longs that are unnecessary (<= sys.maxint) are cast to ints."""
998 999
999 1000 default_value = 0
1000 1001 info_text = 'an integer'
1001 1002
1002 1003 def validate(self, obj, value):
1003 1004 if isinstance(value, int):
1004 1005 return value
1005 1006 if isinstance(value, long):
1006 1007 # downcast longs that fit in int:
1007 1008 # note that int(n > sys.maxint) returns a long, so
1008 1009 # we don't need a condition on this cast
1009 1010 return int(value)
1010 1011 if sys.platform == "cli":
1011 1012 from System import Int64
1012 1013 if isinstance(value, Int64):
1013 1014 return int(value)
1014 1015 self.error(obj, value)
1015 1016
1016 1017
1017 1018 class Float(TraitType):
1018 1019 """A float trait."""
1019 1020
1020 1021 default_value = 0.0
1021 1022 info_text = 'a float'
1022 1023
1023 1024 def validate(self, obj, value):
1024 1025 if isinstance(value, float):
1025 1026 return value
1026 1027 if isinstance(value, int):
1027 1028 return float(value)
1028 1029 self.error(obj, value)
1029 1030
1030 1031
1031 1032 class CFloat(Float):
1032 1033 """A casting version of the float trait."""
1033 1034
1034 1035 def validate(self, obj, value):
1035 1036 try:
1036 1037 return float(value)
1037 1038 except:
1038 1039 self.error(obj, value)
1039 1040
1040 1041 class Complex(TraitType):
1041 1042 """A trait for complex numbers."""
1042 1043
1043 1044 default_value = 0.0 + 0.0j
1044 1045 info_text = 'a complex number'
1045 1046
1046 1047 def validate(self, obj, value):
1047 1048 if isinstance(value, complex):
1048 1049 return value
1049 1050 if isinstance(value, (float, int)):
1050 1051 return complex(value)
1051 1052 self.error(obj, value)
1052 1053
1053 1054
1054 1055 class CComplex(Complex):
1055 1056 """A casting version of the complex number trait."""
1056 1057
1057 1058 def validate (self, obj, value):
1058 1059 try:
1059 1060 return complex(value)
1060 1061 except:
1061 1062 self.error(obj, value)
1062 1063
1063 1064 # We should always be explicit about whether we're using bytes or unicode, both
1064 1065 # for Python 3 conversion and for reliable unicode behaviour on Python 2. So
1065 1066 # we don't have a Str type.
1066 1067 class Bytes(TraitType):
1067 1068 """A trait for byte strings."""
1068 1069
1069 1070 default_value = b''
1070 1071 info_text = 'a bytes object'
1071 1072
1072 1073 def validate(self, obj, value):
1073 1074 if isinstance(value, bytes):
1074 1075 return value
1075 1076 self.error(obj, value)
1076 1077
1077 1078
1078 1079 class CBytes(Bytes):
1079 1080 """A casting version of the byte string trait."""
1080 1081
1081 1082 def validate(self, obj, value):
1082 1083 try:
1083 1084 return bytes(value)
1084 1085 except:
1085 1086 self.error(obj, value)
1086 1087
1087 1088
1088 1089 class Unicode(TraitType):
1089 1090 """A trait for unicode strings."""
1090 1091
1091 1092 default_value = u''
1092 1093 info_text = 'a unicode string'
1093 1094
1094 1095 def validate(self, obj, value):
1095 1096 if isinstance(value, py3compat.unicode_type):
1096 1097 return value
1097 1098 if isinstance(value, bytes):
1098 1099 try:
1099 1100 return value.decode('ascii', 'strict')
1100 1101 except UnicodeDecodeError:
1101 1102 msg = "Could not decode {!r} for unicode trait '{}' of {} instance."
1102 1103 raise TraitError(msg.format(value, self.name, class_of(obj)))
1103 1104 self.error(obj, value)
1104 1105
1105 1106
1106 1107 class CUnicode(Unicode):
1107 1108 """A casting version of the unicode trait."""
1108 1109
1109 1110 def validate(self, obj, value):
1110 1111 try:
1111 1112 return py3compat.unicode_type(value)
1112 1113 except:
1113 1114 self.error(obj, value)
1114 1115
1115 1116
1116 1117 class ObjectName(TraitType):
1117 1118 """A string holding a valid object name in this version of Python.
1118 1119
1119 1120 This does not check that the name exists in any scope."""
1120 1121 info_text = "a valid object identifier in Python"
1121 1122
1122 1123 if py3compat.PY3:
1123 1124 # Python 3:
1124 1125 coerce_str = staticmethod(lambda _,s: s)
1125 1126
1126 1127 else:
1127 1128 # Python 2:
1128 1129 def coerce_str(self, obj, value):
1129 1130 "In Python 2, coerce ascii-only unicode to str"
1130 1131 if isinstance(value, unicode):
1131 1132 try:
1132 1133 return str(value)
1133 1134 except UnicodeEncodeError:
1134 1135 self.error(obj, value)
1135 1136 return value
1136 1137
1137 1138 def validate(self, obj, value):
1138 1139 value = self.coerce_str(obj, value)
1139 1140
1140 1141 if isinstance(value, str) and py3compat.isidentifier(value):
1141 1142 return value
1142 1143 self.error(obj, value)
1143 1144
1144 1145 class DottedObjectName(ObjectName):
1145 1146 """A string holding a valid dotted object name in Python, such as A.b3._c"""
1146 1147 def validate(self, obj, value):
1147 1148 value = self.coerce_str(obj, value)
1148 1149
1149 1150 if isinstance(value, str) and py3compat.isidentifier(value, dotted=True):
1150 1151 return value
1151 1152 self.error(obj, value)
1152 1153
1153 1154
1154 1155 class Bool(TraitType):
1155 1156 """A boolean (True, False) trait."""
1156 1157
1157 1158 default_value = False
1158 1159 info_text = 'a boolean'
1159 1160
1160 1161 def validate(self, obj, value):
1161 1162 if isinstance(value, bool):
1162 1163 return value
1163 1164 self.error(obj, value)
1164 1165
1165 1166
1166 1167 class CBool(Bool):
1167 1168 """A casting version of the boolean trait."""
1168 1169
1169 1170 def validate(self, obj, value):
1170 1171 try:
1171 1172 return bool(value)
1172 1173 except:
1173 1174 self.error(obj, value)
1174 1175
1175 1176
1176 1177 class Enum(TraitType):
1177 1178 """An enum that whose value must be in a given sequence."""
1178 1179
1179 1180 def __init__(self, values, default_value=None, allow_none=True, **metadata):
1180 1181 self.values = values
1181 1182 super(Enum, self).__init__(default_value, allow_none=allow_none, **metadata)
1182 1183
1183 1184 def validate(self, obj, value):
1184 1185 if value in self.values:
1185 1186 return value
1186 1187 self.error(obj, value)
1187 1188
1188 1189 def info(self):
1189 1190 """ Returns a description of the trait."""
1190 1191 result = 'any of ' + repr(self.values)
1191 1192 if self.allow_none:
1192 1193 return result + ' or None'
1193 1194 return result
1194 1195
1195 1196 class CaselessStrEnum(Enum):
1196 1197 """An enum of strings that are caseless in validate."""
1197 1198
1198 1199 def validate(self, obj, value):
1199 1200 if not isinstance(value, py3compat.string_types):
1200 1201 self.error(obj, value)
1201 1202
1202 1203 for v in self.values:
1203 1204 if v.lower() == value.lower():
1204 1205 return v
1205 1206 self.error(obj, value)
1206 1207
1207 1208 class Container(Instance):
1208 1209 """An instance of a container (list, set, etc.)
1209 1210
1210 1211 To be subclassed by overriding klass.
1211 1212 """
1212 1213 klass = None
1213 1214 _cast_types = ()
1214 1215 _valid_defaults = SequenceTypes
1215 1216 _trait = None
1216 1217
1217 1218 def __init__(self, trait=None, default_value=None, allow_none=True,
1218 1219 **metadata):
1219 1220 """Create a container trait type from a list, set, or tuple.
1220 1221
1221 1222 The default value is created by doing ``List(default_value)``,
1222 1223 which creates a copy of the ``default_value``.
1223 1224
1224 1225 ``trait`` can be specified, which restricts the type of elements
1225 1226 in the container to that TraitType.
1226 1227
1227 1228 If only one arg is given and it is not a Trait, it is taken as
1228 1229 ``default_value``:
1229 1230
1230 1231 ``c = List([1,2,3])``
1231 1232
1232 1233 Parameters
1233 1234 ----------
1234 1235
1235 1236 trait : TraitType [ optional ]
1236 1237 the type for restricting the contents of the Container. If unspecified,
1237 1238 types are not checked.
1238 1239
1239 1240 default_value : SequenceType [ optional ]
1240 1241 The default value for the Trait. Must be list/tuple/set, and
1241 1242 will be cast to the container type.
1242 1243
1243 1244 allow_none : Bool [ default True ]
1244 1245 Whether to allow the value to be None
1245 1246
1246 1247 **metadata : any
1247 1248 further keys for extensions to the Trait (e.g. config)
1248 1249
1249 1250 """
1250 1251 # allow List([values]):
1251 1252 if default_value is None and not is_trait(trait):
1252 1253 default_value = trait
1253 1254 trait = None
1254 1255
1255 1256 if default_value is None:
1256 1257 args = ()
1257 1258 elif isinstance(default_value, self._valid_defaults):
1258 1259 args = (default_value,)
1259 1260 else:
1260 1261 raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value))
1261 1262
1262 1263 if is_trait(trait):
1263 1264 self._trait = trait() if isinstance(trait, type) else trait
1264 1265 self._trait.name = 'element'
1265 1266 elif trait is not None:
1266 1267 raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait))
1267 1268
1268 1269 super(Container,self).__init__(klass=self.klass, args=args,
1269 1270 allow_none=allow_none, **metadata)
1270 1271
1271 1272 def element_error(self, obj, element, validator):
1272 1273 e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \
1273 1274 % (self.name, class_of(obj), validator.info(), repr_type(element))
1274 1275 raise TraitError(e)
1275 1276
1276 1277 def validate(self, obj, value):
1277 1278 if isinstance(value, self._cast_types):
1278 1279 value = self.klass(value)
1279 1280 value = super(Container, self).validate(obj, value)
1280 1281 if value is None:
1281 1282 return value
1282 1283
1283 1284 value = self.validate_elements(obj, value)
1284 1285
1285 1286 return value
1286 1287
1287 1288 def validate_elements(self, obj, value):
1288 1289 validated = []
1289 1290 if self._trait is None or isinstance(self._trait, Any):
1290 1291 return value
1291 1292 for v in value:
1292 1293 try:
1293 1294 v = self._trait._validate(obj, v)
1294 1295 except TraitError:
1295 1296 self.element_error(obj, v, self._trait)
1296 1297 else:
1297 1298 validated.append(v)
1298 1299 return self.klass(validated)
1299 1300
1300 1301 def instance_init(self, obj):
1301 1302 if isinstance(self._trait, Instance):
1302 1303 self._trait._resolve_classes()
1303 1304 super(Container, self).instance_init(obj)
1304 1305
1305 1306
1306 1307 class List(Container):
1307 1308 """An instance of a Python list."""
1308 1309 klass = list
1309 1310 _cast_types = (tuple,)
1310 1311
1311 1312 def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize,
1312 1313 allow_none=True, **metadata):
1313 1314 """Create a List trait type from a list, set, or tuple.
1314 1315
1315 1316 The default value is created by doing ``List(default_value)``,
1316 1317 which creates a copy of the ``default_value``.
1317 1318
1318 1319 ``trait`` can be specified, which restricts the type of elements
1319 1320 in the container to that TraitType.
1320 1321
1321 1322 If only one arg is given and it is not a Trait, it is taken as
1322 1323 ``default_value``:
1323 1324
1324 1325 ``c = List([1,2,3])``
1325 1326
1326 1327 Parameters
1327 1328 ----------
1328 1329
1329 1330 trait : TraitType [ optional ]
1330 1331 the type for restricting the contents of the Container. If unspecified,
1331 1332 types are not checked.
1332 1333
1333 1334 default_value : SequenceType [ optional ]
1334 1335 The default value for the Trait. Must be list/tuple/set, and
1335 1336 will be cast to the container type.
1336 1337
1337 1338 minlen : Int [ default 0 ]
1338 1339 The minimum length of the input list
1339 1340
1340 1341 maxlen : Int [ default sys.maxsize ]
1341 1342 The maximum length of the input list
1342 1343
1343 1344 allow_none : Bool [ default True ]
1344 1345 Whether to allow the value to be None
1345 1346
1346 1347 **metadata : any
1347 1348 further keys for extensions to the Trait (e.g. config)
1348 1349
1349 1350 """
1350 1351 self._minlen = minlen
1351 1352 self._maxlen = maxlen
1352 1353 super(List, self).__init__(trait=trait, default_value=default_value,
1353 1354 allow_none=allow_none, **metadata)
1354 1355
1355 1356 def length_error(self, obj, value):
1356 1357 e = "The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified." \
1357 1358 % (self.name, class_of(obj), self._minlen, self._maxlen, value)
1358 1359 raise TraitError(e)
1359 1360
1360 1361 def validate_elements(self, obj, value):
1361 1362 length = len(value)
1362 1363 if length < self._minlen or length > self._maxlen:
1363 1364 self.length_error(obj, value)
1364 1365
1365 1366 return super(List, self).validate_elements(obj, value)
1366 1367
1367 1368 def validate(self, obj, value):
1368 1369 value = super(List, self).validate(obj, value)
1369 1370
1370 1371 value = self.validate_elements(obj, value)
1371 1372
1372 1373 return value
1373 1374
1374 1375
1375 1376
1376 1377 class Set(List):
1377 1378 """An instance of a Python set."""
1378 1379 klass = set
1379 1380 _cast_types = (tuple, list)
1380 1381
1381 1382 class Tuple(Container):
1382 1383 """An instance of a Python tuple."""
1383 1384 klass = tuple
1384 1385 _cast_types = (list,)
1385 1386
1386 1387 def __init__(self, *traits, **metadata):
1387 1388 """Tuple(*traits, default_value=None, allow_none=True, **medatata)
1388 1389
1389 1390 Create a tuple from a list, set, or tuple.
1390 1391
1391 1392 Create a fixed-type tuple with Traits:
1392 1393
1393 1394 ``t = Tuple(Int, Str, CStr)``
1394 1395
1395 1396 would be length 3, with Int,Str,CStr for each element.
1396 1397
1397 1398 If only one arg is given and it is not a Trait, it is taken as
1398 1399 default_value:
1399 1400
1400 1401 ``t = Tuple((1,2,3))``
1401 1402
1402 1403 Otherwise, ``default_value`` *must* be specified by keyword.
1403 1404
1404 1405 Parameters
1405 1406 ----------
1406 1407
1407 1408 *traits : TraitTypes [ optional ]
1408 1409 the tsype for restricting the contents of the Tuple. If unspecified,
1409 1410 types are not checked. If specified, then each positional argument
1410 1411 corresponds to an element of the tuple. Tuples defined with traits
1411 1412 are of fixed length.
1412 1413
1413 1414 default_value : SequenceType [ optional ]
1414 1415 The default value for the Tuple. Must be list/tuple/set, and
1415 1416 will be cast to a tuple. If `traits` are specified, the
1416 1417 `default_value` must conform to the shape and type they specify.
1417 1418
1418 1419 allow_none : Bool [ default True ]
1419 1420 Whether to allow the value to be None
1420 1421
1421 1422 **metadata : any
1422 1423 further keys for extensions to the Trait (e.g. config)
1423 1424
1424 1425 """
1425 1426 default_value = metadata.pop('default_value', None)
1426 1427 allow_none = metadata.pop('allow_none', True)
1427 1428
1428 1429 # allow Tuple((values,)):
1429 1430 if len(traits) == 1 and default_value is None and not is_trait(traits[0]):
1430 1431 default_value = traits[0]
1431 1432 traits = ()
1432 1433
1433 1434 if default_value is None:
1434 1435 args = ()
1435 1436 elif isinstance(default_value, self._valid_defaults):
1436 1437 args = (default_value,)
1437 1438 else:
1438 1439 raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value))
1439 1440
1440 1441 self._traits = []
1441 1442 for trait in traits:
1442 1443 t = trait() if isinstance(trait, type) else trait
1443 1444 t.name = 'element'
1444 1445 self._traits.append(t)
1445 1446
1446 1447 if self._traits and default_value is None:
1447 1448 # don't allow default to be an empty container if length is specified
1448 1449 args = None
1449 1450 super(Container,self).__init__(klass=self.klass, args=args,
1450 1451 allow_none=allow_none, **metadata)
1451 1452
1452 1453 def validate_elements(self, obj, value):
1453 1454 if not self._traits:
1454 1455 # nothing to validate
1455 1456 return value
1456 1457 if len(value) != len(self._traits):
1457 1458 e = "The '%s' trait of %s instance requires %i elements, but a value of %s was specified." \
1458 1459 % (self.name, class_of(obj), len(self._traits), repr_type(value))
1459 1460 raise TraitError(e)
1460 1461
1461 1462 validated = []
1462 1463 for t,v in zip(self._traits, value):
1463 1464 try:
1464 1465 v = t._validate(obj, v)
1465 1466 except TraitError:
1466 1467 self.element_error(obj, v, t)
1467 1468 else:
1468 1469 validated.append(v)
1469 1470 return tuple(validated)
1470 1471
1471 1472
1472 1473 class Dict(Instance):
1473 1474 """An instance of a Python dict."""
1474 1475
1475 1476 def __init__(self, default_value=None, allow_none=True, **metadata):
1476 1477 """Create a dict trait type from a dict.
1477 1478
1478 1479 The default value is created by doing ``dict(default_value)``,
1479 1480 which creates a copy of the ``default_value``.
1480 1481 """
1481 1482 if default_value is None:
1482 1483 args = ((),)
1483 1484 elif isinstance(default_value, dict):
1484 1485 args = (default_value,)
1485 1486 elif isinstance(default_value, SequenceTypes):
1486 1487 args = (default_value,)
1487 1488 else:
1488 1489 raise TypeError('default value of Dict was %s' % default_value)
1489 1490
1490 1491 super(Dict,self).__init__(klass=dict, args=args,
1491 1492 allow_none=allow_none, **metadata)
1492 1493
1494
1495 class EventfulDict(Instance):
1496 """An instance of an EventfulDict."""
1497
1498 def __init__(self, default_value=None, allow_none=True, **metadata):
1499 """Create a EventfulDict trait type from a dict.
1500
1501 The default value is created by doing
1502 ``eventful.EvenfulDict(default_value)``, which creates a copy of the
1503 ``default_value``.
1504 """
1505 if default_value is None:
1506 args = ((),)
1507 elif isinstance(default_value, dict):
1508 args = (default_value,)
1509 elif isinstance(default_value, SequenceTypes):
1510 args = (default_value,)
1511 else:
1512 raise TypeError('default value of EventfulDict was %s' % default_value)
1513
1514 super(EventfulDict, self).__init__(klass=eventful.EventfulDict, args=args,
1515 allow_none=allow_none, **metadata)
1516
1517
1518 class EventfulList(Instance):
1519 """An instance of an EventfulList."""
1520
1521 def __init__(self, default_value=None, allow_none=True, **metadata):
1522 """Create a EventfulList trait type from a dict.
1523
1524 The default value is created by doing
1525 ``eventful.EvenfulList(default_value)``, which creates a copy of the
1526 ``default_value``.
1527 """
1528 if default_value is None:
1529 args = ((),)
1530 else:
1531 args = (default_value,)
1532
1533 super(EventfulList, self).__init__(klass=eventful.EventfulList, args=args,
1534 allow_none=allow_none, **metadata)
1535
1536
1493 1537 class TCPAddress(TraitType):
1494 1538 """A trait for an (ip, port) tuple.
1495 1539
1496 1540 This allows for both IPv4 IP addresses as well as hostnames.
1497 1541 """
1498 1542
1499 1543 default_value = ('127.0.0.1', 0)
1500 1544 info_text = 'an (ip, port) tuple'
1501 1545
1502 1546 def validate(self, obj, value):
1503 1547 if isinstance(value, tuple):
1504 1548 if len(value) == 2:
1505 1549 if isinstance(value[0], py3compat.string_types) and isinstance(value[1], int):
1506 1550 port = value[1]
1507 1551 if port >= 0 and port <= 65535:
1508 1552 return value
1509 1553 self.error(obj, value)
1510 1554
1511 1555 class CRegExp(TraitType):
1512 1556 """A casting compiled regular expression trait.
1513 1557
1514 1558 Accepts both strings and compiled regular expressions. The resulting
1515 1559 attribute will be a compiled regular expression."""
1516 1560
1517 1561 info_text = 'a regular expression'
1518 1562
1519 1563 def validate(self, obj, value):
1520 1564 try:
1521 1565 return re.compile(value)
1522 1566 except:
1523 1567 self.error(obj, value)
General Comments 0
You need to be logged in to leave comments. Login now