##// END OF EJS Templates
More work on traitlets.py. I have added tests for existing traitlets.
Brian Granger -
Show More
@@ -0,0 +1,437 b''
1 #!/usr/bin/env python
2 # encoding: utf-8
3 """
4 Tests for IPython.utils.traitlets.
5
6 Authors:
7
8 * Brian Granger
9 * Enthought, Inc. Some of the code in this file comes from enthought.traits
10 and is licensed under the BSD license. Also, many of the ideas also come
11 from enthought.traits even though our implementation is very different.
12 """
13
14 #-----------------------------------------------------------------------------
15 # Copyright (C) 2008-2009 The IPython Development Team
16 #
17 # Distributed under the terms of the BSD License. The full license is in
18 # the file COPYING, distributed as part of this software.
19 #-----------------------------------------------------------------------------
20
21 #-----------------------------------------------------------------------------
22 # Imports
23 #-----------------------------------------------------------------------------
24
25 import sys
26 import os
27
28
29 from unittest import TestCase
30
31 from IPython.utils.traitlets import (
32 HasTraitlets, MetaHasTraitlets, TraitletType, Any,
33 Int, Long, Float, Complex, Str, Unicode, Bool, TraitletError
34 )
35
36
37 #-----------------------------------------------------------------------------
38 # Helper classes for testing
39 #-----------------------------------------------------------------------------
40
41
42 class HasTraitletsStub(object):
43
44 def __init__(self):
45 self._traitlet_values = {}
46 self._traitlet_notifiers = {}
47
48 def _notify(self, name, old, new):
49 self._notify_name = name
50 self._notify_old = old
51 self._notify_new = new
52
53
54 #-----------------------------------------------------------------------------
55 # Test classes
56 #-----------------------------------------------------------------------------
57
58
59 class TestTraitletType(TestCase):
60
61 name = 'a'
62
63 def setUp(self):
64 self.tt = TraitletType()
65 self.tt.name = self.name
66 self.hast = HasTraitletsStub()
67
68 def test_get(self):
69 value = self.tt.__get__(self.hast)
70 self.assertEquals(value, None)
71
72 def test_set(self):
73 self.tt.__set__(self.hast, 10)
74 self.assertEquals(self.hast._traitlet_values[self.name],10)
75 self.assertEquals(self.hast._notify_name,self.name)
76 self.assertEquals(self.hast._notify_old,None)
77 self.assertEquals(self.hast._notify_new,10)
78
79 def test_validate(self):
80 class MyTT(TraitletType):
81 def validate(self, inst, value):
82 return -1
83 tt = MyTT()
84 tt.name = self.name
85 tt.__set__(self.hast, 10)
86 self.assertEquals(tt.__get__(self.hast),-1)
87
88 def test_is_valid_for(self):
89 class MyTT(TraitletType):
90 def is_valid_for(self, value):
91 return True
92 tt = MyTT()
93 tt.name = self.name
94 tt.__set__(self.hast, 10)
95 self.assertEquals(tt.__get__(self.hast),10)
96
97 def test_value_for(self):
98 class MyTT(TraitletType):
99 def value_for(self, value):
100 return 20
101 tt = MyTT()
102 tt.name = self.name
103 tt.__set__(self.hast, 10)
104 self.assertEquals(tt.__get__(self.hast),20)
105
106 def test_info(self):
107 self.assertEquals(self.tt.info(),'any value')
108
109 def test_error(self):
110 self.assertRaises(TraitletError, self.tt.error, self.hast, 10)
111
112
113 class TestHasTraitletsMeta(TestCase):
114
115 def test_metaclass(self):
116 self.assertEquals(type(HasTraitlets), MetaHasTraitlets)
117
118 class A(HasTraitlets):
119 a = Int
120
121 a = A()
122 self.assertEquals(type(a.__class__), MetaHasTraitlets)
123 self.assertEquals(a.a,0)
124 a.a = 10
125 self.assertEquals(a.a,10)
126
127 class B(HasTraitlets):
128 b = Int()
129
130 b = B()
131 self.assertEquals(b.b,0)
132 b.b = 10
133 self.assertEquals(b.b,10)
134
135 class C(HasTraitlets):
136 c = Int(30)
137
138 c = C()
139 self.assertEquals(c.c,30)
140 c.c = 10
141 self.assertEquals(c.c,10)
142
143
144 class TestHasTraitletsNotify(TestCase):
145
146 def setUp(self):
147 self._notify1 = []
148 self._notify2 = []
149
150 def notify1(self, name, old, new):
151 self._notify1.append((name, old, new))
152
153 def notify2(self, name, old, new):
154 self._notify2.append((name, old, new))
155
156 def test_notify_all(self):
157
158 class A(HasTraitlets):
159 a = Int
160 b = Float
161
162 a = A()
163 a.on_traitlet_change(self.notify1)
164 a.a = 0
165 self.assertEquals(len(self._notify1),0)
166 a.b = 0.0
167 self.assertEquals(len(self._notify1),0)
168 a.a = 10
169 self.assert_(('a',0,10) in self._notify1)
170 a.b = 10.0
171 self.assert_(('b',0.0,10.0) in self._notify1)
172 self.assertRaises(TraitletError,setattr,a,'a','bad string')
173 self.assertRaises(TraitletError,setattr,a,'b','bad string')
174 self._notify1 = []
175 a.on_traitlet_change(self.notify1,remove=True)
176 a.a = 20
177 a.b = 20.0
178 self.assertEquals(len(self._notify1),0)
179
180 def test_notify_one(self):
181
182 class A(HasTraitlets):
183 a = Int
184 b = Float
185
186 a = A()
187 a.on_traitlet_change(self.notify1, 'a')
188 a.a = 0
189 self.assertEquals(len(self._notify1),0)
190 a.a = 10
191 self.assert_(('a',0,10) in self._notify1)
192 self.assertRaises(TraitletError,setattr,a,'a','bad string')
193
194 def test_subclass(self):
195
196 class A(HasTraitlets):
197 a = Int
198
199 class B(A):
200 b = Float
201
202 b = B()
203 self.assertEquals(b.a,0)
204 self.assertEquals(b.b,0.0)
205 b.a = 100
206 b.b = 100.0
207 self.assertEquals(b.a,100)
208 self.assertEquals(b.b,100.0)
209
210 def test_notify_subclass(self):
211
212 class A(HasTraitlets):
213 a = Int
214
215 class B(A):
216 b = Float
217
218 b = B()
219 b.on_traitlet_change(self.notify1, 'a')
220 b.on_traitlet_change(self.notify2, 'b')
221 b.a = 0
222 b.b = 0.0
223 self.assertEquals(len(self._notify1),0)
224 self.assertEquals(len(self._notify2),0)
225 b.a = 10
226 b.b = 10.0
227 self.assert_(('a',0,10) in self._notify1)
228 self.assert_(('b',0.0,10.0) in self._notify2)
229
230 def test_static_notify(self):
231
232 class A(HasTraitlets):
233 a = Int
234 _notify1 = []
235 def _a_changed(self, name, old, new):
236 self._notify1.append((name, old, new))
237
238 a = A()
239 a.a = 0
240 self.assertEquals(len(a._notify1),0)
241 a.a = 10
242 self.assert_(('a',0,10) in a._notify1)
243
244 class B(A):
245 b = Float
246 _notify2 = []
247 def _b_changed(self, name, old, new):
248 self._notify2.append((name, old, new))
249
250 b = B()
251 b.a = 10
252 b.b = 10.0
253 self.assert_(('a',0,10) in b._notify1)
254 self.assert_(('b',0.0,10.0) in b._notify2)
255
256 def test_notify_args(self):
257
258 def callback0():
259 self.cb = ()
260 def callback1(name):
261 self.cb = (name,)
262 def callback2(name, new):
263 self.cb = (name, new)
264 def callback3(name, old, new):
265 self.cb = (name, old, new)
266
267 class A(HasTraitlets):
268 a = Int
269
270 a = A()
271 a.on_traitlet_change(callback0, 'a')
272 a.a = 10
273 self.assertEquals(self.cb,())
274 a.on_traitlet_change(callback0, 'a', remove=True)
275
276 a.on_traitlet_change(callback1, 'a')
277 a.a = 100
278 self.assertEquals(self.cb,('a',))
279 a.on_traitlet_change(callback1, 'a', remove=True)
280
281 a.on_traitlet_change(callback2, 'a')
282 a.a = 1000
283 self.assertEquals(self.cb,('a',1000))
284 a.on_traitlet_change(callback2, 'a', remove=True)
285
286 a.on_traitlet_change(callback3, 'a')
287 a.a = 10000
288 self.assertEquals(self.cb,('a',1000,10000))
289 a.on_traitlet_change(callback3, 'a', remove=True)
290
291 self.assertEquals(len(a._traitlet_notifiers['a']),0)
292
293
294 class TestAddTraitlet(TestCase):
295
296 def test_add_float(self):
297
298 class A(HasTraitlets):
299 a = Int
300
301 a = A()
302 a.a = 10
303 a._add_class_traitlet('b',Float)
304 self.assertEquals(a.b,0.0)
305 a.b = 10.0
306 self.assertEquals(a.b,10.0)
307 self.assertRaises(TraitletError, setattr, a, 'b', 'bad value')
308
309
310 #-----------------------------------------------------------------------------
311 # Tests for specific traitlet types
312 #-----------------------------------------------------------------------------
313
314 class TraitletTestBase(TestCase):
315
316 def assign(self, value):
317 self.obj.value = value
318
319 def coerce(self, value):
320 return value
321
322 def test_good_values(self):
323 if hasattr(self, '_good_values'):
324 for value in self._good_values:
325 self.assign(value)
326 self.assertEquals(self.obj.value, self.coerce(value))
327
328 def test_bad_values(self):
329 if hasattr(self, '_bad_values'):
330 for value in self._bad_values:
331 self.assertRaises(TraitletError, self.assign, value)
332
333 def test_default_value(self):
334 if hasattr(self, '_default_value'):
335 self.assertEquals(self._default_value, self.obj.value)
336
337
338 class AnyTraitlet(HasTraitlets):
339
340 value = Any
341
342 class AnyTraitTest(TraitletTestBase):
343
344 obj = AnyTraitlet()
345
346 _default_value = None
347 _good_values = [10.0, 'ten', u'ten', [10], {'ten': 10},(10,), None, 1j]
348 _bad_values = []
349
350
351 class IntTraitlet(HasTraitlets):
352
353 value = Int(99)
354
355 class TestInt(TraitletTestBase):
356
357 obj = IntTraitlet()
358 _default_value = 99
359 _good_values = [10, -10]
360 _bad_values = ['ten', u'ten', [10], {'ten': 10},(10,), None, 1j, 10L,
361 -10L, 10.1, -10.1, '10L', '-10L', '10.1', '-10.1', u'10L',
362 u'-10L', u'10.1', u'-10.1', '10', '-10', u'10', u'-10']
363
364
365 class LongTraitlet(HasTraitlets):
366
367 value = Long(99L)
368
369 class TestLong(TraitletTestBase):
370
371 obj = LongTraitlet()
372
373 _default_value = 99L
374 _good_values = [10, -10, 10L, -10L]
375 _bad_values = ['ten', u'ten', [10], [10l], {'ten': 10},(10,),(10L,),
376 None, 1j, 10.1, -10.1, '10', '-10', '10L', '-10L', '10.1',
377 '-10.1', u'10', u'-10', u'10L', u'-10L', u'10.1',
378 u'-10.1']
379
380
381 class FloatTraitlet(HasTraitlets):
382
383 value = Float(99.0)
384
385 class TestFloat(TraitletTestBase):
386
387 obj = FloatTraitlet()
388
389 _default_value = 99.0
390 _good_values = [10, -10, 10.1, -10.1]
391 _bad_values = [10L, -10L, 'ten', u'ten', [10], {'ten': 10},(10,), None,
392 1j, '10', '-10', '10L', '-10L', '10.1', '-10.1', u'10',
393 u'-10', u'10L', u'-10L', u'10.1', u'-10.1']
394
395
396 class ComplexTraitlet(HasTraitlets):
397
398 value = Complex(99.0-99.0j)
399
400 class TestComplex(TraitletTestBase):
401
402 obj = ComplexTraitlet()
403
404 _default_value = 99.0-99.0j
405 _good_values = [10, -10, 10.1, -10.1, 10j, 10+10j, 10-10j,
406 10.1j, 10.1+10.1j, 10.1-10.1j]
407 _bad_values = [10L, -10L, u'10L', u'-10L', 'ten', [10], {'ten': 10},(10,), None]
408
409
410 class StringTraitlet(HasTraitlets):
411
412 value = Str('string')
413
414 class TestString(TraitletTestBase):
415
416 obj = StringTraitlet()
417
418 _default_value = 'string'
419 _good_values = ['10', '-10', '10L',
420 '-10L', '10.1', '-10.1', 'string']
421 _bad_values = [10, -10, 10L, -10L, 10.1, -10.1, 1j, [10],
422 ['ten'],{'ten': 10},(10,), None, u'string']
423
424
425 class UnicodeTraitlet(HasTraitlets):
426
427 value = Unicode(u'unicode')
428
429 class TestUnicode(TraitletTestBase):
430
431 obj = UnicodeTraitlet()
432
433 _default_value = u'unicode'
434 _good_values = ['10', '-10', '10L', '-10L', '10.1',
435 '-10.1', '', u'', 'string', u'string', ]
436 _bad_values = [10, -10, 10L, -10L, 10.1, -10.1, 1j,
437 [10], ['ten'], [u'ten'], {'ten': 10},(10,), None]
@@ -3,6 +3,26 b''
3 3 """
4 4 A lightweight Traits like module.
5 5
6 This is designed to provide a lightweight, simple, pure Python version of
7 many of the capabilities of enthought.traits. This includes:
8
9 * Validation
10 * Type specification with defaults
11 * Static and dynamic notification
12 * Basic predefined types
13 * An API that is similar to enthought.traits
14
15 We don't support:
16
17 * Delegation
18 * Automatic GUI generation
19 * A full set of trait types
20 * API compatibility with enthought.traits
21
22 We choose to create this module because we need these capabilities, but
23 we need them to be pure Python so they work in all Python implementations,
24 including Jython and IronPython.
25
6 26 Authors:
7 27
8 28 * Brian Granger
@@ -82,6 +102,18 b' def repr_type(obj):'
82 102
83 103
84 104 def parse_notifier_name(name):
105 """Convert the name argument to a list of names.
106
107 Examples
108 --------
109
110 >>> parse_notifier_name('a')
111 ['a']
112 >>> parse_notifier_name(['a','b'])
113 ['a', 'b']
114 >>> parse_notifier_name(None)
115 ['anytraitlet']
116 """
85 117 if isinstance(name, str):
86 118 return [name]
87 119 elif name is None:
@@ -103,8 +135,6 b' class TraitletType(object):'
103 135 default_value = None
104 136 info_text = 'any value'
105 137
106 # def __init__(self, name, default_value=NoDefaultSpecified, **metadata):
107 # self.name = name
108 138 def __init__(self, default_value=NoDefaultSpecified, **metadata):
109 139 if default_value is not NoDefaultSpecified:
110 140 self.default_value = default_value
@@ -121,7 +151,7 b' class TraitletType(object):'
121 151 old_value = self.__get__(inst)
122 152 if old_value != new_value:
123 153 inst._traitlet_values[self.name] = new_value
124 inst._notify(self.name, old_value, value)
154 inst._notify(self.name, old_value, new_value)
125 155
126 156 def _validate(self, inst, value):
127 157 if hasattr(self, 'validate'):
@@ -181,28 +211,64 b' class HasTraitlets(object):'
181 211
182 212 def __init__(self):
183 213 self._traitlet_values = {}
184 self._notifiers = {}
214 self._traitlet_notifiers = {}
185 215
186 216 def _notify(self, name, old_value, new_value):
187 callables = self._notifiers.get(name,[])
188 more_callables = self._notifiers.get('anytraitlet',[])
217
218 # First dynamic ones
219 callables = self._traitlet_notifiers.get(name,[])
220 more_callables = self._traitlet_notifiers.get('anytraitlet',[])
189 221 callables.extend(more_callables)
222
223 # Now static ones
224 try:
225 cb = getattr(self, '_%s_changed' % name)
226 except:
227 pass
228 else:
229 callables.append(cb)
230
231 # Call them all now
190 232 for c in callables:
191 233 # Traits catches and logs errors here. I allow them to raise
192 c(name, old_value, new_value)
234 if callable(c):
235 argspec = inspect.getargspec(c)
236 nargs = len(argspec[0])
237 # Bound methods have an additional 'self' argument
238 # I don't know how to treat unbound methods, but they
239 # can't really be used for callbacks.
240 if isinstance(c, types.MethodType):
241 offset = -1
242 else:
243 offset = 0
244 if nargs + offset == 0:
245 c()
246 elif nargs + offset == 1:
247 c(name)
248 elif nargs + offset == 2:
249 c(name, new_value)
250 elif nargs + offset == 3:
251 c(name, old_value, new_value)
252 else:
253 raise TraitletError('a traitlet changed callback '
254 'must have 0-3 arguments.')
255 else:
256 raise TraitletError('a traitlet changed callback '
257 'must be callable.')
258
193 259
194 260 def _add_notifiers(self, handler, name):
195 if not self._notifiers.has_key(name):
261 if not self._traitlet_notifiers.has_key(name):
196 262 nlist = []
197 self._notifiers[name] = nlist
263 self._traitlet_notifiers[name] = nlist
198 264 else:
199 nlist = self._notifiers[name]
265 nlist = self._traitlet_notifiers[name]
200 266 if handler not in nlist:
201 267 nlist.append(handler)
202 268
203 269 def _remove_notifiers(self, handler, name):
204 if self._notifiers.has_key(name):
205 nlist = self._notifiers[name]
270 if self._traitlet_notifiers.has_key(name):
271 nlist = self._traitlet_notifiers[name]
206 272 try:
207 273 index = nlist.index(handler)
208 274 except ValueError:
@@ -211,6 +277,30 b' class HasTraitlets(object):'
211 277 del nlist[index]
212 278
213 279 def on_traitlet_change(self, handler, name=None, remove=False):
280 """Setup a handler to be called when a traitlet changes.
281
282 This is used to setup dynamic notifications of traitlet changes.
283
284 Static handlers can be created by creating methods on a HasTraitlets
285 subclass with the naming convention '_[traitletname]_changed'. Thus,
286 to create static handler for the traitlet 'a', create the method
287 _a_changed(self, name, old, new) (fewer arguments can be used, see
288 below).
289
290 Parameters
291 ----------
292 handler : callable
293 A callable that is called when a traitlet changes. Its
294 signature can be handler(), handler(name), handler(name, new)
295 or handler(name, old, new).
296 name : list, str, None
297 If None, the handler will apply to all traitlets. If a list
298 of str, handler will apply to all names in the list. If a
299 str, the handler will apply just to that name.
300 remove : bool
301 If False (the default), then install the handler. If True
302 then unintall it.
303 """
214 304 if remove:
215 305 names = parse_notifier_name(name)
216 306 for n in names:
@@ -220,6 +310,28 b' class HasTraitlets(object):'
220 310 for n in names:
221 311 self._add_notifiers(handler, n)
222 312
313 def _add_class_traitlet(self, name, traitlet):
314 """Add a class-level traitlet.
315
316 This create a new traitlet attached to all instances of this class.
317 But, the value can be different on each instance. But, this behavior
318 is likely to trip up many folks as they would expect the traitlet
319 type to be different on each instance.
320
321 Parameters
322 ----------
323 name : str
324 The name of the traitlet.
325 traitlet : TraitletType or an instance of one
326 The traitlet to assign to the name.
327 """
328 if inspect.isclass(traitlet):
329 inst = traitlet()
330 else:
331 inst = traitlet
332 assert isinstance(inst, TraitletType)
333 inst.name = name
334 setattr(self.__class__, name, inst)
223 335
224 336 #-----------------------------------------------------------------------------
225 337 # Actual TraitletTypes implementations/subclasses
General Comments 0
You need to be logged in to leave comments. Login now