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