Show More
@@ -1,27 +1,28 b'' | |||
|
1 | 1 | # encoding: utf-8 |
|
2 |
# -*- test-case-name: |
|
|
2 | # -*- test-case-name: IPython.frontend.cocoa.tests.test_cocoa_frontend -*- | |
|
3 | 3 | |
|
4 |
"""PyObjC classes to provide a Cocoa frontend to the |
|
|
4 | """PyObjC classes to provide a Cocoa frontend to the | |
|
5 | IPython.kernel.engineservice.EngineService. | |
|
5 | 6 | |
|
6 | The Cocoa frontend is divided into two classes: | |
|
7 | - IPythonCocoaController | |
|
8 | - IPythonCLITextViewDelegate | |
|
7 | To add an IPython interpreter to a cocoa app, instantiate an | |
|
8 | IPythonCocoaController in a XIB and connect its textView outlet to an | |
|
9 | NSTextView instance in your UI. That's it. | |
|
9 | 10 | |
|
10 | To add an IPython interpreter to a cocoa app, instantiate both of these classes in an XIB...[FINISH] | |
|
11 | Author: Barry Wark | |
|
11 | 12 | """ |
|
12 | 13 | |
|
13 | 14 | __docformat__ = "restructuredtext en" |
|
14 | 15 | |
|
15 |
#----------------------------------------------------------------------------- |
|
|
16 | # Copyright (C) 2008 Barry Wark <barrywark@gmail.com> | |
|
16 | #----------------------------------------------------------------------------- | |
|
17 | # Copyright (C) 2008 The IPython Development Team | |
|
17 | 18 | # |
|
18 | 19 | # Distributed under the terms of the BSD License. The full license is in |
|
19 | 20 | # the file COPYING, distributed as part of this software. |
|
20 |
#----------------------------------------------------------------------------- |
|
|
21 | #----------------------------------------------------------------------------- | |
|
21 | 22 | |
|
22 |
#----------------------------------------------------------------------------- |
|
|
23 | #----------------------------------------------------------------------------- | |
|
23 | 24 | # Imports |
|
24 |
#----------------------------------------------------------------------------- |
|
|
25 | #----------------------------------------------------------------------------- | |
|
25 | 26 | |
|
26 | 27 | import objc |
|
27 | 28 | import uuid |
@@ -42,12 +43,13 b' from IPython.frontend.frontendbase import FrontEndBase' | |||
|
42 | 43 | from twisted.internet.threads import blockingCallFromThread |
|
43 | 44 | from twisted.python.failure import Failure |
|
44 | 45 | |
|
45 |
#------------------------------------------------------------------------------ |
|
|
46 | #------------------------------------------------------------------------------ | |
|
46 | 47 | # Classes to implement the Cocoa frontend |
|
47 |
#------------------------------------------------------------------------------ |
|
|
48 | #------------------------------------------------------------------------------ | |
|
48 | 49 | |
|
49 | 50 | # TODO: |
|
50 |
# 1. use MultiEngineClient and out-of-process engine rather than |
|
|
51 | # 1. use MultiEngineClient and out-of-process engine rather than | |
|
52 | # ThreadedEngineService? | |
|
51 | 53 | # 2. integrate Xgrid launching of engines |
|
52 | 54 | |
|
53 | 55 | |
@@ -89,7 +91,8 b' class IPythonCocoaController(NSObject, FrontEndBase):' | |||
|
89 | 91 | NSLog('IPython engine started') |
|
90 | 92 | |
|
91 | 93 | # Register for app termination |
|
92 |
NSNotificationCenter.defaultCenter().addObserver_selector_name_object_( |
|
|
94 | NSNotificationCenter.defaultCenter().addObserver_selector_name_object_( | |
|
95 | self, | |
|
93 | 96 |
|
|
94 | 97 |
|
|
95 | 98 |
|
@@ -163,7 +166,8 b' class IPythonCocoaController(NSObject, FrontEndBase):' | |||
|
163 | 166 | def startCLIForTextView(self): |
|
164 | 167 | """Print banner""" |
|
165 | 168 | |
|
166 |
banner = """IPython1 %s -- An enhanced Interactive Python.""" % |
|
|
169 | banner = """IPython1 %s -- An enhanced Interactive Python.""" % \ | |
|
170 | IPython.__version__ | |
|
167 | 171 | |
|
168 | 172 | self.insert_text(banner + '\n\n') |
|
169 | 173 | |
@@ -206,14 +210,18 b' class IPythonCocoaController(NSObject, FrontEndBase):' | |||
|
206 | 210 | return True |
|
207 | 211 | |
|
208 | 212 | elif(selector == 'moveToBeginningOfParagraph:'): |
|
209 |
textView.setSelectedRange_(NSMakeRange( |
|
|
213 | textView.setSelectedRange_(NSMakeRange( | |
|
214 | self.currentBlockRange().location, | |
|
215 | 0)) | |
|
210 | 216 | return True |
|
211 | 217 | elif(selector == 'moveToEndOfParagraph:'): |
|
212 |
textView.setSelectedRange_(NSMakeRange( |
|
|
218 | textView.setSelectedRange_(NSMakeRange( | |
|
219 | self.currentBlockRange().location + \ | |
|
213 | 220 |
|
|
214 | 221 | return True |
|
215 | 222 | elif(selector == 'deleteToEndOfParagraph:'): |
|
216 |
if(textView.selectedRange().location <= |
|
|
223 | if(textView.selectedRange().location <= \ | |
|
224 | self.currentBlockRange().location): | |
|
217 | 225 | # Intersect the selected range with the current line range |
|
218 | 226 | if(self.currentBlockRange().length < 0): |
|
219 | 227 | self.blockRanges[self.currentBlockID].length = 0 |
@@ -235,7 +243,8 b' class IPythonCocoaController(NSObject, FrontEndBase):' | |||
|
235 | 243 | |
|
236 | 244 | elif(selector == 'deleteBackward:'): |
|
237 | 245 | #if we're at the beginning of the current block, ignore |
|
238 |
if(textView.selectedRange().location == |
|
|
246 | if(textView.selectedRange().location == \ | |
|
247 | self.currentBlockRange().location): | |
|
239 | 248 | return True |
|
240 | 249 | else: |
|
241 | 250 | self.currentBlockRange().length-=1 |
@@ -243,14 +252,15 b' class IPythonCocoaController(NSObject, FrontEndBase):' | |||
|
243 | 252 | return False |
|
244 | 253 | |
|
245 | 254 | |
|
246 |
def textView_shouldChangeTextInRanges_replacementStrings_(self, |
|
|
255 | def textView_shouldChangeTextInRanges_replacementStrings_(self, | |
|
256 | textView, ranges, replacementStrings): | |
|
247 | 257 | """ |
|
248 | 258 | Delegate method for NSTextView. |
|
249 | 259 | |
|
250 |
Refuse change text in ranges not at end, but make those changes at |
|
|
260 | Refuse change text in ranges not at end, but make those changes at | |
|
261 | end. | |
|
251 | 262 | """ |
|
252 | 263 | |
|
253 | #print 'textView_shouldChangeTextInRanges_replacementStrings_:',ranges,replacementStrings | |
|
254 | 264 | assert(len(ranges) == len(replacementStrings)) |
|
255 | 265 | allow = True |
|
256 | 266 | for r,s in zip(ranges, replacementStrings): |
@@ -261,13 +271,17 b' class IPythonCocoaController(NSObject, FrontEndBase):' | |||
|
261 | 271 | allow = False |
|
262 | 272 | |
|
263 | 273 | |
|
264 |
self.blockRanges.setdefault(self.currentBlockID, |
|
|
274 | self.blockRanges.setdefault(self.currentBlockID, | |
|
275 | self.currentBlockRange()).length +=\ | |
|
276 | len(s) | |
|
265 | 277 | |
|
266 | 278 | return allow |
|
267 | 279 | |
|
268 |
def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, |
|
|
280 | def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, | |
|
281 | textView, words, charRange, index): | |
|
269 | 282 | try: |
|
270 |
t |
|
|
283 | ts = textView.textStorage() | |
|
284 | token = ts.string().substringWithRange_(charRange) | |
|
271 | 285 | completions = blockingCallFromThread(self.complete, token) |
|
272 | 286 | except: |
|
273 | 287 | completions = objc.nil |
@@ -288,7 +302,9 b' class IPythonCocoaController(NSObject, FrontEndBase):' | |||
|
288 | 302 | return uuid.uuid4() |
|
289 | 303 | |
|
290 | 304 | def currentBlockRange(self): |
|
291 |
return self.blockRanges.get(self.currentBlockID, |
|
|
305 | return self.blockRanges.get(self.currentBlockID, | |
|
306 | NSMakeRange(self.textView.textStorage().length(), | |
|
307 | 0)) | |
|
292 | 308 | |
|
293 | 309 | def currentBlock(self): |
|
294 | 310 | """The current block's text""" |
@@ -298,7 +314,8 b' class IPythonCocoaController(NSObject, FrontEndBase):' | |||
|
298 | 314 | def textForRange(self, textRange): |
|
299 | 315 | """textForRange""" |
|
300 | 316 | |
|
301 |
|
|
|
317 | ts = self.textView.textStorage() | |
|
318 | return ts.string().substringWithRange_(textRange) | |
|
302 | 319 | |
|
303 | 320 | def currentLine(self): |
|
304 | 321 | block = self.textForRange(self.currentBlockRange()) |
@@ -330,7 +347,8 b' class IPythonCocoaController(NSObject, FrontEndBase):' | |||
|
330 | 347 |
|
|
331 | 348 |
|
|
332 | 349 |
|
|
333 |
|
|
|
350 | textRange=NSMakeRange(inputRange.location+inputRange.length, | |
|
351 | 0)) | |
|
334 | 352 | return result |
|
335 | 353 | |
|
336 | 354 | |
@@ -341,7 +359,9 b' class IPythonCocoaController(NSObject, FrontEndBase):' | |||
|
341 | 359 | |
|
342 | 360 | |
|
343 | 361 | def insert_text(self, string=None, textRange=None, scrollToVisible=True): |
|
344 |
"""Insert text into textView at textRange, updating blockRanges |
|
|
362 | """Insert text into textView at textRange, updating blockRanges | |
|
363 | as necessary | |
|
364 | """ | |
|
345 | 365 | |
|
346 | 366 | if(textRange == None): |
|
347 | 367 | textRange = NSMakeRange(self.textView.textStorage().length(), 0) #range for end of text |
@@ -1,26 +1,19 b'' | |||
|
1 | 1 | # encoding: utf-8 |
|
2 |
"""This file contains unittests for the |
|
|
3 | ||
|
4 | Things that should be tested: | |
|
5 | ||
|
6 | - IPythonCocoaController instantiates an IEngineInteractive | |
|
7 | - IPythonCocoaController executes code on the engine | |
|
8 | - IPythonCocoaController mirrors engine's user_ns | |
|
2 | """This file contains unittests for the | |
|
3 | IPython.frontend.cocoa.cocoa_frontend module. | |
|
9 | 4 | """ |
|
10 | 5 | __docformat__ = "restructuredtext en" |
|
11 | 6 | |
|
12 |
#--------------------------------------------------------------------------- |
|
|
13 | # Copyright (C) 2005 Fernando Perez <fperez@colorado.edu> | |
|
14 | # Brian E Granger <ellisonbg@gmail.com> | |
|
15 | # Benjamin Ragan-Kelley <benjaminrk@gmail.com> | |
|
7 | #--------------------------------------------------------------------------- | |
|
8 | # Copyright (C) 2005 The IPython Development Team | |
|
16 | 9 | # |
|
17 | 10 | # Distributed under the terms of the BSD License. The full license is in |
|
18 | 11 | # the file COPYING, distributed as part of this software. |
|
19 |
#--------------------------------------------------------------------------- |
|
|
12 | #--------------------------------------------------------------------------- | |
|
20 | 13 | |
|
21 |
#--------------------------------------------------------------------------- |
|
|
14 | #--------------------------------------------------------------------------- | |
|
22 | 15 | # Imports |
|
23 |
#--------------------------------------------------------------------------- |
|
|
16 | #--------------------------------------------------------------------------- | |
|
24 | 17 | from IPython.kernel.core.interpreter import Interpreter |
|
25 | 18 | import IPython.kernel.engineservice as es |
|
26 | 19 | from IPython.testing.util import DeferredTestCase |
@@ -51,7 +44,9 b' class TestIPythonCocoaControler(DeferredTestCase):' | |||
|
51 | 44 | del result['number'] |
|
52 | 45 | del result['id'] |
|
53 | 46 | return result |
|
54 | self.assertDeferredEquals(self.controller.execute(code).addCallback(removeNumberAndID), expected) | |
|
47 | self.assertDeferredEquals( | |
|
48 | self.controller.execute(code).addCallback(removeNumberAndID), | |
|
49 | expected) | |
|
55 | 50 | |
|
56 | 51 | def testControllerMirrorsUserNSWithValuesAsStrings(self): |
|
57 | 52 | code = """userns1=1;userns2=2""" |
@@ -1,6 +1,8 b'' | |||
|
1 | 1 | # encoding: utf-8 |
|
2 | # -*- test-case-name: IPython.frontend.tests.test_frontendbase -*- | |
|
2 | 3 | """ |
|
3 |
frontendbase provides an interface and base class for GUI frontends for |
|
|
4 | frontendbase provides an interface and base class for GUI frontends for | |
|
5 | IPython.kernel/IPython.kernel.core. | |
|
4 | 6 | |
|
5 | 7 | Frontend implementations will likely want to subclass FrontEndBase. |
|
6 | 8 | |
@@ -57,20 +59,26 b' class IFrontEndFactory(zi.Interface):' | |||
|
57 | 59 | class IFrontEnd(zi.Interface): |
|
58 | 60 | """Interface for frontends. All methods return t.i.d.Deferred""" |
|
59 | 61 | |
|
60 |
zi.Attribute("input_prompt_template", "string.Template instance |
|
|
61 | zi.Attribute("output_prompt_template", "string.Template instance substituteable with execute result.") | |
|
62 |
zi.Attribute(" |
|
|
62 | zi.Attribute("input_prompt_template", "string.Template instance\ | |
|
63 | substituteable with execute result.") | |
|
64 | zi.Attribute("output_prompt_template", "string.Template instance\ | |
|
65 | substituteable with execute result.") | |
|
66 | zi.Attribute("continuation_prompt_template", "string.Template instance\ | |
|
67 | substituteable with execute result.") | |
|
63 | 68 | |
|
64 | 69 | def update_cell_prompt(self, result): |
|
65 | 70 | """Subclass may override to update the input prompt for a block. |
|
66 |
Since this method will be called as a |
|
|
71 | Since this method will be called as a | |
|
72 | twisted.internet.defer.Deferred's callback, | |
|
67 | 73 | implementations should return result when finished.""" |
|
68 | 74 | |
|
69 | 75 | pass |
|
70 | 76 | |
|
71 | 77 | def render_result(self, result): |
|
72 |
"""Render the result of an execute call. Implementors may choose the |
|
|
73 | For example, a notebook-style frontend might render a Chaco plot inline. | |
|
78 | """Render the result of an execute call. Implementors may choose the | |
|
79 | method of rendering. | |
|
80 | For example, a notebook-style frontend might render a Chaco plot | |
|
81 | inline. | |
|
74 | 82 | |
|
75 | 83 | Parameters: |
|
76 | 84 | result : dict (result of IEngineBase.execute ) |
@@ -82,24 +90,31 b' class IFrontEnd(zi.Interface):' | |||
|
82 | 90 | pass |
|
83 | 91 | |
|
84 | 92 | def render_error(self, failure): |
|
85 |
"""Subclasses must override to render the failure. Since this method |
|
|
86 |
twisted.internet.defer.Deferred's callback, |
|
|
87 | when finished.""" | |
|
93 | """Subclasses must override to render the failure. Since this method | |
|
94 | ill be called as a twisted.internet.defer.Deferred's callback, | |
|
95 | implementations should return result when finished. | |
|
96 | """ | |
|
88 | 97 | |
|
89 | 98 | pass |
|
90 | 99 | |
|
91 | 100 | |
|
92 | 101 | def input_prompt(result={}): |
|
93 |
"""Returns the input prompt by subsituting into |
|
|
102 | """Returns the input prompt by subsituting into | |
|
103 | self.input_prompt_template | |
|
104 | """ | |
|
94 | 105 | pass |
|
95 | 106 | |
|
96 | 107 | def output_prompt(result): |
|
97 |
"""Returns the output prompt by subsituting into |
|
|
108 | """Returns the output prompt by subsituting into | |
|
109 | self.output_prompt_template | |
|
110 | """ | |
|
98 | 111 | |
|
99 | 112 | pass |
|
100 | 113 | |
|
101 | 114 | def continuation_prompt(): |
|
102 |
"""Returns the continuation prompt by subsituting into |
|
|
115 | """Returns the continuation prompt by subsituting into | |
|
116 | self.continuation_prompt_template | |
|
117 | """ | |
|
103 | 118 | |
|
104 | 119 | pass |
|
105 | 120 | |
@@ -221,7 +236,8 b' class FrontEndBase(object):' | |||
|
221 | 236 | Parameters: |
|
222 | 237 | block : {str, AST} |
|
223 | 238 | blockID : any |
|
224 |
Caller may provide an ID to identify this block. |
|
|
239 | Caller may provide an ID to identify this block. | |
|
240 | result['blockID'] := blockID | |
|
225 | 241 | |
|
226 | 242 | Result: |
|
227 | 243 | Deferred result of self.interpreter.execute |
@@ -243,8 +259,8 b' class FrontEndBase(object):' | |||
|
243 | 259 | |
|
244 | 260 | |
|
245 | 261 | def _add_block_id(self, result, blockID): |
|
246 |
"""Add the blockID to result or failure. Unfortunatley, we have to |
|
|
247 | differently than result dicts | |
|
262 | """Add the blockID to result or failure. Unfortunatley, we have to | |
|
263 | treat failures differently than result dicts. | |
|
248 | 264 | """ |
|
249 | 265 | |
|
250 | 266 | if(isinstance(result, Failure)): |
@@ -291,11 +307,12 b' class FrontEndBase(object):' | |||
|
291 | 307 | |
|
292 | 308 | def update_cell_prompt(self, result): |
|
293 | 309 | """Subclass may override to update the input prompt for a block. |
|
294 |
Since this method will be called as a |
|
|
295 | implementations should return result when finished. | |
|
310 | Since this method will be called as a | |
|
311 | twisted.internet.defer.Deferred's callback, implementations should | |
|
312 | return result when finished. | |
|
296 | 313 | |
|
297 |
NP: result is a failure if the execute returned a failre. |
|
|
298 | do something like:: | |
|
314 | NP: result is a failure if the execute returned a failre. | |
|
315 | To get the blockID, you should do something like:: | |
|
299 | 316 | if(isinstance(result, twisted.python.failure.Failure)): |
|
300 | 317 | blockID = result.blockID |
|
301 | 318 | else: |
@@ -308,17 +325,18 b' class FrontEndBase(object):' | |||
|
308 | 325 | |
|
309 | 326 | |
|
310 | 327 | def render_result(self, result): |
|
311 |
"""Subclasses must override to render result. Since this method will |
|
|
312 |
twisted.internet.defer.Deferred's callback, |
|
|
313 | when finished.""" | |
|
328 | """Subclasses must override to render result. Since this method will | |
|
329 | be called as a twisted.internet.defer.Deferred's callback, | |
|
330 | implementations should return result when finished. | |
|
331 | """ | |
|
314 | 332 | |
|
315 | 333 | return result |
|
316 | 334 | |
|
317 | 335 | |
|
318 | 336 | def render_error(self, failure): |
|
319 |
"""Subclasses must override to render the failure. Since this method |
|
|
320 |
twisted.internet.defer.Deferred's callback, |
|
|
321 | when finished.""" | |
|
337 | """Subclasses must override to render the failure. Since this method | |
|
338 | will be called as a twisted.internet.defer.Deferred's callback, | |
|
339 | implementations should return result when finished.""" | |
|
322 | 340 | |
|
323 | 341 | return failure |
|
324 | 342 |
@@ -4,16 +4,16 b'' | |||
|
4 | 4 | |
|
5 | 5 | __docformat__ = "restructuredtext en" |
|
6 | 6 | |
|
7 |
#--------------------------------------------------------------------------- |
|
|
7 | #--------------------------------------------------------------------------- | |
|
8 | 8 | # Copyright (C) 2008 The IPython Development Team |
|
9 | 9 | # |
|
10 | 10 | # Distributed under the terms of the BSD License. The full license is in |
|
11 | 11 | # the file COPYING, distributed as part of this software. |
|
12 |
#--------------------------------------------------------------------------- |
|
|
12 | #--------------------------------------------------------------------------- | |
|
13 | 13 | |
|
14 |
#--------------------------------------------------------------------------- |
|
|
14 | #--------------------------------------------------------------------------- | |
|
15 | 15 | # Imports |
|
16 |
#--------------------------------------------------------------------------- |
|
|
16 | #--------------------------------------------------------------------------- | |
|
17 | 17 | |
|
18 | 18 | import unittest |
|
19 | 19 | from IPython.frontend import frontendbase |
@@ -22,7 +22,8 b' from IPython.kernel.engineservice import EngineService' | |||
|
22 | 22 | class FrontEndCallbackChecker(frontendbase.FrontEndBase): |
|
23 | 23 | """FrontEndBase subclass for checking callbacks""" |
|
24 | 24 | def __init__(self, engine=None, history=None): |
|
25 |
super(FrontEndCallbackChecker, self).__init__(engine=engine, |
|
|
25 | super(FrontEndCallbackChecker, self).__init__(engine=engine, | |
|
26 | history=history) | |
|
26 | 27 | self.updateCalled = False |
|
27 | 28 | self.renderResultCalled = False |
|
28 | 29 | self.renderErrorCalled = False |
@@ -51,7 +52,8 b' class TestFrontendBase(unittest.TestCase):' | |||
|
51 | 52 | |
|
52 | 53 | |
|
53 | 54 | def test_implements_IFrontEnd(self): |
|
54 |
assert(frontendbase.IFrontEnd.implementedBy( |
|
|
55 | assert(frontendbase.IFrontEnd.implementedBy( | |
|
56 | frontendbase.FrontEndBase)) | |
|
55 | 57 | |
|
56 | 58 | |
|
57 | 59 | def test_is_complete_returns_False_for_incomplete_block(self): |
@@ -847,11 +847,13 b' class Command(object):' | |||
|
847 | 847 | self.deferred.errback(reason) |
|
848 | 848 | |
|
849 | 849 | class ThreadedEngineService(EngineService): |
|
850 |
"""An EngineService subclass that defers execute commands to a separate |
|
|
850 | """An EngineService subclass that defers execute commands to a separate | |
|
851 | thread. | |
|
851 | 852 | |
|
852 |
ThreadedEngineService uses twisted.internet.threads.deferToThread to |
|
|
853 |
requests to a separate thread. GUI frontends may want to |
|
|
854 | the engine in an IPython.frontend.frontendbase.FrontEndBase subclass to prevent | |
|
853 | ThreadedEngineService uses twisted.internet.threads.deferToThread to | |
|
854 | defer execute requests to a separate thread. GUI frontends may want to | |
|
855 | use ThreadedEngineService as the engine in an | |
|
856 | IPython.frontend.frontendbase.FrontEndBase subclass to prevent | |
|
855 | 857 | block execution from blocking the GUI thread. |
|
856 | 858 | """ |
|
857 | 859 |
General Comments 0
You need to be logged in to leave comments.
Login now