##// END OF EJS Templates
cocoa frontend tests
Barry Wark -
Show More
@@ -1,97 +1,79 b''
1 1 # encoding: utf-8
2 2 """This file contains unittests for the ipython1.frontend.cocoa.cocoa_frontend module.
3 3
4 4 Things that should be tested:
5 5
6 6 - IPythonCocoaController instantiates an IEngineInteractive
7 7 - IPythonCocoaController executes code on the engine
8 8 - IPythonCocoaController returns continuation for incomplete code
9 9 - IPythonCocoaController returns failure for exceptions raised in executed code
10 10 - IPythonCocoaController mirrors engine's user_ns
11 11 """
12 12 __docformat__ = "restructuredtext en"
13 13
14 14 #-------------------------------------------------------------------------------
15 15 # Copyright (C) 2005 Fernando Perez <fperez@colorado.edu>
16 16 # Brian E Granger <ellisonbg@gmail.com>
17 17 # Benjamin Ragan-Kelley <benjaminrk@gmail.com>
18 18 #
19 19 # Distributed under the terms of the BSD License. The full license is in
20 20 # the file COPYING, distributed as part of this software.
21 21 #-------------------------------------------------------------------------------
22 22
23 23 #-------------------------------------------------------------------------------
24 24 # Imports
25 25 #-------------------------------------------------------------------------------
26 26 from IPython.kernel.core.interpreter import Interpreter
27 from IPython.testutils.parametric import Parametric, parametric
28 from IPython.kernel.core.interpreter import COMPILER_ERROR, INCOMPLETE_INPUT,\
29 COMPLETE_INPUT
30 27 import IPython.kernel.engineservice as es
31 from IPython.testutils.util import DeferredTestCase
28 from IPython.testing.util import DeferredTestCase
32 29 from twisted.internet.defer import succeed
33 from IPython.frontend.cocoa.cocoa_frontend import IPythonCocoaController,\
34 IPythonCLITextViewDelegate,\
35 CompilerError
30 from IPython.frontend.cocoa.cocoa_frontend import IPythonCocoaController
36 31
32 from Foundation import NSMakeRect
33 from AppKit import NSTextView, NSScrollView
37 34
38 35 class TestIPythonCocoaControler(DeferredTestCase):
39 36 """Tests for IPythonCocoaController"""
40 37
41 38 def setUp(self):
42 39 self.controller = IPythonCocoaController.alloc().init()
43 self.controller.awakeFromNib()
44 40 self.engine = es.EngineService()
45 41 self.engine.startService()
46 42
47 43
48 44 def tearDown(self):
49 45 self.controller = None
50 46 self.engine.stopService()
51 47
52 48 def testControllerExecutesCode(self):
53 49 code ="""5+5"""
54 50 expected = Interpreter().execute(code)
55 51 del expected['number']
56 52 def removeNumberAndID(result):
57 53 del result['number']
58 54 del result['id']
59 55 return result
60 self.assertDeferredEquals(self.controller.executeRequest([code]).addCallback(removeNumberAndID), expected)
61
62 def testControllerReturnsNoneForIncompleteCode(self):
63 code = """def test(a):"""
64 expected = None
65 self.assertDeferredEquals(self.controller.executeRequest([code]), expected)
66
67
68 def testControllerRaisesCompilerErrorForIllegalCode(self):
69 """testControllerRaisesCompilerErrorForIllegalCode"""
70
71 code = """def test() pass"""
72 self.assertDeferredRaises(self.controller.executeRequest([code]), CompilerError)
56 self.assertDeferredEquals(self.controller.execute(code).addCallback(removeNumberAndID), expected)
73 57
74 58 def testControllerMirrorsUserNSWithValuesAsStrings(self):
75 59 code = """userns1=1;userns2=2"""
76 60 def testControllerUserNS(result):
77 self.assertEquals(self.controller.userNS['userns1'], str(1))
78 self.assertEquals(self.controller.userNS['userns2'], str(2))
61 self.assertEquals(self.controller.userNS['userns1'], 1)
62 self.assertEquals(self.controller.userNS['userns2'], 2)
79 63
80 self.controller.executeRequest([code]).addCallback(testControllerUserNS)
64 self.controller.execute(code).addCallback(testControllerUserNS)
81 65
82 66
83 67 def testControllerInstantiatesIEngine(self):
84 self.assert_(es.IEngine.providedBy(self.controller.engine))
68 self.assert_(es.IEngineBase.providedBy(self.controller.engine))
85 69
86 70 def testControllerCompletesToken(self):
87 71 code = """longNameVariable=10"""
88 72 def testCompletes(result):
89 73 self.assert_("longNameVariable" in result)
90 74
91 75 def testCompleteToken(result):
92 76 self.controller.complete("longNa").addCallback(testCompletes)
93 77
94 self.controller.executeRequest([code]).addCallback(testCompletes)
78 self.controller.execute(code).addCallback(testCompletes)
95 79
96
97 Parametric(TestIPythonCocoaControler) No newline at end of file
@@ -1,867 +1,874 b''
1 1 # encoding: utf-8
2 2 # -*- test-case-name: IPython.kernel.tests.test_engineservice -*-
3 3
4 4 """A Twisted Service Representation of the IPython core.
5 5
6 6 The IPython Core exposed to the network is called the Engine. Its
7 7 representation in Twisted in the EngineService. Interfaces and adapters
8 8 are used to abstract out the details of the actual network protocol used.
9 9 The EngineService is an Engine that knows nothing about the actual protocol
10 10 used.
11 11
12 12 The EngineService is exposed with various network protocols in modules like:
13 13
14 14 enginepb.py
15 15 enginevanilla.py
16 16
17 17 As of 12/12/06 the classes in this module have been simplified greatly. It was
18 18 felt that we had over-engineered things. To improve the maintainability of the
19 19 code we have taken out the ICompleteEngine interface and the completeEngine
20 20 method that automatically added methods to engines.
21 21
22 22 """
23 23
24 24 __docformat__ = "restructuredtext en"
25 25
26 26 #-------------------------------------------------------------------------------
27 27 # Copyright (C) 2008 The IPython Development Team
28 28 #
29 29 # Distributed under the terms of the BSD License. The full license is in
30 30 # the file COPYING, distributed as part of this software.
31 31 #-------------------------------------------------------------------------------
32 32
33 33 #-------------------------------------------------------------------------------
34 34 # Imports
35 35 #-------------------------------------------------------------------------------
36 36
37 37 import os, sys, copy
38 38 import cPickle as pickle
39 39 from new import instancemethod
40 40
41 41 from twisted.application import service
42 42 from twisted.internet import defer, reactor
43 43 from twisted.python import log, failure, components
44 44 import zope.interface as zi
45 45
46 46 from IPython.kernel.core.interpreter import Interpreter
47 47 from IPython.kernel import newserialized, error, util
48 48 from IPython.kernel.util import printer
49 49 from IPython.kernel.twistedutil import gatherBoth, DeferredList
50 50 from IPython.kernel import codeutil
51 51
52 52
53 53 #-------------------------------------------------------------------------------
54 54 # Interface specification for the Engine
55 55 #-------------------------------------------------------------------------------
56 56
57 57 class IEngineCore(zi.Interface):
58 58 """The minimal required interface for the IPython Engine.
59 59
60 60 This interface provides a formal specification of the IPython core.
61 61 All these methods should return deferreds regardless of what side of a
62 62 network connection they are on.
63 63
64 64 In general, this class simply wraps a shell class and wraps its return
65 65 values as Deferred objects. If the underlying shell class method raises
66 66 an exception, this class should convert it to a twisted.failure.Failure
67 67 that will be propagated along the Deferred's errback chain.
68 68
69 69 In addition, Failures are aggressive. By this, we mean that if a method
70 70 is performing multiple actions (like pulling multiple object) if any
71 71 single one fails, the entire method will fail with that Failure. It is
72 72 all or nothing.
73 73 """
74 74
75 75 id = zi.interface.Attribute("the id of the Engine object")
76 76 properties = zi.interface.Attribute("A dict of properties of the Engine")
77 77
78 78 def execute(lines):
79 79 """Execute lines of Python code.
80 80
81 81 Returns a dictionary with keys (id, number, stdin, stdout, stderr)
82 82 upon success.
83 83
84 84 Returns a failure object if the execution of lines raises an exception.
85 85 """
86 86
87 87 def push(namespace):
88 88 """Push dict namespace into the user's namespace.
89 89
90 90 Returns a deferred to None or a failure.
91 91 """
92 92
93 93 def pull(keys):
94 94 """Pulls values out of the user's namespace by keys.
95 95
96 96 Returns a deferred to a tuple objects or a single object.
97 97
98 98 Raises NameError if any one of objects doess not exist.
99 99 """
100 100
101 101 def push_function(namespace):
102 102 """Push a dict of key, function pairs into the user's namespace.
103 103
104 104 Returns a deferred to None or a failure."""
105 105
106 106 def pull_function(keys):
107 107 """Pulls functions out of the user's namespace by keys.
108 108
109 109 Returns a deferred to a tuple of functions or a single function.
110 110
111 111 Raises NameError if any one of the functions does not exist.
112 112 """
113 113
114 114 def get_result(i=None):
115 115 """Get the stdin/stdout/stderr of command i.
116 116
117 117 Returns a deferred to a dict with keys
118 118 (id, number, stdin, stdout, stderr).
119 119
120 120 Raises IndexError if command i does not exist.
121 121 Raises TypeError if i in not an int.
122 122 """
123 123
124 124 def reset():
125 125 """Reset the shell.
126 126
127 127 This clears the users namespace. Won't cause modules to be
128 128 reloaded. Should also re-initialize certain variables like id.
129 129 """
130 130
131 131 def kill():
132 132 """Kill the engine by stopping the reactor."""
133 133
134 134 def keys():
135 135 """Return the top level variables in the users namspace.
136 136
137 137 Returns a deferred to a dict."""
138 138
139 139
140 140 class IEngineSerialized(zi.Interface):
141 141 """Push/Pull methods that take Serialized objects.
142 142
143 143 All methods should return deferreds.
144 144 """
145 145
146 146 def push_serialized(namespace):
147 147 """Push a dict of keys and Serialized objects into the user's namespace."""
148 148
149 149 def pull_serialized(keys):
150 150 """Pull objects by key from the user's namespace as Serialized.
151 151
152 152 Returns a list of or one Serialized.
153 153
154 154 Raises NameError is any one of the objects does not exist.
155 155 """
156 156
157 157
158 158 class IEngineProperties(zi.Interface):
159 159 """Methods for access to the properties object of an Engine"""
160 160
161 161 properties = zi.Attribute("A StrictDict object, containing the properties")
162 162
163 163 def set_properties(properties):
164 164 """set properties by key and value"""
165 165
166 166 def get_properties(keys=None):
167 167 """get a list of properties by `keys`, if no keys specified, get all"""
168 168
169 169 def del_properties(keys):
170 170 """delete properties by `keys`"""
171 171
172 172 def has_properties(keys):
173 173 """get a list of bool values for whether `properties` has `keys`"""
174 174
175 175 def clear_properties():
176 176 """clear the properties dict"""
177 177
178 178 class IEngineBase(IEngineCore, IEngineSerialized, IEngineProperties):
179 179 """The basic engine interface that EngineService will implement.
180 180
181 181 This exists so it is easy to specify adapters that adapt to and from the
182 182 API that the basic EngineService implements.
183 183 """
184 184 pass
185 185
186 186 class IEngineQueued(IEngineBase):
187 187 """Interface for adding a queue to an IEngineBase.
188 188
189 189 This interface extends the IEngineBase interface to add methods for managing
190 190 the engine's queue. The implicit details of this interface are that the
191 191 execution of all methods declared in IEngineBase should appropriately be
192 192 put through a queue before execution.
193 193
194 194 All methods should return deferreds.
195 195 """
196 196
197 197 def clear_queue():
198 198 """Clear the queue."""
199 199
200 200 def queue_status():
201 201 """Get the queued and pending commands in the queue."""
202 202
203 203 def register_failure_observer(obs):
204 204 """Register an observer of pending Failures.
205 205
206 206 The observer must implement IFailureObserver.
207 207 """
208 208
209 209 def unregister_failure_observer(obs):
210 210 """Unregister an observer of pending Failures."""
211 211
212 212
213 213 class IEngineThreaded(zi.Interface):
214 214 """A place holder for threaded commands.
215 215
216 216 All methods should return deferreds.
217 217 """
218 218 pass
219 219
220 220
221 221 #-------------------------------------------------------------------------------
222 222 # Functions and classes to implement the EngineService
223 223 #-------------------------------------------------------------------------------
224 224
225 225
226 226 class StrictDict(dict):
227 227 """This is a strict copying dictionary for use as the interface to the
228 228 properties of an Engine.
229 229 :IMPORTANT:
230 230 This object copies the values you set to it, and returns copies to you
231 231 when you request them. The only way to change properties os explicitly
232 232 through the setitem and getitem of the dictionary interface.
233 233 Example:
234 234 >>> e = kernel.get_engine(id)
235 235 >>> L = someList
236 236 >>> e.properties['L'] = L
237 237 >>> L == e.properties['L']
238 238 ... True
239 239 >>> L.append(something Else)
240 240 >>> L == e.properties['L']
241 241 ... False
242 242
243 243 getitem copies, so calls to methods of objects do not affect the
244 244 properties, as in the following example:
245 245 >>> e.properties[1] = range(2)
246 246 >>> print e.properties[1]
247 247 ... [0, 1]
248 248 >>> e.properties[1].append(2)
249 249 >>> print e.properties[1]
250 250 ... [0, 1]
251 251
252 252 """
253 253 def __init__(self, *args, **kwargs):
254 254 dict.__init__(self, *args, **kwargs)
255 255 self.modified = True
256 256
257 257 def __getitem__(self, key):
258 258 return copy.deepcopy(dict.__getitem__(self, key))
259 259
260 260 def __setitem__(self, key, value):
261 261 # check if this entry is valid for transport around the network
262 262 # and copying
263 263 try:
264 264 pickle.dumps(key, 2)
265 265 pickle.dumps(value, 2)
266 266 newvalue = copy.deepcopy(value)
267 267 except:
268 268 raise error.InvalidProperty(value)
269 269 dict.__setitem__(self, key, newvalue)
270 270 self.modified = True
271 271
272 272 def __delitem__(self, key):
273 273 dict.__delitem__(self, key)
274 274 self.modified = True
275 275
276 276 def update(self, dikt):
277 277 for k,v in dikt.iteritems():
278 278 self[k] = v
279 279
280 280 def pop(self, key):
281 281 self.modified = True
282 282 return dict.pop(self, key)
283 283
284 284 def popitem(self):
285 285 self.modified = True
286 286 return dict.popitem(self)
287 287
288 288 def clear(self):
289 289 self.modified = True
290 290 dict.clear(self)
291 291
292 292 def subDict(self, *keys):
293 293 d = {}
294 294 for key in keys:
295 295 d[key] = self[key]
296 296 return d
297 297
298 298
299 299
300 300 class EngineAPI(object):
301 301 """This is the object through which the user can edit the `properties`
302 302 attribute of an Engine.
303 303 The Engine Properties object copies all object in and out of itself.
304 304 See the EngineProperties object for details.
305 305 """
306 306 _fix=False
307 307 def __init__(self, id):
308 308 self.id = id
309 309 self.properties = StrictDict()
310 310 self._fix=True
311 311
312 312 def __setattr__(self, k,v):
313 313 if self._fix:
314 314 raise error.KernelError("I am protected!")
315 315 else:
316 316 object.__setattr__(self, k, v)
317 317
318 318 def __delattr__(self, key):
319 319 raise error.KernelError("I am protected!")
320 320
321 321
322 322 _apiDict = {}
323 323
324 324 def get_engine(id):
325 325 """Get the Engine API object, whcih currently just provides the properties
326 326 object, by ID"""
327 327 global _apiDict
328 328 if not _apiDict.get(id):
329 329 _apiDict[id] = EngineAPI(id)
330 330 return _apiDict[id]
331 331
332 332 def drop_engine(id):
333 333 """remove an engine"""
334 334 global _apiDict
335 335 if _apiDict.has_key(id):
336 336 del _apiDict[id]
337 337
338 338 class EngineService(object, service.Service):
339 339 """Adapt a IPython shell into a IEngine implementing Twisted Service."""
340 340
341 341 zi.implements(IEngineBase)
342 342 name = 'EngineService'
343 343
344 344 def __init__(self, shellClass=Interpreter, mpi=None):
345 345 """Create an EngineService.
346 346
347 347 shellClass: something that implements IInterpreter or core1
348 348 mpi: an mpi module that has rank and size attributes
349 349 """
350 350 self.shellClass = shellClass
351 351 self.shell = self.shellClass()
352 352 self.mpi = mpi
353 353 self.id = None
354 354 self.properties = get_engine(self.id).properties
355 355 if self.mpi is not None:
356 356 log.msg("MPI started with rank = %i and size = %i" %
357 357 (self.mpi.rank, self.mpi.size))
358 358 self.id = self.mpi.rank
359 359 self._seedNamespace()
360 360
361 361 # Make id a property so that the shell can get the updated id
362 362
363 363 def _setID(self, id):
364 364 self._id = id
365 365 self.properties = get_engine(id).properties
366 366 self.shell.push({'id': id})
367 367
368 368 def _getID(self):
369 369 return self._id
370 370
371 371 id = property(_getID, _setID)
372 372
373 373 def _seedNamespace(self):
374 374 self.shell.push({'mpi': self.mpi, 'id' : self.id})
375 375
376 376 def executeAndRaise(self, msg, callable, *args, **kwargs):
377 377 """Call a method of self.shell and wrap any exception."""
378 378 d = defer.Deferred()
379 379 try:
380 380 result = callable(*args, **kwargs)
381 381 except:
382 382 # This gives the following:
383 383 # et=exception class
384 384 # ev=exception class instance
385 385 # tb=traceback object
386 386 et,ev,tb = sys.exc_info()
387 387 # This call adds attributes to the exception value
388 388 et,ev,tb = self.shell.formatTraceback(et,ev,tb,msg)
389 389 # Add another attribute
390 390 ev._ipython_engine_info = msg
391 391 f = failure.Failure(ev,et,None)
392 392 d.errback(f)
393 393 else:
394 394 d.callback(result)
395 395
396 396 return d
397 397
398 398 # The IEngine methods. See the interface for documentation.
399 399
400 400 def execute(self, lines):
401 401 msg = {'engineid':self.id,
402 402 'method':'execute',
403 403 'args':[lines]}
404 404 d = self.executeAndRaise(msg, self.shell.execute, lines)
405 405 d.addCallback(self.addIDToResult)
406 406 return d
407 407
408 408 def addIDToResult(self, result):
409 409 result['id'] = self.id
410 410 return result
411 411
412 412 def push(self, namespace):
413 413 msg = {'engineid':self.id,
414 414 'method':'push',
415 415 'args':[repr(namespace.keys())]}
416 416 d = self.executeAndRaise(msg, self.shell.push, namespace)
417 417 return d
418 418
419 419 def pull(self, keys):
420 420 msg = {'engineid':self.id,
421 421 'method':'pull',
422 422 'args':[repr(keys)]}
423 423 d = self.executeAndRaise(msg, self.shell.pull, keys)
424 424 return d
425 425
426 426 def push_function(self, namespace):
427 427 msg = {'engineid':self.id,
428 428 'method':'push_function',
429 429 'args':[repr(namespace.keys())]}
430 430 d = self.executeAndRaise(msg, self.shell.push_function, namespace)
431 431 return d
432 432
433 433 def pull_function(self, keys):
434 434 msg = {'engineid':self.id,
435 435 'method':'pull_function',
436 436 'args':[repr(keys)]}
437 437 d = self.executeAndRaise(msg, self.shell.pull_function, keys)
438 438 return d
439 439
440 440 def get_result(self, i=None):
441 441 msg = {'engineid':self.id,
442 442 'method':'get_result',
443 443 'args':[repr(i)]}
444 444 d = self.executeAndRaise(msg, self.shell.getCommand, i)
445 445 d.addCallback(self.addIDToResult)
446 446 return d
447 447
448 448 def reset(self):
449 449 msg = {'engineid':self.id,
450 450 'method':'reset',
451 451 'args':[]}
452 452 del self.shell
453 453 self.shell = self.shellClass()
454 454 self.properties.clear()
455 455 d = self.executeAndRaise(msg, self._seedNamespace)
456 456 return d
457 457
458 458 def kill(self):
459 459 drop_engine(self.id)
460 460 try:
461 461 reactor.stop()
462 462 except RuntimeError:
463 463 log.msg('The reactor was not running apparently.')
464 464 return defer.fail()
465 465 else:
466 466 return defer.succeed(None)
467 467
468 468 def keys(self):
469 469 """Return a list of variables names in the users top level namespace.
470 470
471 471 This used to return a dict of all the keys/repr(values) in the
472 472 user's namespace. This was too much info for the ControllerService
473 473 to handle so it is now just a list of keys.
474 474 """
475 475
476 476 remotes = []
477 477 for k in self.shell.user_ns.iterkeys():
478 478 if k not in ['__name__', '_ih', '_oh', '__builtins__',
479 479 'In', 'Out', '_', '__', '___', '__IP', 'input', 'raw_input']:
480 480 remotes.append(k)
481 481 return defer.succeed(remotes)
482 482
483 483 def set_properties(self, properties):
484 484 msg = {'engineid':self.id,
485 485 'method':'set_properties',
486 486 'args':[repr(properties.keys())]}
487 487 return self.executeAndRaise(msg, self.properties.update, properties)
488 488
489 489 def get_properties(self, keys=None):
490 490 msg = {'engineid':self.id,
491 491 'method':'get_properties',
492 492 'args':[repr(keys)]}
493 493 if keys is None:
494 494 keys = self.properties.keys()
495 495 return self.executeAndRaise(msg, self.properties.subDict, *keys)
496 496
497 497 def _doDel(self, keys):
498 498 for key in keys:
499 499 del self.properties[key]
500 500
501 501 def del_properties(self, keys):
502 502 msg = {'engineid':self.id,
503 503 'method':'del_properties',
504 504 'args':[repr(keys)]}
505 505 return self.executeAndRaise(msg, self._doDel, keys)
506 506
507 507 def _doHas(self, keys):
508 508 return [self.properties.has_key(key) for key in keys]
509 509
510 510 def has_properties(self, keys):
511 511 msg = {'engineid':self.id,
512 512 'method':'has_properties',
513 513 'args':[repr(keys)]}
514 514 return self.executeAndRaise(msg, self._doHas, keys)
515 515
516 516 def clear_properties(self):
517 517 msg = {'engineid':self.id,
518 518 'method':'clear_properties',
519 519 'args':[]}
520 520 return self.executeAndRaise(msg, self.properties.clear)
521 521
522 522 def push_serialized(self, sNamespace):
523 523 msg = {'engineid':self.id,
524 524 'method':'push_serialized',
525 525 'args':[repr(sNamespace.keys())]}
526 526 ns = {}
527 527 for k,v in sNamespace.iteritems():
528 528 try:
529 529 unserialized = newserialized.IUnSerialized(v)
530 530 ns[k] = unserialized.getObject()
531 531 except:
532 532 return defer.fail()
533 533 return self.executeAndRaise(msg, self.shell.push, ns)
534 534
535 535 def pull_serialized(self, keys):
536 536 msg = {'engineid':self.id,
537 537 'method':'pull_serialized',
538 538 'args':[repr(keys)]}
539 539 if isinstance(keys, str):
540 540 keys = [keys]
541 541 if len(keys)==1:
542 542 d = self.executeAndRaise(msg, self.shell.pull, keys)
543 543 d.addCallback(newserialized.serialize)
544 544 return d
545 545 elif len(keys)>1:
546 546 d = self.executeAndRaise(msg, self.shell.pull, keys)
547 547 @d.addCallback
548 548 def packThemUp(values):
549 549 serials = []
550 550 for v in values:
551 551 try:
552 552 serials.append(newserialized.serialize(v))
553 553 except:
554 554 return defer.fail(failure.Failure())
555 555 return serials
556 556 return packThemUp
557 557
558 558
559 559 def queue(methodToQueue):
560 560 def queuedMethod(this, *args, **kwargs):
561 561 name = methodToQueue.__name__
562 562 return this.submitCommand(Command(name, *args, **kwargs))
563 563 return queuedMethod
564 564
565 565 class QueuedEngine(object):
566 566 """Adapt an IEngineBase to an IEngineQueued by wrapping it.
567 567
568 568 The resulting object will implement IEngineQueued which extends
569 569 IEngineCore which extends (IEngineBase, IEngineSerialized).
570 570
571 571 This seems like the best way of handling it, but I am not sure. The
572 572 other option is to have the various base interfaces be used like
573 573 mix-in intefaces. The problem I have with this is adpatation is
574 574 more difficult and complicated because there can be can multiple
575 575 original and final Interfaces.
576 576 """
577 577
578 578 zi.implements(IEngineQueued)
579 579
580 580 def __init__(self, engine):
581 581 """Create a QueuedEngine object from an engine
582 582
583 583 engine: An implementor of IEngineCore and IEngineSerialized
584 584 keepUpToDate: whether to update the remote status when the
585 585 queue is empty. Defaults to False.
586 586 """
587 587
588 588 # This is the right way to do these tests rather than
589 589 # IEngineCore in list(zi.providedBy(engine)) which will only
590 590 # picks of the interfaces that are directly declared by engine.
591 591 assert IEngineBase.providedBy(engine), \
592 592 "engine passed to QueuedEngine doesn't provide IEngineBase"
593 593
594 594 self.engine = engine
595 595 self.id = engine.id
596 596 self.queued = []
597 597 self.history = {}
598 598 self.engineStatus = {}
599 599 self.currentCommand = None
600 600 self.failureObservers = []
601 601
602 602 def _get_properties(self):
603 603 return self.engine.properties
604 604
605 605 properties = property(_get_properties, lambda self, _: None)
606 606 # Queue management methods. You should not call these directly
607 607
608 608 def submitCommand(self, cmd):
609 609 """Submit command to queue."""
610 610
611 611 d = defer.Deferred()
612 612 cmd.setDeferred(d)
613 613 if self.currentCommand is not None:
614 614 if self.currentCommand.finished:
615 615 # log.msg("Running command immediately: %r" % cmd)
616 616 self.currentCommand = cmd
617 617 self.runCurrentCommand()
618 618 else: # command is still running
619 619 # log.msg("Command is running: %r" % self.currentCommand)
620 620 # log.msg("Queueing: %r" % cmd)
621 621 self.queued.append(cmd)
622 622 else:
623 623 # log.msg("No current commands, running: %r" % cmd)
624 624 self.currentCommand = cmd
625 625 self.runCurrentCommand()
626 626 return d
627 627
628 628 def runCurrentCommand(self):
629 629 """Run current command."""
630 630
631 631 cmd = self.currentCommand
632 632 f = getattr(self.engine, cmd.remoteMethod, None)
633 633 if f:
634 634 d = f(*cmd.args, **cmd.kwargs)
635 635 if cmd.remoteMethod is 'execute':
636 636 d.addCallback(self.saveResult)
637 637 d.addCallback(self.finishCommand)
638 638 d.addErrback(self.abortCommand)
639 639 else:
640 640 return defer.fail(AttributeError(cmd.remoteMethod))
641 641
642 642 def _flushQueue(self):
643 643 """Pop next command in queue and run it."""
644 644
645 645 if len(self.queued) > 0:
646 646 self.currentCommand = self.queued.pop(0)
647 647 self.runCurrentCommand()
648 648
649 649 def saveResult(self, result):
650 650 """Put the result in the history."""
651 651 self.history[result['number']] = result
652 652 return result
653 653
654 654 def finishCommand(self, result):
655 655 """Finish currrent command."""
656 656
657 657 # The order of these commands is absolutely critical.
658 658 self.currentCommand.handleResult(result)
659 659 self.currentCommand.finished = True
660 660 self._flushQueue()
661 661 return result
662 662
663 663 def abortCommand(self, reason):
664 664 """Abort current command.
665 665
666 666 This eats the Failure but first passes it onto the Deferred that the
667 667 user has.
668 668
669 669 It also clear out the queue so subsequence commands don't run.
670 670 """
671 671
672 672 # The order of these 3 commands is absolutely critical. The currentCommand
673 673 # must first be marked as finished BEFORE the queue is cleared and before
674 674 # the current command is sent the failure.
675 675 # Also, the queue must be cleared BEFORE the current command is sent the Failure
676 676 # otherwise the errback chain could trigger new commands to be added to the
677 677 # queue before we clear it. We should clear ONLY the commands that were in
678 678 # the queue when the error occured.
679 679 self.currentCommand.finished = True
680 680 s = "%r %r %r" % (self.currentCommand.remoteMethod, self.currentCommand.args, self.currentCommand.kwargs)
681 681 self.clear_queue(msg=s)
682 682 self.currentCommand.handleError(reason)
683 683
684 684 return None
685 685
686 686 #---------------------------------------------------------------------------
687 687 # IEngineCore methods
688 688 #---------------------------------------------------------------------------
689 689
690 690 @queue
691 691 def execute(self, lines):
692 692 pass
693 693
694 694 @queue
695 695 def push(self, namespace):
696 696 pass
697 697
698 698 @queue
699 699 def pull(self, keys):
700 700 pass
701 701
702 702 @queue
703 703 def push_function(self, namespace):
704 704 pass
705 705
706 706 @queue
707 707 def pull_function(self, keys):
708 708 pass
709 709
710 710 def get_result(self, i=None):
711 711 if i is None:
712 712 i = max(self.history.keys()+[None])
713 713
714 714 cmd = self.history.get(i, None)
715 715 # Uncomment this line to disable chaching of results
716 716 #cmd = None
717 717 if cmd is None:
718 718 return self.submitCommand(Command('get_result', i))
719 719 else:
720 720 return defer.succeed(cmd)
721 721
722 722 def reset(self):
723 723 self.clear_queue()
724 724 self.history = {} # reset the cache - I am not sure we should do this
725 725 return self.submitCommand(Command('reset'))
726 726
727 727 def kill(self):
728 728 self.clear_queue()
729 729 return self.submitCommand(Command('kill'))
730 730
731 731 @queue
732 732 def keys(self):
733 733 pass
734 734
735 735 #---------------------------------------------------------------------------
736 736 # IEngineSerialized methods
737 737 #---------------------------------------------------------------------------
738 738
739 739 @queue
740 740 def push_serialized(self, namespace):
741 741 pass
742 742
743 743 @queue
744 744 def pull_serialized(self, keys):
745 745 pass
746 746
747 747 #---------------------------------------------------------------------------
748 748 # IEngineProperties methods
749 749 #---------------------------------------------------------------------------
750 750
751 751 @queue
752 752 def set_properties(self, namespace):
753 753 pass
754 754
755 755 @queue
756 756 def get_properties(self, keys=None):
757 757 pass
758 758
759 759 @queue
760 760 def del_properties(self, keys):
761 761 pass
762 762
763 763 @queue
764 764 def has_properties(self, keys):
765 765 pass
766 766
767 767 @queue
768 768 def clear_properties(self):
769 769 pass
770 770
771 771 #---------------------------------------------------------------------------
772 772 # IQueuedEngine methods
773 773 #---------------------------------------------------------------------------
774 774
775 775 def clear_queue(self, msg=''):
776 776 """Clear the queue, but doesn't cancel the currently running commmand."""
777 777
778 778 for cmd in self.queued:
779 779 cmd.deferred.errback(failure.Failure(error.QueueCleared(msg)))
780 780 self.queued = []
781 781 return defer.succeed(None)
782 782
783 783 def queue_status(self):
784 784 if self.currentCommand is not None:
785 785 if self.currentCommand.finished:
786 786 pending = repr(None)
787 787 else:
788 788 pending = repr(self.currentCommand)
789 789 else:
790 790 pending = repr(None)
791 791 dikt = {'queue':map(repr,self.queued), 'pending':pending}
792 792 return defer.succeed(dikt)
793 793
794 794 def register_failure_observer(self, obs):
795 795 self.failureObservers.append(obs)
796 796
797 797 def unregister_failure_observer(self, obs):
798 798 self.failureObservers.remove(obs)
799 799
800 800
801 801 # Now register QueuedEngine as an adpater class that makes an IEngineBase into a
802 802 # IEngineQueued.
803 803 components.registerAdapter(QueuedEngine, IEngineBase, IEngineQueued)
804 804
805 805
806 806 class Command(object):
807 807 """A command object that encapslates queued commands.
808 808
809 809 This class basically keeps track of a command that has been queued
810 810 in a QueuedEngine. It manages the deferreds and hold the method to be called
811 811 and the arguments to that method.
812 812 """
813 813
814 814
815 815 def __init__(self, remoteMethod, *args, **kwargs):
816 816 """Build a new Command object."""
817 817
818 818 self.remoteMethod = remoteMethod
819 819 self.args = args
820 820 self.kwargs = kwargs
821 821 self.finished = False
822 822
823 823 def setDeferred(self, d):
824 824 """Sets the deferred attribute of the Command."""
825 825
826 826 self.deferred = d
827 827
828 828 def __repr__(self):
829 829 if not self.args:
830 830 args = ''
831 831 else:
832 832 args = str(self.args)[1:-2] #cut off (...,)
833 833 for k,v in self.kwargs.iteritems():
834 834 if args:
835 835 args += ', '
836 836 args += '%s=%r' %(k,v)
837 837 return "%s(%s)" %(self.remoteMethod, args)
838 838
839 839 def handleResult(self, result):
840 840 """When the result is ready, relay it to self.deferred."""
841 841
842 842 self.deferred.callback(result)
843 843
844 844 def handleError(self, reason):
845 845 """When an error has occured, relay it to self.deferred."""
846 846
847 847 self.deferred.errback(reason)
848 848
849 849 class ThreadedEngineService(EngineService):
850 """An EngineService subclass that defers execute commands to a separate thread.
851
852 ThreadedEngineService uses twisted.internet.threads.deferToThread to defer execute
853 requests to a separate thread. GUI frontends may want to use ThreadedEngineService as
854 the engine in an IPython.frontend.frontendbase.FrontEndBase subclass to prevent
855 block execution from blocking the GUI thread.
856 """
850 857
851 858 zi.implements(IEngineBase)
852 859
853 860 def __init__(self, shellClass=Interpreter, mpi=None):
854 861 EngineService.__init__(self, shellClass, mpi)
855 862
856 863
857 864 def execute(self, lines):
858 865 # Only import this if we are going to use this class
859 866 from twisted.internet import threads
860 867
861 868 msg = {'engineid':self.id,
862 869 'method':'execute',
863 870 'args':[lines]}
864 871
865 872 d = threads.deferToThread(self.shell.execute, lines)
866 873 d.addCallback(self.addIDToResult)
867 874 return d
General Comments 0
You need to be logged in to leave comments. Login now