##// END OF EJS Templates
Fixes for parallel code on Python 3.
Thomas Kluyver -
Show More
@@ -1,445 +1,445
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 """
4 4 The IPython controller application.
5 5
6 6 Authors:
7 7
8 8 * Brian Granger
9 9 * MinRK
10 10
11 11 """
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Copyright (C) 2008-2011 The IPython Development Team
15 15 #
16 16 # Distributed under the terms of the BSD License. The full license is in
17 17 # the file COPYING, distributed as part of this software.
18 18 #-----------------------------------------------------------------------------
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Imports
22 22 #-----------------------------------------------------------------------------
23 23
24 24 from __future__ import with_statement
25 25
26 26 import os
27 27 import socket
28 28 import stat
29 29 import sys
30 30
31 31 from multiprocessing import Process
32 32
33 33 import zmq
34 34 from zmq.devices import ProcessMonitoredQueue
35 35 from zmq.log.handlers import PUBHandler
36 36 from zmq.utils import jsonapi as json
37 37
38 38 from IPython.core.profiledir import ProfileDir
39 39
40 40 from IPython.parallel.apps.baseapp import (
41 41 BaseParallelApplication,
42 42 base_aliases,
43 43 base_flags,
44 44 catch_config_error,
45 45 )
46 46 from IPython.utils.importstring import import_item
47 47 from IPython.utils.traitlets import Instance, Unicode, Bool, List, Dict, TraitError
48 48
49 49 from IPython.zmq.session import (
50 50 Session, session_aliases, session_flags, default_secure
51 51 )
52 52
53 53 from IPython.parallel.controller.heartmonitor import HeartMonitor
54 54 from IPython.parallel.controller.hub import HubFactory
55 55 from IPython.parallel.controller.scheduler import TaskScheduler,launch_scheduler
56 56 from IPython.parallel.controller.sqlitedb import SQLiteDB
57 57
58 58 from IPython.parallel.util import signal_children, split_url, asbytes, disambiguate_url
59 59
60 60 # conditional import of MongoDB backend class
61 61
62 62 try:
63 63 from IPython.parallel.controller.mongodb import MongoDB
64 64 except ImportError:
65 65 maybe_mongo = []
66 66 else:
67 67 maybe_mongo = [MongoDB]
68 68
69 69
70 70 #-----------------------------------------------------------------------------
71 71 # Module level variables
72 72 #-----------------------------------------------------------------------------
73 73
74 74
75 75 #: The default config file name for this application
76 76 default_config_file_name = u'ipcontroller_config.py'
77 77
78 78
79 79 _description = """Start the IPython controller for parallel computing.
80 80
81 81 The IPython controller provides a gateway between the IPython engines and
82 82 clients. The controller needs to be started before the engines and can be
83 83 configured using command line options or using a cluster directory. Cluster
84 84 directories contain config, log and security files and are usually located in
85 85 your ipython directory and named as "profile_name". See the `profile`
86 86 and `profile-dir` options for details.
87 87 """
88 88
89 89 _examples = """
90 90 ipcontroller --ip=192.168.0.1 --port=1000 # listen on ip, port for engines
91 91 ipcontroller --scheme=pure # use the pure zeromq scheduler
92 92 """
93 93
94 94
95 95 #-----------------------------------------------------------------------------
96 96 # The main application
97 97 #-----------------------------------------------------------------------------
98 98 flags = {}
99 99 flags.update(base_flags)
100 100 flags.update({
101 101 'usethreads' : ( {'IPControllerApp' : {'use_threads' : True}},
102 102 'Use threads instead of processes for the schedulers'),
103 103 'sqlitedb' : ({'HubFactory' : {'db_class' : 'IPython.parallel.controller.sqlitedb.SQLiteDB'}},
104 104 'use the SQLiteDB backend'),
105 105 'mongodb' : ({'HubFactory' : {'db_class' : 'IPython.parallel.controller.mongodb.MongoDB'}},
106 106 'use the MongoDB backend'),
107 107 'dictdb' : ({'HubFactory' : {'db_class' : 'IPython.parallel.controller.dictdb.DictDB'}},
108 108 'use the in-memory DictDB backend'),
109 109 'reuse' : ({'IPControllerApp' : {'reuse_files' : True}},
110 110 'reuse existing json connection files')
111 111 })
112 112
113 113 flags.update(session_flags)
114 114
115 115 aliases = dict(
116 116 ssh = 'IPControllerApp.ssh_server',
117 117 enginessh = 'IPControllerApp.engine_ssh_server',
118 118 location = 'IPControllerApp.location',
119 119
120 120 url = 'HubFactory.url',
121 121 ip = 'HubFactory.ip',
122 122 transport = 'HubFactory.transport',
123 123 port = 'HubFactory.regport',
124 124
125 125 ping = 'HeartMonitor.period',
126 126
127 127 scheme = 'TaskScheduler.scheme_name',
128 128 hwm = 'TaskScheduler.hwm',
129 129 )
130 130 aliases.update(base_aliases)
131 131 aliases.update(session_aliases)
132 132
133 133
134 134 class IPControllerApp(BaseParallelApplication):
135 135
136 136 name = u'ipcontroller'
137 137 description = _description
138 138 examples = _examples
139 139 config_file_name = Unicode(default_config_file_name)
140 140 classes = [ProfileDir, Session, HubFactory, TaskScheduler, HeartMonitor, SQLiteDB] + maybe_mongo
141 141
142 142 # change default to True
143 143 auto_create = Bool(True, config=True,
144 144 help="""Whether to create profile dir if it doesn't exist.""")
145 145
146 146 reuse_files = Bool(False, config=True,
147 147 help='Whether to reuse existing json connection files.'
148 148 )
149 149 ssh_server = Unicode(u'', config=True,
150 150 help="""ssh url for clients to use when connecting to the Controller
151 151 processes. It should be of the form: [user@]server[:port]. The
152 152 Controller's listening addresses must be accessible from the ssh server""",
153 153 )
154 154 engine_ssh_server = Unicode(u'', config=True,
155 155 help="""ssh url for engines to use when connecting to the Controller
156 156 processes. It should be of the form: [user@]server[:port]. The
157 157 Controller's listening addresses must be accessible from the ssh server""",
158 158 )
159 159 location = Unicode(u'', config=True,
160 160 help="""The external IP or domain name of the Controller, used for disambiguating
161 161 engine and client connections.""",
162 162 )
163 163 import_statements = List([], config=True,
164 164 help="import statements to be run at startup. Necessary in some environments"
165 165 )
166 166
167 167 use_threads = Bool(False, config=True,
168 168 help='Use threads instead of processes for the schedulers',
169 169 )
170 170
171 171 engine_json_file = Unicode('ipcontroller-engine.json', config=True,
172 172 help="JSON filename where engine connection info will be stored.")
173 173 client_json_file = Unicode('ipcontroller-client.json', config=True,
174 174 help="JSON filename where client connection info will be stored.")
175 175
176 176 def _cluster_id_changed(self, name, old, new):
177 177 super(IPControllerApp, self)._cluster_id_changed(name, old, new)
178 178 self.engine_json_file = "%s-engine.json" % self.name
179 179 self.client_json_file = "%s-client.json" % self.name
180 180
181 181
182 182 # internal
183 183 children = List()
184 184 mq_class = Unicode('zmq.devices.ProcessMonitoredQueue')
185 185
186 186 def _use_threads_changed(self, name, old, new):
187 187 self.mq_class = 'zmq.devices.%sMonitoredQueue'%('Thread' if new else 'Process')
188 188
189 189 aliases = Dict(aliases)
190 190 flags = Dict(flags)
191 191
192 192
193 193 def save_connection_dict(self, fname, cdict):
194 194 """save a connection dict to json file."""
195 195 c = self.config
196 196 url = cdict['url']
197 197 location = cdict['location']
198 198 if not location:
199 199 try:
200 200 proto,ip,port = split_url(url)
201 201 except AssertionError:
202 202 pass
203 203 else:
204 204 try:
205 205 location = socket.gethostbyname_ex(socket.gethostname())[2][-1]
206 206 except (socket.gaierror, IndexError):
207 207 self.log.warn("Could not identify this machine's IP, assuming 127.0.0.1."
208 208 " You may need to specify '--location=<external_ip_address>' to help"
209 209 " IPython decide when to connect via loopback.")
210 210 location = '127.0.0.1'
211 211 cdict['location'] = location
212 212 fname = os.path.join(self.profile_dir.security_dir, fname)
213 213 with open(fname, 'wb') as f:
214 214 f.write(json.dumps(cdict, indent=2))
215 215 os.chmod(fname, stat.S_IRUSR|stat.S_IWUSR)
216 216
217 217 def load_config_from_json(self):
218 218 """load config from existing json connector files."""
219 219 c = self.config
220 220 self.log.debug("loading config from JSON")
221 221 # load from engine config
222 222 with open(os.path.join(self.profile_dir.security_dir, self.engine_json_file)) as f:
223 223 cfg = json.loads(f.read())
224 224 key = c.Session.key = asbytes(cfg['exec_key'])
225 225 xport,addr = cfg['url'].split('://')
226 226 c.HubFactory.engine_transport = xport
227 227 ip,ports = addr.split(':')
228 228 c.HubFactory.engine_ip = ip
229 229 c.HubFactory.regport = int(ports)
230 230 self.location = cfg['location']
231 231 if not self.engine_ssh_server:
232 232 self.engine_ssh_server = cfg['ssh']
233 233 # load client config
234 234 with open(os.path.join(self.profile_dir.security_dir, self.client_json_file)) as f:
235 235 cfg = json.loads(f.read())
236 236 assert key == cfg['exec_key'], "exec_key mismatch between engine and client keys"
237 237 xport,addr = cfg['url'].split('://')
238 238 c.HubFactory.client_transport = xport
239 239 ip,ports = addr.split(':')
240 240 c.HubFactory.client_ip = ip
241 241 if not self.ssh_server:
242 242 self.ssh_server = cfg['ssh']
243 243 assert int(ports) == c.HubFactory.regport, "regport mismatch"
244 244
245 245 def load_secondary_config(self):
246 246 """secondary config, loading from JSON and setting defaults"""
247 247 if self.reuse_files:
248 248 try:
249 249 self.load_config_from_json()
250 250 except (AssertionError,IOError) as e:
251 251 self.log.error("Could not load config from JSON: %s" % e)
252 252 self.reuse_files=False
253 253 # switch Session.key default to secure
254 254 default_secure(self.config)
255 255 self.log.debug("Config changed")
256 256 self.log.debug(repr(self.config))
257 257
258 258 def init_hub(self):
259 259 c = self.config
260 260
261 261 self.do_import_statements()
262 262
263 263 try:
264 264 self.factory = HubFactory(config=c, log=self.log)
265 265 # self.start_logging()
266 266 self.factory.init_hub()
267 267 except TraitError:
268 268 raise
269 269 except Exception:
270 270 self.log.error("Couldn't construct the Controller", exc_info=True)
271 271 self.exit(1)
272 272
273 273 if not self.reuse_files:
274 274 # save to new json config files
275 275 f = self.factory
276 cdict = {'exec_key' : f.session.key,
276 cdict = {'exec_key' : f.session.key.decode('ascii'),
277 277 'ssh' : self.ssh_server,
278 278 'url' : "%s://%s:%s"%(f.client_transport, f.client_ip, f.regport),
279 279 'location' : self.location
280 280 }
281 281 self.save_connection_dict(self.client_json_file, cdict)
282 282 edict = cdict
283 283 edict['url']="%s://%s:%s"%((f.client_transport, f.client_ip, f.regport))
284 284 edict['ssh'] = self.engine_ssh_server
285 285 self.save_connection_dict(self.engine_json_file, edict)
286 286
287 287 #
288 288 def init_schedulers(self):
289 289 children = self.children
290 290 mq = import_item(str(self.mq_class))
291 291
292 292 hub = self.factory
293 293 # disambiguate url, in case of *
294 294 monitor_url = disambiguate_url(hub.monitor_url)
295 295 # maybe_inproc = 'inproc://monitor' if self.use_threads else monitor_url
296 296 # IOPub relay (in a Process)
297 297 q = mq(zmq.PUB, zmq.SUB, zmq.PUB, b'N/A',b'iopub')
298 298 q.bind_in(hub.client_info['iopub'])
299 299 q.bind_out(hub.engine_info['iopub'])
300 300 q.setsockopt_out(zmq.SUBSCRIBE, b'')
301 301 q.connect_mon(monitor_url)
302 302 q.daemon=True
303 303 children.append(q)
304 304
305 305 # Multiplexer Queue (in a Process)
306 306 q = mq(zmq.ROUTER, zmq.ROUTER, zmq.PUB, b'in', b'out')
307 307 q.bind_in(hub.client_info['mux'])
308 308 q.setsockopt_in(zmq.IDENTITY, b'mux')
309 309 q.bind_out(hub.engine_info['mux'])
310 310 q.connect_mon(monitor_url)
311 311 q.daemon=True
312 312 children.append(q)
313 313
314 314 # Control Queue (in a Process)
315 315 q = mq(zmq.ROUTER, zmq.ROUTER, zmq.PUB, b'incontrol', b'outcontrol')
316 316 q.bind_in(hub.client_info['control'])
317 317 q.setsockopt_in(zmq.IDENTITY, b'control')
318 318 q.bind_out(hub.engine_info['control'])
319 319 q.connect_mon(monitor_url)
320 320 q.daemon=True
321 321 children.append(q)
322 322 try:
323 323 scheme = self.config.TaskScheduler.scheme_name
324 324 except AttributeError:
325 325 scheme = TaskScheduler.scheme_name.get_default_value()
326 326 # Task Queue (in a Process)
327 327 if scheme == 'pure':
328 328 self.log.warn("task::using pure XREQ Task scheduler")
329 329 q = mq(zmq.ROUTER, zmq.DEALER, zmq.PUB, b'intask', b'outtask')
330 330 # q.setsockopt_out(zmq.HWM, hub.hwm)
331 331 q.bind_in(hub.client_info['task'][1])
332 332 q.setsockopt_in(zmq.IDENTITY, b'task')
333 333 q.bind_out(hub.engine_info['task'])
334 334 q.connect_mon(monitor_url)
335 335 q.daemon=True
336 336 children.append(q)
337 337 elif scheme == 'none':
338 338 self.log.warn("task::using no Task scheduler")
339 339
340 340 else:
341 341 self.log.info("task::using Python %s Task scheduler"%scheme)
342 342 sargs = (hub.client_info['task'][1], hub.engine_info['task'],
343 343 monitor_url, disambiguate_url(hub.client_info['notification']))
344 344 kwargs = dict(logname='scheduler', loglevel=self.log_level,
345 345 log_url = self.log_url, config=dict(self.config))
346 346 if 'Process' in self.mq_class:
347 347 # run the Python scheduler in a Process
348 348 q = Process(target=launch_scheduler, args=sargs, kwargs=kwargs)
349 349 q.daemon=True
350 350 children.append(q)
351 351 else:
352 352 # single-threaded Controller
353 353 kwargs['in_thread'] = True
354 354 launch_scheduler(*sargs, **kwargs)
355 355
356 356
357 357 def save_urls(self):
358 358 """save the registration urls to files."""
359 359 c = self.config
360 360
361 361 sec_dir = self.profile_dir.security_dir
362 362 cf = self.factory
363 363
364 364 with open(os.path.join(sec_dir, 'ipcontroller-engine.url'), 'w') as f:
365 365 f.write("%s://%s:%s"%(cf.engine_transport, cf.engine_ip, cf.regport))
366 366
367 367 with open(os.path.join(sec_dir, 'ipcontroller-client.url'), 'w') as f:
368 368 f.write("%s://%s:%s"%(cf.client_transport, cf.client_ip, cf.regport))
369 369
370 370
371 371 def do_import_statements(self):
372 372 statements = self.import_statements
373 373 for s in statements:
374 374 try:
375 375 self.log.msg("Executing statement: '%s'" % s)
376 376 exec s in globals(), locals()
377 377 except:
378 378 self.log.msg("Error running statement: %s" % s)
379 379
380 380 def forward_logging(self):
381 381 if self.log_url:
382 382 self.log.info("Forwarding logging to %s"%self.log_url)
383 383 context = zmq.Context.instance()
384 384 lsock = context.socket(zmq.PUB)
385 385 lsock.connect(self.log_url)
386 386 handler = PUBHandler(lsock)
387 387 self.log.removeHandler(self._log_handler)
388 388 handler.root_topic = 'controller'
389 389 handler.setLevel(self.log_level)
390 390 self.log.addHandler(handler)
391 391 self._log_handler = handler
392 392
393 393 @catch_config_error
394 394 def initialize(self, argv=None):
395 395 super(IPControllerApp, self).initialize(argv)
396 396 self.forward_logging()
397 397 self.load_secondary_config()
398 398 self.init_hub()
399 399 self.init_schedulers()
400 400
401 401 def start(self):
402 402 # Start the subprocesses:
403 403 self.factory.start()
404 404 child_procs = []
405 405 for child in self.children:
406 406 child.start()
407 407 if isinstance(child, ProcessMonitoredQueue):
408 408 child_procs.append(child.launcher)
409 409 elif isinstance(child, Process):
410 410 child_procs.append(child)
411 411 if child_procs:
412 412 signal_children(child_procs)
413 413
414 414 self.write_pid_file(overwrite=True)
415 415
416 416 try:
417 417 self.factory.loop.start()
418 418 except KeyboardInterrupt:
419 419 self.log.critical("Interrupted, Exiting...\n")
420 420
421 421
422 422
423 423 def launch_new_instance():
424 424 """Create and run the IPython controller"""
425 425 if sys.platform == 'win32':
426 426 # make sure we don't get called from a multiprocessing subprocess
427 427 # this can result in infinite Controllers being started on Windows
428 428 # which doesn't have a proper fork, so multiprocessing is wonky
429 429
430 430 # this only comes up when IPython has been installed using vanilla
431 431 # setuptools, and *not* distribute.
432 432 import multiprocessing
433 433 p = multiprocessing.current_process()
434 434 # the main process has name 'MainProcess'
435 435 # subprocesses will have names like 'Process-1'
436 436 if p.name != 'MainProcess':
437 437 # we are a subprocess, don't start another Controller!
438 438 return
439 439 app = IPControllerApp.instance()
440 440 app.initialize()
441 441 app.start()
442 442
443 443
444 444 if __name__ == '__main__':
445 445 launch_new_instance()
@@ -1,1394 +1,1394
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 Authors:
32 32
33 33 * Brian Granger
34 34 * Enthought, Inc. Some of the code in this file comes from enthought.traits
35 35 and is licensed under the BSD license. Also, many of the ideas also come
36 36 from enthought.traits even though our implementation is very different.
37 37 """
38 38
39 39 #-----------------------------------------------------------------------------
40 40 # Copyright (C) 2008-2009 The IPython Development Team
41 41 #
42 42 # Distributed under the terms of the BSD License. The full license is in
43 43 # the file COPYING, distributed as part of this software.
44 44 #-----------------------------------------------------------------------------
45 45
46 46 #-----------------------------------------------------------------------------
47 47 # Imports
48 48 #-----------------------------------------------------------------------------
49 49
50 50
51 51 import inspect
52 52 import re
53 53 import sys
54 54 import types
55 55 from types import FunctionType
56 56 try:
57 57 from types import ClassType, InstanceType
58 58 ClassTypes = (ClassType, type)
59 59 except:
60 60 ClassTypes = (type,)
61 61
62 62 from .importstring import import_item
63 63 from IPython.utils import py3compat
64 64
65 65 SequenceTypes = (list, tuple, set, frozenset)
66 66
67 67 #-----------------------------------------------------------------------------
68 68 # Basic classes
69 69 #-----------------------------------------------------------------------------
70 70
71 71
72 72 class NoDefaultSpecified ( object ): pass
73 73 NoDefaultSpecified = NoDefaultSpecified()
74 74
75 75
76 76 class Undefined ( object ): pass
77 77 Undefined = Undefined()
78 78
79 79 class TraitError(Exception):
80 80 pass
81 81
82 82 #-----------------------------------------------------------------------------
83 83 # Utilities
84 84 #-----------------------------------------------------------------------------
85 85
86 86
87 87 def class_of ( object ):
88 88 """ Returns a string containing the class name of an object with the
89 89 correct indefinite article ('a' or 'an') preceding it (e.g., 'an Image',
90 90 'a PlotValue').
91 91 """
92 92 if isinstance( object, basestring ):
93 93 return add_article( object )
94 94
95 95 return add_article( object.__class__.__name__ )
96 96
97 97
98 98 def add_article ( name ):
99 99 """ Returns a string containing the correct indefinite article ('a' or 'an')
100 100 prefixed to the specified string.
101 101 """
102 102 if name[:1].lower() in 'aeiou':
103 103 return 'an ' + name
104 104
105 105 return 'a ' + name
106 106
107 107
108 108 def repr_type(obj):
109 109 """ Return a string representation of a value and its type for readable
110 110 error messages.
111 111 """
112 112 the_type = type(obj)
113 113 if (not py3compat.PY3) and the_type is InstanceType:
114 114 # Old-style class.
115 115 the_type = obj.__class__
116 116 msg = '%r %r' % (obj, the_type)
117 117 return msg
118 118
119 119
120 120 def parse_notifier_name(name):
121 121 """Convert the name argument to a list of names.
122 122
123 123 Examples
124 124 --------
125 125
126 126 >>> parse_notifier_name('a')
127 127 ['a']
128 128 >>> parse_notifier_name(['a','b'])
129 129 ['a', 'b']
130 130 >>> parse_notifier_name(None)
131 131 ['anytrait']
132 132 """
133 133 if isinstance(name, str):
134 134 return [name]
135 135 elif name is None:
136 136 return ['anytrait']
137 137 elif isinstance(name, (list, tuple)):
138 138 for n in name:
139 139 assert isinstance(n, str), "names must be strings"
140 140 return name
141 141
142 142
143 143 class _SimpleTest:
144 144 def __init__ ( self, value ): self.value = value
145 145 def __call__ ( self, test ):
146 146 return test == self.value
147 147 def __repr__(self):
148 148 return "<SimpleTest(%r)" % self.value
149 149 def __str__(self):
150 150 return self.__repr__()
151 151
152 152
153 153 def getmembers(object, predicate=None):
154 154 """A safe version of inspect.getmembers that handles missing attributes.
155 155
156 156 This is useful when there are descriptor based attributes that for
157 157 some reason raise AttributeError even though they exist. This happens
158 158 in zope.inteface with the __provides__ attribute.
159 159 """
160 160 results = []
161 161 for key in dir(object):
162 162 try:
163 163 value = getattr(object, key)
164 164 except AttributeError:
165 165 pass
166 166 else:
167 167 if not predicate or predicate(value):
168 168 results.append((key, value))
169 169 results.sort()
170 170 return results
171 171
172 172
173 173 #-----------------------------------------------------------------------------
174 174 # Base TraitType for all traits
175 175 #-----------------------------------------------------------------------------
176 176
177 177
178 178 class TraitType(object):
179 179 """A base class for all trait descriptors.
180 180
181 181 Notes
182 182 -----
183 183 Our implementation of traits is based on Python's descriptor
184 184 prototol. This class is the base class for all such descriptors. The
185 185 only magic we use is a custom metaclass for the main :class:`HasTraits`
186 186 class that does the following:
187 187
188 188 1. Sets the :attr:`name` attribute of every :class:`TraitType`
189 189 instance in the class dict to the name of the attribute.
190 190 2. Sets the :attr:`this_class` attribute of every :class:`TraitType`
191 191 instance in the class dict to the *class* that declared the trait.
192 192 This is used by the :class:`This` trait to allow subclasses to
193 193 accept superclasses for :class:`This` values.
194 194 """
195 195
196 196
197 197 metadata = {}
198 198 default_value = Undefined
199 199 info_text = 'any value'
200 200
201 201 def __init__(self, default_value=NoDefaultSpecified, **metadata):
202 202 """Create a TraitType.
203 203 """
204 204 if default_value is not NoDefaultSpecified:
205 205 self.default_value = default_value
206 206
207 207 if len(metadata) > 0:
208 208 if len(self.metadata) > 0:
209 209 self._metadata = self.metadata.copy()
210 210 self._metadata.update(metadata)
211 211 else:
212 212 self._metadata = metadata
213 213 else:
214 214 self._metadata = self.metadata
215 215
216 216 self.init()
217 217
218 218 def init(self):
219 219 pass
220 220
221 221 def get_default_value(self):
222 222 """Create a new instance of the default value."""
223 223 return self.default_value
224 224
225 225 def instance_init(self, obj):
226 226 """This is called by :meth:`HasTraits.__new__` to finish init'ing.
227 227
228 228 Some stages of initialization must be delayed until the parent
229 229 :class:`HasTraits` instance has been created. This method is
230 230 called in :meth:`HasTraits.__new__` after the instance has been
231 231 created.
232 232
233 233 This method trigger the creation and validation of default values
234 234 and also things like the resolution of str given class names in
235 235 :class:`Type` and :class`Instance`.
236 236
237 237 Parameters
238 238 ----------
239 239 obj : :class:`HasTraits` instance
240 240 The parent :class:`HasTraits` instance that has just been
241 241 created.
242 242 """
243 243 self.set_default_value(obj)
244 244
245 245 def set_default_value(self, obj):
246 246 """Set the default value on a per instance basis.
247 247
248 248 This method is called by :meth:`instance_init` to create and
249 249 validate the default value. The creation and validation of
250 250 default values must be delayed until the parent :class:`HasTraits`
251 251 class has been instantiated.
252 252 """
253 253 # Check for a deferred initializer defined in the same class as the
254 254 # trait declaration or above.
255 255 mro = type(obj).mro()
256 256 meth_name = '_%s_default' % self.name
257 257 for cls in mro[:mro.index(self.this_class)+1]:
258 258 if meth_name in cls.__dict__:
259 259 break
260 260 else:
261 261 # We didn't find one. Do static initialization.
262 262 dv = self.get_default_value()
263 263 newdv = self._validate(obj, dv)
264 264 obj._trait_values[self.name] = newdv
265 265 return
266 266 # Complete the dynamic initialization.
267 267 obj._trait_dyn_inits[self.name] = cls.__dict__[meth_name]
268 268
269 269 def __get__(self, obj, cls=None):
270 270 """Get the value of the trait by self.name for the instance.
271 271
272 272 Default values are instantiated when :meth:`HasTraits.__new__`
273 273 is called. Thus by the time this method gets called either the
274 274 default value or a user defined value (they called :meth:`__set__`)
275 275 is in the :class:`HasTraits` instance.
276 276 """
277 277 if obj is None:
278 278 return self
279 279 else:
280 280 try:
281 281 value = obj._trait_values[self.name]
282 282 except KeyError:
283 283 # Check for a dynamic initializer.
284 284 if self.name in obj._trait_dyn_inits:
285 285 value = obj._trait_dyn_inits[self.name](obj)
286 286 # FIXME: Do we really validate here?
287 287 value = self._validate(obj, value)
288 288 obj._trait_values[self.name] = value
289 289 return value
290 290 else:
291 291 raise TraitError('Unexpected error in TraitType: '
292 292 'both default value and dynamic initializer are '
293 293 'absent.')
294 294 except Exception:
295 295 # HasTraits should call set_default_value to populate
296 296 # this. So this should never be reached.
297 297 raise TraitError('Unexpected error in TraitType: '
298 298 'default value not set properly')
299 299 else:
300 300 return value
301 301
302 302 def __set__(self, obj, value):
303 303 new_value = self._validate(obj, value)
304 304 old_value = self.__get__(obj)
305 305 if old_value != new_value:
306 306 obj._trait_values[self.name] = new_value
307 307 obj._notify_trait(self.name, old_value, new_value)
308 308
309 309 def _validate(self, obj, value):
310 310 if hasattr(self, 'validate'):
311 311 return self.validate(obj, value)
312 312 elif hasattr(self, 'is_valid_for'):
313 313 valid = self.is_valid_for(value)
314 314 if valid:
315 315 return value
316 316 else:
317 317 raise TraitError('invalid value for type: %r' % value)
318 318 elif hasattr(self, 'value_for'):
319 319 return self.value_for(value)
320 320 else:
321 321 return value
322 322
323 323 def info(self):
324 324 return self.info_text
325 325
326 326 def error(self, obj, value):
327 327 if obj is not None:
328 328 e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
329 329 % (self.name, class_of(obj),
330 330 self.info(), repr_type(value))
331 331 else:
332 332 e = "The '%s' trait must be %s, but a value of %r was specified." \
333 333 % (self.name, self.info(), repr_type(value))
334 334 raise TraitError(e)
335 335
336 336 def get_metadata(self, key):
337 337 return getattr(self, '_metadata', {}).get(key, None)
338 338
339 339 def set_metadata(self, key, value):
340 340 getattr(self, '_metadata', {})[key] = value
341 341
342 342
343 343 #-----------------------------------------------------------------------------
344 344 # The HasTraits implementation
345 345 #-----------------------------------------------------------------------------
346 346
347 347
348 348 class MetaHasTraits(type):
349 349 """A metaclass for HasTraits.
350 350
351 351 This metaclass makes sure that any TraitType class attributes are
352 352 instantiated and sets their name attribute.
353 353 """
354 354
355 355 def __new__(mcls, name, bases, classdict):
356 356 """Create the HasTraits class.
357 357
358 358 This instantiates all TraitTypes in the class dict and sets their
359 359 :attr:`name` attribute.
360 360 """
361 361 # print "MetaHasTraitlets (mcls, name): ", mcls, name
362 362 # print "MetaHasTraitlets (bases): ", bases
363 363 # print "MetaHasTraitlets (classdict): ", classdict
364 364 for k,v in classdict.iteritems():
365 365 if isinstance(v, TraitType):
366 366 v.name = k
367 367 elif inspect.isclass(v):
368 368 if issubclass(v, TraitType):
369 369 vinst = v()
370 370 vinst.name = k
371 371 classdict[k] = vinst
372 372 return super(MetaHasTraits, mcls).__new__(mcls, name, bases, classdict)
373 373
374 374 def __init__(cls, name, bases, classdict):
375 375 """Finish initializing the HasTraits class.
376 376
377 377 This sets the :attr:`this_class` attribute of each TraitType in the
378 378 class dict to the newly created class ``cls``.
379 379 """
380 380 for k, v in classdict.iteritems():
381 381 if isinstance(v, TraitType):
382 382 v.this_class = cls
383 383 super(MetaHasTraits, cls).__init__(name, bases, classdict)
384 384
385 385 class HasTraits(object):
386 386
387 387 __metaclass__ = MetaHasTraits
388 388
389 389 def __new__(cls, **kw):
390 390 # This is needed because in Python 2.6 object.__new__ only accepts
391 391 # the cls argument.
392 392 new_meth = super(HasTraits, cls).__new__
393 393 if new_meth is object.__new__:
394 394 inst = new_meth(cls)
395 395 else:
396 396 inst = new_meth(cls, **kw)
397 397 inst._trait_values = {}
398 398 inst._trait_notifiers = {}
399 399 inst._trait_dyn_inits = {}
400 400 # Here we tell all the TraitType instances to set their default
401 401 # values on the instance.
402 402 for key in dir(cls):
403 403 # Some descriptors raise AttributeError like zope.interface's
404 404 # __provides__ attributes even though they exist. This causes
405 405 # AttributeErrors even though they are listed in dir(cls).
406 406 try:
407 407 value = getattr(cls, key)
408 408 except AttributeError:
409 409 pass
410 410 else:
411 411 if isinstance(value, TraitType):
412 412 value.instance_init(inst)
413 413
414 414 return inst
415 415
416 416 def __init__(self, **kw):
417 417 # Allow trait values to be set using keyword arguments.
418 418 # We need to use setattr for this to trigger validation and
419 419 # notifications.
420 420 for key, value in kw.iteritems():
421 421 setattr(self, key, value)
422 422
423 423 def _notify_trait(self, name, old_value, new_value):
424 424
425 425 # First dynamic ones
426 426 callables = self._trait_notifiers.get(name,[])
427 427 more_callables = self._trait_notifiers.get('anytrait',[])
428 428 callables.extend(more_callables)
429 429
430 430 # Now static ones
431 431 try:
432 432 cb = getattr(self, '_%s_changed' % name)
433 433 except:
434 434 pass
435 435 else:
436 436 callables.append(cb)
437 437
438 438 # Call them all now
439 439 for c in callables:
440 440 # Traits catches and logs errors here. I allow them to raise
441 441 if callable(c):
442 442 argspec = inspect.getargspec(c)
443 443 nargs = len(argspec[0])
444 444 # Bound methods have an additional 'self' argument
445 445 # I don't know how to treat unbound methods, but they
446 446 # can't really be used for callbacks.
447 447 if isinstance(c, types.MethodType):
448 448 offset = -1
449 449 else:
450 450 offset = 0
451 451 if nargs + offset == 0:
452 452 c()
453 453 elif nargs + offset == 1:
454 454 c(name)
455 455 elif nargs + offset == 2:
456 456 c(name, new_value)
457 457 elif nargs + offset == 3:
458 458 c(name, old_value, new_value)
459 459 else:
460 460 raise TraitError('a trait changed callback '
461 461 'must have 0-3 arguments.')
462 462 else:
463 463 raise TraitError('a trait changed callback '
464 464 'must be callable.')
465 465
466 466
467 467 def _add_notifiers(self, handler, name):
468 468 if not self._trait_notifiers.has_key(name):
469 469 nlist = []
470 470 self._trait_notifiers[name] = nlist
471 471 else:
472 472 nlist = self._trait_notifiers[name]
473 473 if handler not in nlist:
474 474 nlist.append(handler)
475 475
476 476 def _remove_notifiers(self, handler, name):
477 477 if self._trait_notifiers.has_key(name):
478 478 nlist = self._trait_notifiers[name]
479 479 try:
480 480 index = nlist.index(handler)
481 481 except ValueError:
482 482 pass
483 483 else:
484 484 del nlist[index]
485 485
486 486 def on_trait_change(self, handler, name=None, remove=False):
487 487 """Setup a handler to be called when a trait changes.
488 488
489 489 This is used to setup dynamic notifications of trait changes.
490 490
491 491 Static handlers can be created by creating methods on a HasTraits
492 492 subclass with the naming convention '_[traitname]_changed'. Thus,
493 493 to create static handler for the trait 'a', create the method
494 494 _a_changed(self, name, old, new) (fewer arguments can be used, see
495 495 below).
496 496
497 497 Parameters
498 498 ----------
499 499 handler : callable
500 500 A callable that is called when a trait changes. Its
501 501 signature can be handler(), handler(name), handler(name, new)
502 502 or handler(name, old, new).
503 503 name : list, str, None
504 504 If None, the handler will apply to all traits. If a list
505 505 of str, handler will apply to all names in the list. If a
506 506 str, the handler will apply just to that name.
507 507 remove : bool
508 508 If False (the default), then install the handler. If True
509 509 then unintall it.
510 510 """
511 511 if remove:
512 512 names = parse_notifier_name(name)
513 513 for n in names:
514 514 self._remove_notifiers(handler, n)
515 515 else:
516 516 names = parse_notifier_name(name)
517 517 for n in names:
518 518 self._add_notifiers(handler, n)
519 519
520 520 @classmethod
521 521 def class_trait_names(cls, **metadata):
522 522 """Get a list of all the names of this classes traits.
523 523
524 524 This method is just like the :meth:`trait_names` method, but is unbound.
525 525 """
526 526 return cls.class_traits(**metadata).keys()
527 527
528 528 @classmethod
529 529 def class_traits(cls, **metadata):
530 530 """Get a list of all the traits of this class.
531 531
532 532 This method is just like the :meth:`traits` method, but is unbound.
533 533
534 534 The TraitTypes returned don't know anything about the values
535 535 that the various HasTrait's instances are holding.
536 536
537 537 This follows the same algorithm as traits does and does not allow
538 538 for any simple way of specifying merely that a metadata name
539 539 exists, but has any value. This is because get_metadata returns
540 540 None if a metadata key doesn't exist.
541 541 """
542 542 traits = dict([memb for memb in getmembers(cls) if \
543 543 isinstance(memb[1], TraitType)])
544 544
545 545 if len(metadata) == 0:
546 546 return traits
547 547
548 548 for meta_name, meta_eval in metadata.items():
549 549 if type(meta_eval) is not FunctionType:
550 550 metadata[meta_name] = _SimpleTest(meta_eval)
551 551
552 552 result = {}
553 553 for name, trait in traits.items():
554 554 for meta_name, meta_eval in metadata.items():
555 555 if not meta_eval(trait.get_metadata(meta_name)):
556 556 break
557 557 else:
558 558 result[name] = trait
559 559
560 560 return result
561 561
562 562 def trait_names(self, **metadata):
563 563 """Get a list of all the names of this classes traits."""
564 564 return self.traits(**metadata).keys()
565 565
566 566 def traits(self, **metadata):
567 567 """Get a list of all the traits of this class.
568 568
569 569 The TraitTypes returned don't know anything about the values
570 570 that the various HasTrait's instances are holding.
571 571
572 572 This follows the same algorithm as traits does and does not allow
573 573 for any simple way of specifying merely that a metadata name
574 574 exists, but has any value. This is because get_metadata returns
575 575 None if a metadata key doesn't exist.
576 576 """
577 577 traits = dict([memb for memb in getmembers(self.__class__) if \
578 578 isinstance(memb[1], TraitType)])
579 579
580 580 if len(metadata) == 0:
581 581 return traits
582 582
583 583 for meta_name, meta_eval in metadata.items():
584 584 if type(meta_eval) is not FunctionType:
585 585 metadata[meta_name] = _SimpleTest(meta_eval)
586 586
587 587 result = {}
588 588 for name, trait in traits.items():
589 589 for meta_name, meta_eval in metadata.items():
590 590 if not meta_eval(trait.get_metadata(meta_name)):
591 591 break
592 592 else:
593 593 result[name] = trait
594 594
595 595 return result
596 596
597 597 def trait_metadata(self, traitname, key):
598 598 """Get metadata values for trait by key."""
599 599 try:
600 600 trait = getattr(self.__class__, traitname)
601 601 except AttributeError:
602 602 raise TraitError("Class %s does not have a trait named %s" %
603 603 (self.__class__.__name__, traitname))
604 604 else:
605 605 return trait.get_metadata(key)
606 606
607 607 #-----------------------------------------------------------------------------
608 608 # Actual TraitTypes implementations/subclasses
609 609 #-----------------------------------------------------------------------------
610 610
611 611 #-----------------------------------------------------------------------------
612 612 # TraitTypes subclasses for handling classes and instances of classes
613 613 #-----------------------------------------------------------------------------
614 614
615 615
616 616 class ClassBasedTraitType(TraitType):
617 617 """A trait with error reporting for Type, Instance and This."""
618 618
619 619 def error(self, obj, value):
620 620 kind = type(value)
621 621 if (not py3compat.PY3) and kind is InstanceType:
622 622 msg = 'class %s' % value.__class__.__name__
623 623 else:
624 624 msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) )
625 625
626 626 if obj is not None:
627 627 e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
628 628 % (self.name, class_of(obj),
629 629 self.info(), msg)
630 630 else:
631 631 e = "The '%s' trait must be %s, but a value of %r was specified." \
632 632 % (self.name, self.info(), msg)
633 633
634 634 raise TraitError(e)
635 635
636 636
637 637 class Type(ClassBasedTraitType):
638 638 """A trait whose value must be a subclass of a specified class."""
639 639
640 640 def __init__ (self, default_value=None, klass=None, allow_none=True, **metadata ):
641 641 """Construct a Type trait
642 642
643 643 A Type trait specifies that its values must be subclasses of
644 644 a particular class.
645 645
646 646 If only ``default_value`` is given, it is used for the ``klass`` as
647 647 well.
648 648
649 649 Parameters
650 650 ----------
651 651 default_value : class, str or None
652 652 The default value must be a subclass of klass. If an str,
653 653 the str must be a fully specified class name, like 'foo.bar.Bah'.
654 654 The string is resolved into real class, when the parent
655 655 :class:`HasTraits` class is instantiated.
656 656 klass : class, str, None
657 657 Values of this trait must be a subclass of klass. The klass
658 658 may be specified in a string like: 'foo.bar.MyClass'.
659 659 The string is resolved into real class, when the parent
660 660 :class:`HasTraits` class is instantiated.
661 661 allow_none : boolean
662 662 Indicates whether None is allowed as an assignable value. Even if
663 663 ``False``, the default value may be ``None``.
664 664 """
665 665 if default_value is None:
666 666 if klass is None:
667 667 klass = object
668 668 elif klass is None:
669 669 klass = default_value
670 670
671 671 if not (inspect.isclass(klass) or isinstance(klass, basestring)):
672 672 raise TraitError("A Type trait must specify a class.")
673 673
674 674 self.klass = klass
675 675 self._allow_none = allow_none
676 676
677 677 super(Type, self).__init__(default_value, **metadata)
678 678
679 679 def validate(self, obj, value):
680 680 """Validates that the value is a valid object instance."""
681 681 try:
682 682 if issubclass(value, self.klass):
683 683 return value
684 684 except:
685 685 if (value is None) and (self._allow_none):
686 686 return value
687 687
688 688 self.error(obj, value)
689 689
690 690 def info(self):
691 691 """ Returns a description of the trait."""
692 692 if isinstance(self.klass, basestring):
693 693 klass = self.klass
694 694 else:
695 695 klass = self.klass.__name__
696 696 result = 'a subclass of ' + klass
697 697 if self._allow_none:
698 698 return result + ' or None'
699 699 return result
700 700
701 701 def instance_init(self, obj):
702 702 self._resolve_classes()
703 703 super(Type, self).instance_init(obj)
704 704
705 705 def _resolve_classes(self):
706 706 if isinstance(self.klass, basestring):
707 707 self.klass = import_item(self.klass)
708 708 if isinstance(self.default_value, basestring):
709 709 self.default_value = import_item(self.default_value)
710 710
711 711 def get_default_value(self):
712 712 return self.default_value
713 713
714 714
715 715 class DefaultValueGenerator(object):
716 716 """A class for generating new default value instances."""
717 717
718 718 def __init__(self, *args, **kw):
719 719 self.args = args
720 720 self.kw = kw
721 721
722 722 def generate(self, klass):
723 723 return klass(*self.args, **self.kw)
724 724
725 725
726 726 class Instance(ClassBasedTraitType):
727 727 """A trait whose value must be an instance of a specified class.
728 728
729 729 The value can also be an instance of a subclass of the specified class.
730 730 """
731 731
732 732 def __init__(self, klass=None, args=None, kw=None,
733 733 allow_none=True, **metadata ):
734 734 """Construct an Instance trait.
735 735
736 736 This trait allows values that are instances of a particular
737 737 class or its sublclasses. Our implementation is quite different
738 738 from that of enthough.traits as we don't allow instances to be used
739 739 for klass and we handle the ``args`` and ``kw`` arguments differently.
740 740
741 741 Parameters
742 742 ----------
743 743 klass : class, str
744 744 The class that forms the basis for the trait. Class names
745 745 can also be specified as strings, like 'foo.bar.Bar'.
746 746 args : tuple
747 747 Positional arguments for generating the default value.
748 748 kw : dict
749 749 Keyword arguments for generating the default value.
750 750 allow_none : bool
751 751 Indicates whether None is allowed as a value.
752 752
753 753 Default Value
754 754 -------------
755 755 If both ``args`` and ``kw`` are None, then the default value is None.
756 756 If ``args`` is a tuple and ``kw`` is a dict, then the default is
757 757 created as ``klass(*args, **kw)``. If either ``args`` or ``kw`` is
758 758 not (but not both), None is replace by ``()`` or ``{}``.
759 759 """
760 760
761 761 self._allow_none = allow_none
762 762
763 763 if (klass is None) or (not (inspect.isclass(klass) or isinstance(klass, basestring))):
764 764 raise TraitError('The klass argument must be a class'
765 765 ' you gave: %r' % klass)
766 766 self.klass = klass
767 767
768 768 # self.klass is a class, so handle default_value
769 769 if args is None and kw is None:
770 770 default_value = None
771 771 else:
772 772 if args is None:
773 773 # kw is not None
774 774 args = ()
775 775 elif kw is None:
776 776 # args is not None
777 777 kw = {}
778 778
779 779 if not isinstance(kw, dict):
780 780 raise TraitError("The 'kw' argument must be a dict or None.")
781 781 if not isinstance(args, tuple):
782 782 raise TraitError("The 'args' argument must be a tuple or None.")
783 783
784 784 default_value = DefaultValueGenerator(*args, **kw)
785 785
786 786 super(Instance, self).__init__(default_value, **metadata)
787 787
788 788 def validate(self, obj, value):
789 789 if value is None:
790 790 if self._allow_none:
791 791 return value
792 792 self.error(obj, value)
793 793
794 794 if isinstance(value, self.klass):
795 795 return value
796 796 else:
797 797 self.error(obj, value)
798 798
799 799 def info(self):
800 800 if isinstance(self.klass, basestring):
801 801 klass = self.klass
802 802 else:
803 803 klass = self.klass.__name__
804 804 result = class_of(klass)
805 805 if self._allow_none:
806 806 return result + ' or None'
807 807
808 808 return result
809 809
810 810 def instance_init(self, obj):
811 811 self._resolve_classes()
812 812 super(Instance, self).instance_init(obj)
813 813
814 814 def _resolve_classes(self):
815 815 if isinstance(self.klass, basestring):
816 816 self.klass = import_item(self.klass)
817 817
818 818 def get_default_value(self):
819 819 """Instantiate a default value instance.
820 820
821 821 This is called when the containing HasTraits classes'
822 822 :meth:`__new__` method is called to ensure that a unique instance
823 823 is created for each HasTraits instance.
824 824 """
825 825 dv = self.default_value
826 826 if isinstance(dv, DefaultValueGenerator):
827 827 return dv.generate(self.klass)
828 828 else:
829 829 return dv
830 830
831 831
832 832 class This(ClassBasedTraitType):
833 833 """A trait for instances of the class containing this trait.
834 834
835 835 Because how how and when class bodies are executed, the ``This``
836 836 trait can only have a default value of None. This, and because we
837 837 always validate default values, ``allow_none`` is *always* true.
838 838 """
839 839
840 840 info_text = 'an instance of the same type as the receiver or None'
841 841
842 842 def __init__(self, **metadata):
843 843 super(This, self).__init__(None, **metadata)
844 844
845 845 def validate(self, obj, value):
846 846 # What if value is a superclass of obj.__class__? This is
847 847 # complicated if it was the superclass that defined the This
848 848 # trait.
849 849 if isinstance(value, self.this_class) or (value is None):
850 850 return value
851 851 else:
852 852 self.error(obj, value)
853 853
854 854
855 855 #-----------------------------------------------------------------------------
856 856 # Basic TraitTypes implementations/subclasses
857 857 #-----------------------------------------------------------------------------
858 858
859 859
860 860 class Any(TraitType):
861 861 default_value = None
862 862 info_text = 'any value'
863 863
864 864
865 865 class Int(TraitType):
866 866 """A integer trait."""
867 867
868 868 default_value = 0
869 869 info_text = 'an integer'
870 870
871 871 def validate(self, obj, value):
872 872 if isinstance(value, int):
873 873 return value
874 874 self.error(obj, value)
875 875
876 876 class CInt(Int):
877 877 """A casting version of the int trait."""
878 878
879 879 def validate(self, obj, value):
880 880 try:
881 881 return int(value)
882 882 except:
883 883 self.error(obj, value)
884 884
885 885 if py3compat.PY3:
886 886 Long, CLong = Int, CInt
887 887 else:
888 888 class Long(TraitType):
889 889 """A long integer trait."""
890 890
891 891 default_value = 0L
892 892 info_text = 'a long'
893 893
894 894 def validate(self, obj, value):
895 895 if isinstance(value, long):
896 896 return value
897 897 if isinstance(value, int):
898 898 return long(value)
899 899 self.error(obj, value)
900 900
901 901
902 902 class CLong(Long):
903 903 """A casting version of the long integer trait."""
904 904
905 905 def validate(self, obj, value):
906 906 try:
907 907 return long(value)
908 908 except:
909 909 self.error(obj, value)
910 910
911 911
912 912 class Float(TraitType):
913 913 """A float trait."""
914 914
915 915 default_value = 0.0
916 916 info_text = 'a float'
917 917
918 918 def validate(self, obj, value):
919 919 if isinstance(value, float):
920 920 return value
921 921 if isinstance(value, int):
922 922 return float(value)
923 923 self.error(obj, value)
924 924
925 925
926 926 class CFloat(Float):
927 927 """A casting version of the float trait."""
928 928
929 929 def validate(self, obj, value):
930 930 try:
931 931 return float(value)
932 932 except:
933 933 self.error(obj, value)
934 934
935 935 class Complex(TraitType):
936 936 """A trait for complex numbers."""
937 937
938 938 default_value = 0.0 + 0.0j
939 939 info_text = 'a complex number'
940 940
941 941 def validate(self, obj, value):
942 942 if isinstance(value, complex):
943 943 return value
944 944 if isinstance(value, (float, int)):
945 945 return complex(value)
946 946 self.error(obj, value)
947 947
948 948
949 949 class CComplex(Complex):
950 950 """A casting version of the complex number trait."""
951 951
952 952 def validate (self, obj, value):
953 953 try:
954 954 return complex(value)
955 955 except:
956 956 self.error(obj, value)
957 957
958 958 # We should always be explicit about whether we're using bytes or unicode, both
959 959 # for Python 3 conversion and for reliable unicode behaviour on Python 2. So
960 960 # we don't have a Str type.
961 961 class Bytes(TraitType):
962 962 """A trait for byte strings."""
963 963
964 default_value = ''
964 default_value = b''
965 965 info_text = 'a string'
966 966
967 967 def validate(self, obj, value):
968 968 if isinstance(value, bytes):
969 969 return value
970 970 self.error(obj, value)
971 971
972 972
973 973 class CBytes(Bytes):
974 974 """A casting version of the byte string trait."""
975 975
976 976 def validate(self, obj, value):
977 977 try:
978 978 return bytes(value)
979 979 except:
980 980 self.error(obj, value)
981 981
982 982
983 983 class Unicode(TraitType):
984 984 """A trait for unicode strings."""
985 985
986 986 default_value = u''
987 987 info_text = 'a unicode string'
988 988
989 989 def validate(self, obj, value):
990 990 if isinstance(value, unicode):
991 991 return value
992 992 if isinstance(value, bytes):
993 993 return unicode(value)
994 994 self.error(obj, value)
995 995
996 996
997 997 class CUnicode(Unicode):
998 998 """A casting version of the unicode trait."""
999 999
1000 1000 def validate(self, obj, value):
1001 1001 try:
1002 1002 return unicode(value)
1003 1003 except:
1004 1004 self.error(obj, value)
1005 1005
1006 1006
1007 1007 class ObjectName(TraitType):
1008 1008 """A string holding a valid object name in this version of Python.
1009 1009
1010 1010 This does not check that the name exists in any scope."""
1011 1011 info_text = "a valid object identifier in Python"
1012 1012
1013 1013 if py3compat.PY3:
1014 1014 # Python 3:
1015 1015 coerce_str = staticmethod(lambda _,s: s)
1016 1016
1017 1017 else:
1018 1018 # Python 2:
1019 1019 def coerce_str(self, obj, value):
1020 1020 "In Python 2, coerce ascii-only unicode to str"
1021 1021 if isinstance(value, unicode):
1022 1022 try:
1023 1023 return str(value)
1024 1024 except UnicodeEncodeError:
1025 1025 self.error(obj, value)
1026 1026 return value
1027 1027
1028 1028 def validate(self, obj, value):
1029 1029 value = self.coerce_str(obj, value)
1030 1030
1031 1031 if isinstance(value, str) and py3compat.isidentifier(value):
1032 1032 return value
1033 1033 self.error(obj, value)
1034 1034
1035 1035 class DottedObjectName(ObjectName):
1036 1036 """A string holding a valid dotted object name in Python, such as A.b3._c"""
1037 1037 def validate(self, obj, value):
1038 1038 value = self.coerce_str(obj, value)
1039 1039
1040 1040 if isinstance(value, str) and py3compat.isidentifier(value, dotted=True):
1041 1041 return value
1042 1042 self.error(obj, value)
1043 1043
1044 1044
1045 1045 class Bool(TraitType):
1046 1046 """A boolean (True, False) trait."""
1047 1047
1048 1048 default_value = False
1049 1049 info_text = 'a boolean'
1050 1050
1051 1051 def validate(self, obj, value):
1052 1052 if isinstance(value, bool):
1053 1053 return value
1054 1054 self.error(obj, value)
1055 1055
1056 1056
1057 1057 class CBool(Bool):
1058 1058 """A casting version of the boolean trait."""
1059 1059
1060 1060 def validate(self, obj, value):
1061 1061 try:
1062 1062 return bool(value)
1063 1063 except:
1064 1064 self.error(obj, value)
1065 1065
1066 1066
1067 1067 class Enum(TraitType):
1068 1068 """An enum that whose value must be in a given sequence."""
1069 1069
1070 1070 def __init__(self, values, default_value=None, allow_none=True, **metadata):
1071 1071 self.values = values
1072 1072 self._allow_none = allow_none
1073 1073 super(Enum, self).__init__(default_value, **metadata)
1074 1074
1075 1075 def validate(self, obj, value):
1076 1076 if value is None:
1077 1077 if self._allow_none:
1078 1078 return value
1079 1079
1080 1080 if value in self.values:
1081 1081 return value
1082 1082 self.error(obj, value)
1083 1083
1084 1084 def info(self):
1085 1085 """ Returns a description of the trait."""
1086 1086 result = 'any of ' + repr(self.values)
1087 1087 if self._allow_none:
1088 1088 return result + ' or None'
1089 1089 return result
1090 1090
1091 1091 class CaselessStrEnum(Enum):
1092 1092 """An enum of strings that are caseless in validate."""
1093 1093
1094 1094 def validate(self, obj, value):
1095 1095 if value is None:
1096 1096 if self._allow_none:
1097 1097 return value
1098 1098
1099 1099 if not isinstance(value, basestring):
1100 1100 self.error(obj, value)
1101 1101
1102 1102 for v in self.values:
1103 1103 if v.lower() == value.lower():
1104 1104 return v
1105 1105 self.error(obj, value)
1106 1106
1107 1107 class Container(Instance):
1108 1108 """An instance of a container (list, set, etc.)
1109 1109
1110 1110 To be subclassed by overriding klass.
1111 1111 """
1112 1112 klass = None
1113 1113 _valid_defaults = SequenceTypes
1114 1114 _trait = None
1115 1115
1116 1116 def __init__(self, trait=None, default_value=None, allow_none=True,
1117 1117 **metadata):
1118 1118 """Create a container trait type from a list, set, or tuple.
1119 1119
1120 1120 The default value is created by doing ``List(default_value)``,
1121 1121 which creates a copy of the ``default_value``.
1122 1122
1123 1123 ``trait`` can be specified, which restricts the type of elements
1124 1124 in the container to that TraitType.
1125 1125
1126 1126 If only one arg is given and it is not a Trait, it is taken as
1127 1127 ``default_value``:
1128 1128
1129 1129 ``c = List([1,2,3])``
1130 1130
1131 1131 Parameters
1132 1132 ----------
1133 1133
1134 1134 trait : TraitType [ optional ]
1135 1135 the type for restricting the contents of the Container. If unspecified,
1136 1136 types are not checked.
1137 1137
1138 1138 default_value : SequenceType [ optional ]
1139 1139 The default value for the Trait. Must be list/tuple/set, and
1140 1140 will be cast to the container type.
1141 1141
1142 1142 allow_none : Bool [ default True ]
1143 1143 Whether to allow the value to be None
1144 1144
1145 1145 **metadata : any
1146 1146 further keys for extensions to the Trait (e.g. config)
1147 1147
1148 1148 """
1149 1149 istrait = lambda t: isinstance(t, type) and issubclass(t, TraitType)
1150 1150
1151 1151 # allow List([values]):
1152 1152 if default_value is None and not istrait(trait):
1153 1153 default_value = trait
1154 1154 trait = None
1155 1155
1156 1156 if default_value is None:
1157 1157 args = ()
1158 1158 elif isinstance(default_value, self._valid_defaults):
1159 1159 args = (default_value,)
1160 1160 else:
1161 1161 raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value))
1162 1162
1163 1163 if istrait(trait):
1164 1164 self._trait = trait()
1165 1165 self._trait.name = 'element'
1166 1166 elif trait is not None:
1167 1167 raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait))
1168 1168
1169 1169 super(Container,self).__init__(klass=self.klass, args=args,
1170 1170 allow_none=allow_none, **metadata)
1171 1171
1172 1172 def element_error(self, obj, element, validator):
1173 1173 e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \
1174 1174 % (self.name, class_of(obj), validator.info(), repr_type(element))
1175 1175 raise TraitError(e)
1176 1176
1177 1177 def validate(self, obj, value):
1178 1178 value = super(Container, self).validate(obj, value)
1179 1179 if value is None:
1180 1180 return value
1181 1181
1182 1182 value = self.validate_elements(obj, value)
1183 1183
1184 1184 return value
1185 1185
1186 1186 def validate_elements(self, obj, value):
1187 1187 validated = []
1188 1188 if self._trait is None or isinstance(self._trait, Any):
1189 1189 return value
1190 1190 for v in value:
1191 1191 try:
1192 1192 v = self._trait.validate(obj, v)
1193 1193 except TraitError:
1194 1194 self.element_error(obj, v, self._trait)
1195 1195 else:
1196 1196 validated.append(v)
1197 1197 return self.klass(validated)
1198 1198
1199 1199
1200 1200 class List(Container):
1201 1201 """An instance of a Python list."""
1202 1202 klass = list
1203 1203
1204 1204 def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxint,
1205 1205 allow_none=True, **metadata):
1206 1206 """Create a List trait type from a list, set, or tuple.
1207 1207
1208 1208 The default value is created by doing ``List(default_value)``,
1209 1209 which creates a copy of the ``default_value``.
1210 1210
1211 1211 ``trait`` can be specified, which restricts the type of elements
1212 1212 in the container to that TraitType.
1213 1213
1214 1214 If only one arg is given and it is not a Trait, it is taken as
1215 1215 ``default_value``:
1216 1216
1217 1217 ``c = List([1,2,3])``
1218 1218
1219 1219 Parameters
1220 1220 ----------
1221 1221
1222 1222 trait : TraitType [ optional ]
1223 1223 the type for restricting the contents of the Container. If unspecified,
1224 1224 types are not checked.
1225 1225
1226 1226 default_value : SequenceType [ optional ]
1227 1227 The default value for the Trait. Must be list/tuple/set, and
1228 1228 will be cast to the container type.
1229 1229
1230 1230 minlen : Int [ default 0 ]
1231 1231 The minimum length of the input list
1232 1232
1233 1233 maxlen : Int [ default sys.maxint ]
1234 1234 The maximum length of the input list
1235 1235
1236 1236 allow_none : Bool [ default True ]
1237 1237 Whether to allow the value to be None
1238 1238
1239 1239 **metadata : any
1240 1240 further keys for extensions to the Trait (e.g. config)
1241 1241
1242 1242 """
1243 1243 self._minlen = minlen
1244 1244 self._maxlen = maxlen
1245 1245 super(List, self).__init__(trait=trait, default_value=default_value,
1246 1246 allow_none=allow_none, **metadata)
1247 1247
1248 1248 def length_error(self, obj, value):
1249 1249 e = "The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified." \
1250 1250 % (self.name, class_of(obj), self._minlen, self._maxlen, value)
1251 1251 raise TraitError(e)
1252 1252
1253 1253 def validate_elements(self, obj, value):
1254 1254 length = len(value)
1255 1255 if length < self._minlen or length > self._maxlen:
1256 1256 self.length_error(obj, value)
1257 1257
1258 1258 return super(List, self).validate_elements(obj, value)
1259 1259
1260 1260
1261 1261 class Set(Container):
1262 1262 """An instance of a Python set."""
1263 1263 klass = set
1264 1264
1265 1265 class Tuple(Container):
1266 1266 """An instance of a Python tuple."""
1267 1267 klass = tuple
1268 1268
1269 1269 def __init__(self, *traits, **metadata):
1270 1270 """Tuple(*traits, default_value=None, allow_none=True, **medatata)
1271 1271
1272 1272 Create a tuple from a list, set, or tuple.
1273 1273
1274 1274 Create a fixed-type tuple with Traits:
1275 1275
1276 1276 ``t = Tuple(Int, Str, CStr)``
1277 1277
1278 1278 would be length 3, with Int,Str,CStr for each element.
1279 1279
1280 1280 If only one arg is given and it is not a Trait, it is taken as
1281 1281 default_value:
1282 1282
1283 1283 ``t = Tuple((1,2,3))``
1284 1284
1285 1285 Otherwise, ``default_value`` *must* be specified by keyword.
1286 1286
1287 1287 Parameters
1288 1288 ----------
1289 1289
1290 1290 *traits : TraitTypes [ optional ]
1291 1291 the tsype for restricting the contents of the Tuple. If unspecified,
1292 1292 types are not checked. If specified, then each positional argument
1293 1293 corresponds to an element of the tuple. Tuples defined with traits
1294 1294 are of fixed length.
1295 1295
1296 1296 default_value : SequenceType [ optional ]
1297 1297 The default value for the Tuple. Must be list/tuple/set, and
1298 1298 will be cast to a tuple. If `traits` are specified, the
1299 1299 `default_value` must conform to the shape and type they specify.
1300 1300
1301 1301 allow_none : Bool [ default True ]
1302 1302 Whether to allow the value to be None
1303 1303
1304 1304 **metadata : any
1305 1305 further keys for extensions to the Trait (e.g. config)
1306 1306
1307 1307 """
1308 1308 default_value = metadata.pop('default_value', None)
1309 1309 allow_none = metadata.pop('allow_none', True)
1310 1310
1311 1311 istrait = lambda t: isinstance(t, type) and issubclass(t, TraitType)
1312 1312
1313 1313 # allow Tuple((values,)):
1314 1314 if len(traits) == 1 and default_value is None and not istrait(traits[0]):
1315 1315 default_value = traits[0]
1316 1316 traits = ()
1317 1317
1318 1318 if default_value is None:
1319 1319 args = ()
1320 1320 elif isinstance(default_value, self._valid_defaults):
1321 1321 args = (default_value,)
1322 1322 else:
1323 1323 raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value))
1324 1324
1325 1325 self._traits = []
1326 1326 for trait in traits:
1327 1327 t = trait()
1328 1328 t.name = 'element'
1329 1329 self._traits.append(t)
1330 1330
1331 1331 if self._traits and default_value is None:
1332 1332 # don't allow default to be an empty container if length is specified
1333 1333 args = None
1334 1334 super(Container,self).__init__(klass=self.klass, args=args,
1335 1335 allow_none=allow_none, **metadata)
1336 1336
1337 1337 def validate_elements(self, obj, value):
1338 1338 if not self._traits:
1339 1339 # nothing to validate
1340 1340 return value
1341 1341 if len(value) != len(self._traits):
1342 1342 e = "The '%s' trait of %s instance requires %i elements, but a value of %s was specified." \
1343 1343 % (self.name, class_of(obj), len(self._traits), repr_type(value))
1344 1344 raise TraitError(e)
1345 1345
1346 1346 validated = []
1347 1347 for t,v in zip(self._traits, value):
1348 1348 try:
1349 1349 v = t.validate(obj, v)
1350 1350 except TraitError:
1351 1351 self.element_error(obj, v, t)
1352 1352 else:
1353 1353 validated.append(v)
1354 1354 return tuple(validated)
1355 1355
1356 1356
1357 1357 class Dict(Instance):
1358 1358 """An instance of a Python dict."""
1359 1359
1360 1360 def __init__(self, default_value=None, allow_none=True, **metadata):
1361 1361 """Create a dict trait type from a dict.
1362 1362
1363 1363 The default value is created by doing ``dict(default_value)``,
1364 1364 which creates a copy of the ``default_value``.
1365 1365 """
1366 1366 if default_value is None:
1367 1367 args = ((),)
1368 1368 elif isinstance(default_value, dict):
1369 1369 args = (default_value,)
1370 1370 elif isinstance(default_value, SequenceTypes):
1371 1371 args = (default_value,)
1372 1372 else:
1373 1373 raise TypeError('default value of Dict was %s' % default_value)
1374 1374
1375 1375 super(Dict,self).__init__(klass=dict, args=args,
1376 1376 allow_none=allow_none, **metadata)
1377 1377
1378 1378 class TCPAddress(TraitType):
1379 1379 """A trait for an (ip, port) tuple.
1380 1380
1381 1381 This allows for both IPv4 IP addresses as well as hostnames.
1382 1382 """
1383 1383
1384 1384 default_value = ('127.0.0.1', 0)
1385 1385 info_text = 'an (ip, port) tuple'
1386 1386
1387 1387 def validate(self, obj, value):
1388 1388 if isinstance(value, tuple):
1389 1389 if len(value) == 2:
1390 1390 if isinstance(value[0], basestring) and isinstance(value[1], int):
1391 1391 port = value[1]
1392 1392 if port >= 0 and port <= 65535:
1393 1393 return value
1394 1394 self.error(obj, value)
General Comments 0
You need to be logged in to leave comments. Login now