##// END OF EJS Templates
Merged changes from ipython-frontend1. Better error rendering in Cocoa frontend. Refactored frontendbase.FrontEndBase so that FrontEndBase is synchronous and AsynchronousFronEndBase(FrontEndBase) wraps FrontEndBase in Twisted goodness.
Barry Wark -
r1312:06dc07c6 merge
parent child Browse files
Show More
@@ -0,0 +1,256 b''
1 // !$*UTF8*$!
2 {
3 archiveVersion = 1;
4 classes = {
5 };
6 objectVersion = 42;
7 objects = {
8
9 /* Begin PBXContainerItemProxy section */
10 4C5B7ADB0E1A0BCD006CB905 /* PBXContainerItemProxy */ = {
11 isa = PBXContainerItemProxy;
12 containerPortal = 4C96F4FE0E199AB500B03430 /* Project object */;
13 proxyType = 1;
14 remoteGlobalIDString = 4C96F50C0E199AF100B03430 /* Cocoa Frontend Plugin */;
15 remoteInfo = "Cocoa Frontend Plugin";
16 };
17 /* End PBXContainerItemProxy section */
18
19 /* Begin PBXFileReference section */
20 4C5B7A8D0E1A0B4C006CB905 /* Plugin-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Plugin-Info.plist"; sourceTree = "<group>"; };
21 4C5B7AD30E1A0BC8006CB905 /* Placeholder (Do Not Use).bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Placeholder (Do Not Use).bundle"; sourceTree = BUILT_PRODUCTS_DIR; };
22 4C5B7AD40E1A0BC8006CB905 /* Placeholder (Do Not Use)-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Placeholder (Do Not Use)-Info.plist"; sourceTree = "<group>"; };
23 /* End PBXFileReference section */
24
25 /* Begin PBXFrameworksBuildPhase section */
26 4C5B7AD10E1A0BC8006CB905 /* Frameworks */ = {
27 isa = PBXFrameworksBuildPhase;
28 buildActionMask = 2147483647;
29 files = (
30 );
31 runOnlyForDeploymentPostprocessing = 0;
32 };
33 /* End PBXFrameworksBuildPhase section */
34
35 /* Begin PBXGroup section */
36 4C5B7A8C0E1A0B4C006CB905 /* Products */ = {
37 isa = PBXGroup;
38 children = (
39 4C5B7AD30E1A0BC8006CB905 /* Placeholder (Do Not Use).bundle */,
40 );
41 name = Products;
42 sourceTree = "<group>";
43 };
44 4C96F4FC0E199AB500B03430 = {
45 isa = PBXGroup;
46 children = (
47 4C5B7A8C0E1A0B4C006CB905 /* Products */,
48 4C5B7A8D0E1A0B4C006CB905 /* Plugin-Info.plist */,
49 4C5B7AD40E1A0BC8006CB905 /* Placeholder (Do Not Use)-Info.plist */,
50 );
51 sourceTree = "<group>";
52 };
53 /* End PBXGroup section */
54
55 /* Begin PBXLegacyTarget section */
56 4C96F50C0E199AF100B03430 /* Cocoa Frontend Plugin */ = {
57 isa = PBXLegacyTarget;
58 buildArgumentsString = "$(ACTION)";
59 buildConfigurationList = 4C96F5110E199B3300B03430 /* Build configuration list for PBXLegacyTarget "Cocoa Frontend Plugin" */;
60 buildPhases = (
61 );
62 buildToolPath = /usr/bin/make;
63 buildWorkingDirectory = "";
64 dependencies = (
65 );
66 name = "Cocoa Frontend Plugin";
67 passBuildSettingsInEnvironment = 1;
68 productName = "Cocoa Frontend Plugin";
69 };
70 /* End PBXLegacyTarget section */
71
72 /* Begin PBXNativeTarget section */
73 4C5B7AD20E1A0BC8006CB905 /* Placeholder (Do Not Use) */ = {
74 isa = PBXNativeTarget;
75 buildConfigurationList = 4C5B7ADA0E1A0BC9006CB905 /* Build configuration list for PBXNativeTarget "Placeholder (Do Not Use)" */;
76 buildPhases = (
77 4C5B7ACF0E1A0BC8006CB905 /* Resources */,
78 4C5B7AD00E1A0BC8006CB905 /* Sources */,
79 4C5B7AD10E1A0BC8006CB905 /* Frameworks */,
80 );
81 buildRules = (
82 );
83 dependencies = (
84 4C5B7ADC0E1A0BCD006CB905 /* PBXTargetDependency */,
85 );
86 name = "Placeholder (Do Not Use)";
87 productName = "Placeholder (Do Not Use)";
88 productReference = 4C5B7AD30E1A0BC8006CB905 /* Placeholder (Do Not Use).bundle */;
89 productType = "com.apple.product-type.bundle";
90 };
91 /* End PBXNativeTarget section */
92
93 /* Begin PBXProject section */
94 4C96F4FE0E199AB500B03430 /* Project object */ = {
95 isa = PBXProject;
96 buildConfigurationList = 4C96F5010E199AB500B03430 /* Build configuration list for PBXProject "CocoaFrontendPlugin" */;
97 compatibilityVersion = "Xcode 2.4";
98 hasScannedForEncodings = 0;
99 mainGroup = 4C96F4FC0E199AB500B03430;
100 productRefGroup = 4C5B7A8C0E1A0B4C006CB905 /* Products */;
101 projectDirPath = "";
102 projectRoot = "";
103 targets = (
104 4C96F50C0E199AF100B03430 /* Cocoa Frontend Plugin */,
105 4C5B7AD20E1A0BC8006CB905 /* Placeholder (Do Not Use) */,
106 );
107 };
108 /* End PBXProject section */
109
110 /* Begin PBXResourcesBuildPhase section */
111 4C5B7ACF0E1A0BC8006CB905 /* Resources */ = {
112 isa = PBXResourcesBuildPhase;
113 buildActionMask = 2147483647;
114 files = (
115 );
116 runOnlyForDeploymentPostprocessing = 0;
117 };
118 /* End PBXResourcesBuildPhase section */
119
120 /* Begin PBXSourcesBuildPhase section */
121 4C5B7AD00E1A0BC8006CB905 /* Sources */ = {
122 isa = PBXSourcesBuildPhase;
123 buildActionMask = 2147483647;
124 files = (
125 );
126 runOnlyForDeploymentPostprocessing = 0;
127 };
128 /* End PBXSourcesBuildPhase section */
129
130 /* Begin PBXTargetDependency section */
131 4C5B7ADC0E1A0BCD006CB905 /* PBXTargetDependency */ = {
132 isa = PBXTargetDependency;
133 target = 4C96F50C0E199AF100B03430 /* Cocoa Frontend Plugin */;
134 targetProxy = 4C5B7ADB0E1A0BCD006CB905 /* PBXContainerItemProxy */;
135 };
136 /* End PBXTargetDependency section */
137
138 /* Begin XCBuildConfiguration section */
139 4C5B7AD50E1A0BC9006CB905 /* Debug */ = {
140 isa = XCBuildConfiguration;
141 buildSettings = {
142 COPY_PHASE_STRIP = NO;
143 GCC_DYNAMIC_NO_PIC = NO;
144 GCC_ENABLE_FIX_AND_CONTINUE = YES;
145 GCC_MODEL_TUNING = G5;
146 GCC_OPTIMIZATION_LEVEL = 0;
147 GCC_PRECOMPILE_PREFIX_HEADER = YES;
148 GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/AppKit.framework/Headers/AppKit.h";
149 INFOPLIST_FILE = "Placeholder (Do Not Use)-Info.plist";
150 INSTALL_PATH = "$(HOME)/Library/Bundles";
151 OTHER_LDFLAGS = (
152 "-framework",
153 Foundation,
154 "-framework",
155 AppKit,
156 );
157 PREBINDING = NO;
158 PRODUCT_NAME = "Placeholder (Do Not Use)";
159 WRAPPER_EXTENSION = bundle;
160 ZERO_LINK = YES;
161 };
162 name = Debug;
163 };
164 4C5B7AD60E1A0BC9006CB905 /* Release */ = {
165 isa = XCBuildConfiguration;
166 buildSettings = {
167 COPY_PHASE_STRIP = YES;
168 DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
169 GCC_ENABLE_FIX_AND_CONTINUE = NO;
170 GCC_MODEL_TUNING = G5;
171 GCC_PRECOMPILE_PREFIX_HEADER = YES;
172 GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/AppKit.framework/Headers/AppKit.h";
173 INFOPLIST_FILE = "Placeholder (Do Not Use)-Info.plist";
174 INSTALL_PATH = "$(HOME)/Library/Bundles";
175 OTHER_LDFLAGS = (
176 "-framework",
177 Foundation,
178 "-framework",
179 AppKit,
180 );
181 PREBINDING = NO;
182 PRODUCT_NAME = "Placeholder (Do Not Use)";
183 WRAPPER_EXTENSION = bundle;
184 ZERO_LINK = NO;
185 };
186 name = Release;
187 };
188 4C96F4FF0E199AB500B03430 /* Debug */ = {
189 isa = XCBuildConfiguration;
190 buildSettings = {
191 COPY_PHASE_STRIP = NO;
192 };
193 name = Debug;
194 };
195 4C96F5000E199AB500B03430 /* Release */ = {
196 isa = XCBuildConfiguration;
197 buildSettings = {
198 COPY_PHASE_STRIP = YES;
199 };
200 name = Release;
201 };
202 4C96F50D0E199AF100B03430 /* Debug */ = {
203 isa = XCBuildConfiguration;
204 buildSettings = {
205 COPY_PHASE_STRIP = NO;
206 GCC_DYNAMIC_NO_PIC = NO;
207 GCC_OPTIMIZATION_LEVEL = 0;
208 PRODUCT_NAME = "Cocoa Frontend Plugin";
209 };
210 name = Debug;
211 };
212 4C96F50E0E199AF100B03430 /* Release */ = {
213 isa = XCBuildConfiguration;
214 buildSettings = {
215 COPY_PHASE_STRIP = YES;
216 DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
217 GCC_ENABLE_FIX_AND_CONTINUE = NO;
218 PRODUCT_NAME = "Cocoa Frontend Plugin";
219 ZERO_LINK = NO;
220 };
221 name = Release;
222 };
223 /* End XCBuildConfiguration section */
224
225 /* Begin XCConfigurationList section */
226 4C5B7ADA0E1A0BC9006CB905 /* Build configuration list for PBXNativeTarget "Placeholder (Do Not Use)" */ = {
227 isa = XCConfigurationList;
228 buildConfigurations = (
229 4C5B7AD50E1A0BC9006CB905 /* Debug */,
230 4C5B7AD60E1A0BC9006CB905 /* Release */,
231 );
232 defaultConfigurationIsVisible = 0;
233 defaultConfigurationName = Release;
234 };
235 4C96F5010E199AB500B03430 /* Build configuration list for PBXProject "CocoaFrontendPlugin" */ = {
236 isa = XCConfigurationList;
237 buildConfigurations = (
238 4C96F4FF0E199AB500B03430 /* Debug */,
239 4C96F5000E199AB500B03430 /* Release */,
240 );
241 defaultConfigurationIsVisible = 0;
242 defaultConfigurationName = Release;
243 };
244 4C96F5110E199B3300B03430 /* Build configuration list for PBXLegacyTarget "Cocoa Frontend Plugin" */ = {
245 isa = XCConfigurationList;
246 buildConfigurations = (
247 4C96F50D0E199AF100B03430 /* Debug */,
248 4C96F50E0E199AF100B03430 /* Release */,
249 );
250 defaultConfigurationIsVisible = 0;
251 defaultConfigurationName = Release;
252 };
253 /* End XCConfigurationList section */
254 };
255 rootObject = 4C96F4FE0E199AB500B03430 /* Project object */;
256 }
@@ -0,0 +1,25 b''
1 # encoding: utf-8
2 """
3 Provides a namespace for loading the Cocoa frontend via a Cocoa plugin.
4
5 Author: Barry Wark
6 """
7 __docformat__ = "restructuredtext en"
8
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2008 The IPython Development Team
11 #
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
15
16 from PyObjCTools import AppHelper
17 from twisted.internet import _threadedselect
18
19 #make sure _threadedselect is installed first
20 reactor = _threadedselect.install()
21
22 # load the Cocoa frontend controller
23 from IPython.frontend.cocoa.cocoa_frontend import IPythonCocoaController
24 reactor.interleave(AppHelper.callAfter)
25 assert(reactor.running)
@@ -0,0 +1,20 b''
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 <plist version="1.0">
4 <dict>
5 <key>CFBundleDevelopmentRegion</key>
6 <string>English</string>
7 <key>CFBundleExecutable</key>
8 <string>${EXECUTABLE_NAME}</string>
9 <key>CFBundleIdentifier</key>
10 <string>com.yourcompany.Placeholder (Do Not Use)</string>
11 <key>CFBundleInfoDictionaryVersion</key>
12 <string>6.0</string>
13 <key>CFBundlePackageType</key>
14 <string>BNDL</string>
15 <key>CFBundleSignature</key>
16 <string>????</string>
17 <key>CFBundleVersion</key>
18 <string>1.0</string>
19 </dict>
20 </plist>
@@ -1,504 +1,504 b''
1 1 # encoding: utf-8
2 2 # -*- test-case-name: IPython.frontend.cocoa.tests.test_cocoa_frontend -*-
3 3
4 4 """PyObjC classes to provide a Cocoa frontend to the
5 5 IPython.kernel.engineservice.IEngineBase.
6 6
7 7 To add an IPython interpreter to a cocoa app, instantiate an
8 8 IPythonCocoaController in a XIB and connect its textView outlet to an
9 9 NSTextView instance in your UI. That's it.
10 10
11 11 Author: Barry Wark
12 12 """
13 13
14 14 __docformat__ = "restructuredtext en"
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Copyright (C) 2008 The IPython Development Team
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
27 27 import sys
28 28 import objc
29 29 import uuid
30 30
31 31 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
32 32 NSLog, NSNotificationCenter, NSMakeRange,\
33 33 NSLocalizedString, NSIntersectionRange,\
34 34 NSString, NSAutoreleasePool
35 35
36 36 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
37 37 NSTextView, NSRulerView, NSVerticalRuler
38 38
39 39 from pprint import saferepr
40 40
41 41 import IPython
42 42 from IPython.kernel.engineservice import ThreadedEngineService
43 from IPython.frontend.frontendbase import FrontEndBase
43 from IPython.frontend.frontendbase import AsynchronousFrontEndBase
44 44
45 45 from twisted.internet.threads import blockingCallFromThread
46 46 from twisted.python.failure import Failure
47 47
48 48 #------------------------------------------------------------------------------
49 49 # Classes to implement the Cocoa frontend
50 50 #------------------------------------------------------------------------------
51 51
52 52 # TODO:
53 53 # 1. use MultiEngineClient and out-of-process engine rather than
54 54 # ThreadedEngineService?
55 55 # 2. integrate Xgrid launching of engines
56 56
57 57 class AutoreleasePoolWrappedThreadedEngineService(ThreadedEngineService):
58 58 """Wrap all blocks in an NSAutoreleasePool"""
59 59
60 60 def wrapped_execute(self, msg, lines):
61 61 """wrapped_execute"""
62 62 try:
63 63 p = NSAutoreleasePool.alloc().init()
64 64 result = self.shell.execute(lines)
65 65 except Exception,e:
66 66 # This gives the following:
67 67 # et=exception class
68 68 # ev=exception class instance
69 69 # tb=traceback object
70 70 et,ev,tb = sys.exc_info()
71 71 # This call adds attributes to the exception value
72 72 et,ev,tb = self.shell.formatTraceback(et,ev,tb,msg)
73 73 # Add another attribute
74 74
75 75 # Create a new exception with the new attributes
76 76 e = et(ev._ipython_traceback_text)
77 77 e._ipython_engine_info = msg
78 78
79 79 # Re-raise
80 80 raise e
81 81 finally:
82 82 p.drain()
83 83
84 84 return result
85 85
86 86 def execute(self, lines):
87 87 # Only import this if we are going to use this class
88 88 from twisted.internet import threads
89 89
90 90 msg = {'engineid':self.id,
91 91 'method':'execute',
92 92 'args':[lines]}
93 93
94 94 d = threads.deferToThread(self.wrapped_execute, msg, lines)
95 95 d.addCallback(self.addIDToResult)
96 96 return d
97 97
98 98
99 class IPythonCocoaController(NSObject, FrontEndBase):
99 class IPythonCocoaController(NSObject, AsynchronousFrontEndBase):
100 100 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
101 101 waitingForEngine = objc.ivar().bool()
102 102 textView = objc.IBOutlet()
103 103
104 104 def init(self):
105 105 self = super(IPythonCocoaController, self).init()
106 FrontEndBase.__init__(self,
106 AsynchronousFrontEndBase.__init__(self,
107 107 engine=AutoreleasePoolWrappedThreadedEngineService())
108 108 if(self != None):
109 109 self._common_init()
110 110
111 111 return self
112 112
113 113 def _common_init(self):
114 114 """_common_init"""
115 115
116 116 self.userNS = NSMutableDictionary.dictionary()
117 117 self.waitingForEngine = False
118 118
119 119 self.lines = {}
120 120 self.tabSpaces = 4
121 121 self.tabUsesSpaces = True
122 122 self.currentBlockID = self.next_block_ID()
123 123 self.blockRanges = {} # blockID=>NSRange
124 124
125 125
126 126 def awakeFromNib(self):
127 127 """awakeFromNib"""
128 128
129 129 self._common_init()
130 130
131 131 # Start the IPython engine
132 132 self.engine.startService()
133 133 NSLog('IPython engine started')
134 134
135 135 # Register for app termination
136 136 nc = NSNotificationCenter.defaultCenter()
137 137 nc.addObserver_selector_name_object_(
138 138 self,
139 139 'appWillTerminate:',
140 140 NSApplicationWillTerminateNotification,
141 141 None)
142 142
143 143 self.textView.setDelegate_(self)
144 144 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
145 145 r = NSRulerView.alloc().initWithScrollView_orientation_(
146 146 self.textView.enclosingScrollView(),
147 147 NSVerticalRuler)
148 148 self.verticalRulerView = r
149 149 self.verticalRulerView.setClientView_(self.textView)
150 150 self._start_cli_banner()
151 151
152 152
153 153 def appWillTerminate_(self, notification):
154 154 """appWillTerminate"""
155 155
156 156 self.engine.stopService()
157 157
158 158
159 159 def complete(self, token):
160 160 """Complete token in engine's user_ns
161 161
162 162 Parameters
163 163 ----------
164 164 token : string
165 165
166 166 Result
167 167 ------
168 168 Deferred result of
169 169 IPython.kernel.engineservice.IEngineBase.complete
170 170 """
171 171
172 172 return self.engine.complete(token)
173 173
174 174
175 175 def execute(self, block, blockID=None):
176 176 self.waitingForEngine = True
177 177 self.willChangeValueForKey_('commandHistory')
178 178 d = super(IPythonCocoaController, self).execute(block,
179 179 blockID)
180 180 d.addBoth(self._engine_done)
181 181 d.addCallback(self._update_user_ns)
182 182
183 183 return d
184 184
185 185
186 186 def push_(self, namespace):
187 187 """Push dictionary of key=>values to python namespace"""
188 188
189 189 self.waitingForEngine = True
190 190 self.willChangeValueForKey_('commandHistory')
191 191 d = self.engine.push(namespace)
192 192 d.addBoth(self._engine_done)
193 193 d.addCallback(self._update_user_ns)
194 194
195 195
196 196 def pull_(self, keys):
197 197 """Pull keys from python namespace"""
198 198
199 199 self.waitingForEngine = True
200 200 result = blockingCallFromThread(self.engine.pull, keys)
201 201 self.waitingForEngine = False
202 202
203 203 def executeFileAtPath_(self, path):
204 204 """Execute file at path in an empty namespace. Update the engine
205 205 user_ns with the resulting locals."""
206 206
207 207 lines,err = NSString.stringWithContentsOfFile_encoding_error_(
208 208 path,
209 209 NSString.defaultCStringEncoding(),
210 210 None)
211 211 self.engine.execute(lines)
212 212
213 213
214 214 def _engine_done(self, x):
215 215 self.waitingForEngine = False
216 216 self.didChangeValueForKey_('commandHistory')
217 217 return x
218 218
219 219 def _update_user_ns(self, result):
220 220 """Update self.userNS from self.engine's namespace"""
221 221 d = self.engine.keys()
222 222 d.addCallback(self._get_engine_namespace_values_for_keys)
223 223
224 224 return result
225 225
226 226
227 227 def _get_engine_namespace_values_for_keys(self, keys):
228 228 d = self.engine.pull(keys)
229 229 d.addCallback(self._store_engine_namespace_values, keys=keys)
230 230
231 231
232 232 def _store_engine_namespace_values(self, values, keys=[]):
233 233 assert(len(values) == len(keys))
234 234 self.willChangeValueForKey_('userNS')
235 235 for (k,v) in zip(keys,values):
236 236 self.userNS[k] = saferepr(v)
237 237 self.didChangeValueForKey_('userNS')
238 238
239 239
240 240 def update_cell_prompt(self, result, blockID=None):
241 241 if(isinstance(result, Failure)):
242 242 self.insert_text(self.input_prompt(),
243 243 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
244 244 scrollToVisible=False
245 245 )
246 246 else:
247 247 self.insert_text(self.input_prompt(number=result['number']),
248 248 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
249 249 scrollToVisible=False
250 250 )
251 251
252 252 return result
253 253
254 254
255 255 def render_result(self, result):
256 256 blockID = result['blockID']
257 257 inputRange = self.blockRanges[blockID]
258 258 del self.blockRanges[blockID]
259 259
260 260 #print inputRange,self.current_block_range()
261 261 self.insert_text('\n' +
262 262 self.output_prompt(number=result['number']) +
263 263 result.get('display',{}).get('pprint','') +
264 264 '\n\n',
265 265 textRange=NSMakeRange(inputRange.location+inputRange.length,
266 266 0))
267 267 return result
268 268
269 269
270 270 def render_error(self, failure):
271 271 self.insert_text('\n' +
272 272 self.output_prompt() +
273 273 '\n' +
274 274 failure.getErrorMessage() +
275 275 '\n\n')
276 276 self.start_new_block()
277 277 return failure
278 278
279 279
280 280 def _start_cli_banner(self):
281 281 """Print banner"""
282 282
283 283 banner = """IPython1 %s -- An enhanced Interactive Python.""" % \
284 284 IPython.__version__
285 285
286 286 self.insert_text(banner + '\n\n')
287 287
288 288
289 289 def start_new_block(self):
290 290 """"""
291 291
292 292 self.currentBlockID = self.next_block_ID()
293 293
294 294
295 295
296 296 def next_block_ID(self):
297 297
298 298 return uuid.uuid4()
299 299
300 300 def current_block_range(self):
301 301 return self.blockRanges.get(self.currentBlockID,
302 302 NSMakeRange(self.textView.textStorage().length(),
303 303 0))
304 304
305 305 def current_block(self):
306 306 """The current block's text"""
307 307
308 308 return self.text_for_range(self.current_block_range())
309 309
310 310 def text_for_range(self, textRange):
311 311 """text_for_range"""
312 312
313 313 ts = self.textView.textStorage()
314 314 return ts.string().substringWithRange_(textRange)
315 315
316 316 def current_line(self):
317 317 block = self.text_for_range(self.current_block_range())
318 318 block = block.split('\n')
319 319 return block[-1]
320 320
321 321
322 322 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
323 323 """Insert text into textView at textRange, updating blockRanges
324 324 as necessary
325 325 """
326 326
327 327 if(textRange == None):
328 328 #range for end of text
329 329 textRange = NSMakeRange(self.textView.textStorage().length(), 0)
330 330
331 331 for r in self.blockRanges.itervalues():
332 332 intersection = NSIntersectionRange(r,textRange)
333 333 if(intersection.length == 0): #ranges don't intersect
334 334 if r.location >= textRange.location:
335 335 r.location += len(string)
336 336 else: #ranges intersect
337 337 if(r.location <= textRange.location):
338 338 assert(intersection.length == textRange.length)
339 339 r.length += textRange.length
340 340 else:
341 341 r.location += intersection.length
342 342
343 343 self.textView.replaceCharactersInRange_withString_(
344 344 textRange, string)
345 345 self.textView.setSelectedRange_(
346 346 NSMakeRange(textRange.location+len(string), 0))
347 347 if(scrollToVisible):
348 348 self.textView.scrollRangeToVisible_(textRange)
349 349
350 350
351 351
352 352
353 353 def replace_current_block_with_string(self, textView, string):
354 354 textView.replaceCharactersInRange_withString_(
355 355 self.current_block_range(),
356 356 string)
357 357 self.current_block_range().length = len(string)
358 358 r = NSMakeRange(textView.textStorage().length(), 0)
359 359 textView.scrollRangeToVisible_(r)
360 360 textView.setSelectedRange_(r)
361 361
362 362
363 363 def current_indent_string(self):
364 364 """returns string for indent or None if no indent"""
365 365
366 366 return self._indent_for_block(self.current_block())
367 367
368 368
369 369 def _indent_for_block(self, block):
370 370 lines = block.split('\n')
371 371 if(len(lines) > 1):
372 372 currentIndent = len(lines[-1]) - len(lines[-1].lstrip())
373 373 if(currentIndent == 0):
374 374 currentIndent = self.tabSpaces
375 375
376 376 if(self.tabUsesSpaces):
377 377 result = ' ' * currentIndent
378 378 else:
379 379 result = '\t' * (currentIndent/self.tabSpaces)
380 380 else:
381 381 result = None
382 382
383 383 return result
384 384
385 385
386 386 # NSTextView delegate methods...
387 387 def textView_doCommandBySelector_(self, textView, selector):
388 388 assert(textView == self.textView)
389 389 NSLog("textView_doCommandBySelector_: "+selector)
390 390
391 391
392 392 if(selector == 'insertNewline:'):
393 393 indent = self.current_indent_string()
394 394 if(indent):
395 395 line = indent + self.current_line()
396 396 else:
397 397 line = self.current_line()
398 398
399 399 if(self.is_complete(self.current_block())):
400 400 self.execute(self.current_block(),
401 401 blockID=self.currentBlockID)
402 402 self.start_new_block()
403 403
404 404 return True
405 405
406 406 return False
407 407
408 408 elif(selector == 'moveUp:'):
409 409 prevBlock = self.get_history_previous(self.current_block())
410 410 if(prevBlock != None):
411 411 self.replace_current_block_with_string(textView, prevBlock)
412 412 else:
413 413 NSBeep()
414 414 return True
415 415
416 416 elif(selector == 'moveDown:'):
417 417 nextBlock = self.get_history_next()
418 418 if(nextBlock != None):
419 419 self.replace_current_block_with_string(textView, nextBlock)
420 420 else:
421 421 NSBeep()
422 422 return True
423 423
424 424 elif(selector == 'moveToBeginningOfParagraph:'):
425 425 textView.setSelectedRange_(NSMakeRange(
426 426 self.current_block_range().location,
427 427 0))
428 428 return True
429 429 elif(selector == 'moveToEndOfParagraph:'):
430 430 textView.setSelectedRange_(NSMakeRange(
431 431 self.current_block_range().location + \
432 432 self.current_block_range().length, 0))
433 433 return True
434 434 elif(selector == 'deleteToEndOfParagraph:'):
435 435 if(textView.selectedRange().location <= \
436 436 self.current_block_range().location):
437 437 # Intersect the selected range with the current line range
438 438 if(self.current_block_range().length < 0):
439 439 self.blockRanges[self.currentBlockID].length = 0
440 440
441 441 r = NSIntersectionRange(textView.rangesForUserTextChange()[0],
442 442 self.current_block_range())
443 443
444 444 if(r.length > 0): #no intersection
445 445 textView.setSelectedRange_(r)
446 446
447 447 return False # don't actually handle the delete
448 448
449 449 elif(selector == 'insertTab:'):
450 450 if(len(self.current_line().strip()) == 0): #only white space
451 451 return False
452 452 else:
453 453 self.textView.complete_(self)
454 454 return True
455 455
456 456 elif(selector == 'deleteBackward:'):
457 457 #if we're at the beginning of the current block, ignore
458 458 if(textView.selectedRange().location == \
459 459 self.current_block_range().location):
460 460 return True
461 461 else:
462 462 self.current_block_range().length-=1
463 463 return False
464 464 return False
465 465
466 466
467 467 def textView_shouldChangeTextInRanges_replacementStrings_(self,
468 468 textView, ranges, replacementStrings):
469 469 """
470 470 Delegate method for NSTextView.
471 471
472 472 Refuse change text in ranges not at end, but make those changes at
473 473 end.
474 474 """
475 475
476 476 assert(len(ranges) == len(replacementStrings))
477 477 allow = True
478 478 for r,s in zip(ranges, replacementStrings):
479 479 r = r.rangeValue()
480 480 if(textView.textStorage().length() > 0 and
481 481 r.location < self.current_block_range().location):
482 482 self.insert_text(s)
483 483 allow = False
484 484
485 485
486 486 self.blockRanges.setdefault(self.currentBlockID,
487 487 self.current_block_range()).length +=\
488 488 len(s)
489 489
490 490 return allow
491 491
492 492 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self,
493 493 textView, words, charRange, index):
494 494 try:
495 495 ts = textView.textStorage()
496 496 token = ts.string().substringWithRange_(charRange)
497 497 completions = blockingCallFromThread(self.complete, token)
498 498 except:
499 499 completions = objc.nil
500 500 NSBeep()
501 501
502 502 return (completions,0)
503 503
504 504
@@ -1,34 +1,35 b''
1 1 # encoding: utf-8
2 2 """
3 3 setup.py
4 4
5 5 Setuptools installer script for generating a Cocoa plugin for the
6 6 IPython cocoa frontend
7 7
8 8 Author: Barry Wark
9 9 """
10 10 __docformat__ = "restructuredtext en"
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Copyright (C) 2008 The IPython Development Team
14 14 #
15 15 # Distributed under the terms of the BSD License. The full license is in
16 16 # the file COPYING, distributed as part of this software.
17 17 #-----------------------------------------------------------------------------
18 18
19 19 from setuptools import setup
20 20
21 21 infoPlist = dict(
22 22 CFBundleDevelopmentRegion='English',
23 23 CFBundleIdentifier='org.scipy.ipython.cocoa_frontend',
24 24 NSPrincipalClass='IPythonCocoaController',
25 25 )
26 26
27 27 setup(
28 28 plugin=['IPythonCocoaFrontendLoader.py'],
29 29 setup_requires=['py2app'],
30 30 options=dict(py2app=dict(
31 31 plist=infoPlist,
32 excludes=['IPython','twisted']
32 site_packages=True,
33 excludes=['IPython','twisted','PyObjCTools']
33 34 )),
34 35 ) No newline at end of file
@@ -1,349 +1,396 b''
1 1 # encoding: utf-8
2 2 # -*- test-case-name: IPython.frontend.tests.test_frontendbase -*-
3 3 """
4 4 frontendbase provides an interface and base class for GUI frontends for
5 5 IPython.kernel/IPython.kernel.core.
6 6
7 7 Frontend implementations will likely want to subclass FrontEndBase.
8 8
9 9 Author: Barry Wark
10 10 """
11 11 __docformat__ = "restructuredtext en"
12 12
13 13 #-------------------------------------------------------------------------------
14 14 # Copyright (C) 2008 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 import string
24 24 import uuid
25 25 import _ast
26 26
27 27 import zope.interface as zi
28 28
29 29 from IPython.kernel.core.history import FrontEndHistory
30 30 from IPython.kernel.core.util import Bunch
31 31 from IPython.kernel.engineservice import IEngineCore
32 32
33 33 from twisted.python.failure import Failure
34 34
35 35 ##############################################################################
36 36 # TEMPORARY!!! fake configuration, while we decide whether to use tconfig or
37 37 # not
38 38
39 39 rc = Bunch()
40 40 rc.prompt_in1 = r'In [$number]: '
41 41 rc.prompt_in2 = r'...'
42 42 rc.prompt_out = r'Out [$number]: '
43 43
44 44 ##############################################################################
45 45
46 46 class IFrontEndFactory(zi.Interface):
47 47 """Factory interface for frontends."""
48 48
49 49 def __call__(engine=None, history=None):
50 50 """
51 51 Parameters:
52 52 interpreter : IPython.kernel.engineservice.IEngineCore
53 53 """
54 54
55 55 pass
56 56
57 57
58 58
59 59 class IFrontEnd(zi.Interface):
60 60 """Interface for frontends. All methods return t.i.d.Deferred"""
61 61
62 62 zi.Attribute("input_prompt_template", "string.Template instance\
63 63 substituteable with execute result.")
64 64 zi.Attribute("output_prompt_template", "string.Template instance\
65 65 substituteable with execute result.")
66 66 zi.Attribute("continuation_prompt_template", "string.Template instance\
67 67 substituteable with execute result.")
68 68
69 69 def update_cell_prompt(result, blockID=None):
70 70 """Subclass may override to update the input prompt for a block.
71 71 Since this method will be called as a
72 72 twisted.internet.defer.Deferred's callback/errback,
73 73 implementations should return result when finished.
74 74
75 75 Result is a result dict in case of success, and a
76 76 twisted.python.util.failure.Failure in case of an error
77 77 """
78 78
79 79 pass
80 80
81 81
82 82 def render_result(result):
83 83 """Render the result of an execute call. Implementors may choose the
84 84 method of rendering.
85 85 For example, a notebook-style frontend might render a Chaco plot
86 86 inline.
87 87
88 88 Parameters:
89 89 result : dict (result of IEngineBase.execute )
90 90 blockID = result['blockID']
91 91
92 92 Result:
93 93 Output of frontend rendering
94 94 """
95 95
96 96 pass
97 97
98 98 def render_error(failure):
99 99 """Subclasses must override to render the failure. Since this method
100 100 will be called as a twisted.internet.defer.Deferred's callback,
101 101 implementations should return result when finished.
102 102
103 103 blockID = failure.blockID
104 104 """
105 105
106 106 pass
107 107
108 108
109 109 def input_prompt(number=''):
110 110 """Returns the input prompt by subsituting into
111 111 self.input_prompt_template
112 112 """
113 113 pass
114 114
115 115 def output_prompt(number=''):
116 116 """Returns the output prompt by subsituting into
117 117 self.output_prompt_template
118 118 """
119 119
120 120 pass
121 121
122 122 def continuation_prompt():
123 123 """Returns the continuation prompt by subsituting into
124 124 self.continuation_prompt_template
125 125 """
126 126
127 127 pass
128 128
129 129 def is_complete(block):
130 130 """Returns True if block is complete, False otherwise."""
131 131
132 132 pass
133 133
134 134 def compile_ast(block):
135 135 """Compiles block to an _ast.AST"""
136 136
137 137 pass
138 138
139 139
140 140 def get_history_previous(currentBlock):
141 141 """Returns the block previous in the history. Saves currentBlock if
142 142 the history_cursor is currently at the end of the input history"""
143 143 pass
144 144
145 145 def get_history_next():
146 146 """Returns the next block in the history."""
147 147
148 148 pass
149 149
150 150
151 151 class FrontEndBase(object):
152 152 """
153 153 FrontEndBase manages the state tasks for a CLI frontend:
154 154 - Input and output history management
155 155 - Input/continuation and output prompt generation
156 156
157 157 Some issues (due to possibly unavailable engine):
158 158 - How do we get the current cell number for the engine?
159 159 - How do we handle completions?
160 160 """
161 161
162 zi.implements(IFrontEnd)
163 zi.classProvides(IFrontEndFactory)
164
165 162 history_cursor = 0
166 163
167 164 current_indent_level = 0
168 165
169 166
170 167 input_prompt_template = string.Template(rc.prompt_in1)
171 168 output_prompt_template = string.Template(rc.prompt_out)
172 169 continuation_prompt_template = string.Template(rc.prompt_in2)
173 170
174 def __init__(self, engine=None, history=None):
175 assert(engine==None or IEngineCore.providedBy(engine))
176 self.engine = IEngineCore(engine)
171 def __init__(self, shell=None, history=None):
172 self.shell = shell
177 173 if history is None:
178 174 self.history = FrontEndHistory(input_cache=[''])
179 175 else:
180 176 self.history = history
181 177
182 178
183 179 def input_prompt(self, number=''):
184 180 """Returns the current input prompt
185 181
186 182 It would be great to use ipython1.core.prompts.Prompt1 here
187 183 """
188 184 return self.input_prompt_template.safe_substitute({'number':number})
189 185
190 186
191 187 def continuation_prompt(self):
192 188 """Returns the current continuation prompt"""
193 189
194 190 return self.continuation_prompt_template.safe_substitute()
195 191
196 192 def output_prompt(self, number=''):
197 193 """Returns the output prompt for result"""
198 194
199 195 return self.output_prompt_template.safe_substitute({'number':number})
200 196
201 197
202 198 def is_complete(self, block):
203 199 """Determine if block is complete.
204 200
205 201 Parameters
206 202 block : string
207 203
208 204 Result
209 205 True if block can be sent to the engine without compile errors.
210 206 False otherwise.
211 207 """
212 208
213 209 try:
214 210 ast = self.compile_ast(block)
215 211 except:
216 212 return False
217 213
218 214 lines = block.split('\n')
219 215 return (len(lines)==1 or str(lines[-1])=='')
220 216
221 217
222 218 def compile_ast(self, block):
223 219 """Compile block to an AST
224 220
225 221 Parameters:
226 222 block : str
227 223
228 224 Result:
229 225 AST
230 226
231 227 Throws:
232 228 Exception if block cannot be compiled
233 229 """
234 230
235 231 return compile(block, "<string>", "exec", _ast.PyCF_ONLY_AST)
236 232
237 233
238 234 def execute(self, block, blockID=None):
239 """Execute the block and return result.
235 """Execute the block and return the result.
240 236
241 237 Parameters:
242 238 block : {str, AST}
243 239 blockID : any
244 240 Caller may provide an ID to identify this block.
245 241 result['blockID'] := blockID
246 242
247 243 Result:
248 244 Deferred result of self.interpreter.execute
249 245 """
250 246
251 247 if(not self.is_complete(block)):
252 return Failure(Exception("Block is not compilable"))
248 raise Exception("Block is not compilable")
253 249
254 250 if(blockID == None):
255 251 blockID = uuid.uuid4() #random UUID
256 252
257 d = self.engine.execute(block)
258 d.addCallback(self._add_history, block=block)
259 d.addCallbacks(self._add_block_id_for_result,
260 errback=self._add_block_id_for_failure,
261 callbackArgs=(blockID,),
262 errbackArgs=(blockID,))
263 d.addBoth(self.update_cell_prompt, blockID=blockID)
264 d.addCallbacks(self.render_result,
265 errback=self.render_error)
253 try:
254 result = self.shell.execute(block)
255 except Exception,e:
256 e = self._add_block_id_for_failure(e, blockID=blockID)
257 e = self.update_cell_prompt(e, blockID=blockID)
258 e = self.render_error(e)
259 else:
260 result = self._add_block_id_for_result(result, blockID=blockID)
261 result = self.update_cell_prompt(result, blockID=blockID)
262 result = self.render_result(result)
266 263
267 return d
264 return result
268 265
269 266
270 267 def _add_block_id_for_result(self, result, blockID):
271 268 """Add the blockID to result or failure. Unfortunatley, we have to
272 269 treat failures differently than result dicts.
273 270 """
274 271
275 272 result['blockID'] = blockID
276 273
277 274 return result
278 275
279 276 def _add_block_id_for_failure(self, failure, blockID):
280 277 """_add_block_id_for_failure"""
281 278
282 279 failure.blockID = blockID
283 280 return failure
284 281
285 282
286 283 def _add_history(self, result, block=None):
287 284 """Add block to the history"""
288 285
289 286 assert(block != None)
290 287 self.history.add_items([block])
291 288 self.history_cursor += 1
292 289
293 290 return result
294 291
295 292
296 293 def get_history_previous(self, currentBlock):
297 294 """ Returns previous history string and decrement history cursor.
298 295 """
299 296 command = self.history.get_history_item(self.history_cursor - 1)
300 297
301 298 if command is not None:
302 299 if(self.history_cursor == len(self.history.input_cache)):
303 300 self.history.input_cache[self.history_cursor] = currentBlock
304 301 self.history_cursor -= 1
305 302 return command
306 303
307 304
308 305 def get_history_next(self):
309 306 """ Returns next history string and increment history cursor.
310 307 """
311 308 command = self.history.get_history_item(self.history_cursor+1)
312 309
313 310 if command is not None:
314 311 self.history_cursor += 1
315 312 return command
316 313
317 314 ###
318 315 # Subclasses probably want to override these methods...
319 316 ###
320 317
321 318 def update_cell_prompt(self, result, blockID=None):
322 319 """Subclass may override to update the input prompt for a block.
323 320 Since this method will be called as a
324 321 twisted.internet.defer.Deferred's callback, implementations should
325 322 return result when finished.
326 323 """
327 324
328 325 return result
329 326
330 327
331 328 def render_result(self, result):
332 329 """Subclasses must override to render result. Since this method will
333 330 be called as a twisted.internet.defer.Deferred's callback,
334 331 implementations should return result when finished.
335 332 """
336 333
337 334 return result
338 335
339 336
340 337 def render_error(self, failure):
341 338 """Subclasses must override to render the failure. Since this method
342 339 will be called as a twisted.internet.defer.Deferred's callback,
343 340 implementations should return result when finished.
344 341 """
345 342
346 343 return failure
347 344
348 345
349 346
347 class AsynchronousFrontEndBase(FrontEndBase):
348 """
349 Overrides FrontEndBase to wrap execute in a deferred result.
350 All callbacks are made as callbacks on the deferred result.
351 """
352
353 zi.implements(IFrontEnd)
354 zi.classProvides(IFrontEndFactory)
355
356 def __init__(self, engine=None, history=None):
357 assert(engine==None or IEngineCore.providedBy(engine))
358 self.engine = IEngineCore(engine)
359 if history is None:
360 self.history = FrontEndHistory(input_cache=[''])
361 else:
362 self.history = history
363
364
365 def execute(self, block, blockID=None):
366 """Execute the block and return the deferred result.
367
368 Parameters:
369 block : {str, AST}
370 blockID : any
371 Caller may provide an ID to identify this block.
372 result['blockID'] := blockID
373
374 Result:
375 Deferred result of self.interpreter.execute
376 """
377
378 if(not self.is_complete(block)):
379 return Failure(Exception("Block is not compilable"))
380
381 if(blockID == None):
382 blockID = uuid.uuid4() #random UUID
383
384 d = self.engine.execute(block)
385 d.addCallback(self._add_history, block=block)
386 d.addCallbacks(self._add_block_id_for_result,
387 errback=self._add_block_id_for_failure,
388 callbackArgs=(blockID,),
389 errbackArgs=(blockID,))
390 d.addBoth(self.update_cell_prompt, blockID=blockID)
391 d.addCallbacks(self.render_result,
392 errback=self.render_error)
393
394 return d
395
396
@@ -1,151 +1,151 b''
1 1 # encoding: utf-8
2 2
3 3 """This file contains unittests for the frontendbase module."""
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
20 20 from IPython.kernel.engineservice import EngineService
21 21
22 class FrontEndCallbackChecker(frontendbase.FrontEndBase):
22 class FrontEndCallbackChecker(frontendbase.AsynchronousFrontEndBase):
23 23 """FrontEndBase subclass for checking callbacks"""
24 24 def __init__(self, engine=None, history=None):
25 25 super(FrontEndCallbackChecker, self).__init__(engine=engine,
26 26 history=history)
27 27 self.updateCalled = False
28 28 self.renderResultCalled = False
29 29 self.renderErrorCalled = False
30 30
31 31 def update_cell_prompt(self, result, blockID=None):
32 32 self.updateCalled = True
33 33 return result
34 34
35 35 def render_result(self, result):
36 36 self.renderResultCalled = True
37 37 return result
38 38
39 39
40 40 def render_error(self, failure):
41 41 self.renderErrorCalled = True
42 42 return failure
43 43
44 44
45 45
46 46
47 class TestFrontendBase(unittest.TestCase):
47 class TestAsynchronousFrontendBase(unittest.TestCase):
48 48 def setUp(self):
49 49 """Setup the EngineService and FrontEndBase"""
50 50
51 51 self.fb = FrontEndCallbackChecker(engine=EngineService())
52 52
53 53
54 54 def test_implements_IFrontEnd(self):
55 55 assert(frontendbase.IFrontEnd.implementedBy(
56 frontendbase.FrontEndBase))
56 frontendbase.AsynchronousFrontEndBase))
57 57
58 58
59 59 def test_is_complete_returns_False_for_incomplete_block(self):
60 60 """"""
61 61
62 62 block = """def test(a):"""
63 63
64 64 assert(self.fb.is_complete(block) == False)
65 65
66 66 def test_is_complete_returns_True_for_complete_block(self):
67 67 """"""
68 68
69 69 block = """def test(a): pass"""
70 70
71 71 assert(self.fb.is_complete(block))
72 72
73 73 block = """a=3"""
74 74
75 75 assert(self.fb.is_complete(block))
76 76
77 77
78 78 def test_blockID_added_to_result(self):
79 79 block = """3+3"""
80 80
81 81 d = self.fb.execute(block, blockID='TEST_ID')
82 82
83 83 d.addCallback(self.checkBlockID, expected='TEST_ID')
84 84
85 85 def test_blockID_added_to_failure(self):
86 86 block = "raise Exception()"
87 87
88 88 d = self.fb.execute(block,blockID='TEST_ID')
89 89 d.addErrback(self.checkFailureID, expected='TEST_ID')
90 90
91 91 def checkBlockID(self, result, expected=""):
92 92 assert(result['blockID'] == expected)
93 93
94 94
95 95 def checkFailureID(self, failure, expected=""):
96 96 assert(failure.blockID == expected)
97 97
98 98
99 99 def test_callbacks_added_to_execute(self):
100 100 """test that
101 101 update_cell_prompt
102 102 render_result
103 103
104 104 are added to execute request
105 105 """
106 106
107 107 d = self.fb.execute("10+10")
108 108 d.addCallback(self.checkCallbacks)
109 109
110 110
111 111 def checkCallbacks(self, result):
112 112 assert(self.fb.updateCalled)
113 113 assert(self.fb.renderResultCalled)
114 114
115 115
116 116 def test_error_callback_added_to_execute(self):
117 117 """test that render_error called on execution error"""
118 118
119 119 d = self.fb.execute("raise Exception()")
120 120 d.addCallback(self.checkRenderError)
121 121
122 122 def checkRenderError(self, result):
123 123 assert(self.fb.renderErrorCalled)
124 124
125 125 def test_history_returns_expected_block(self):
126 126 """Make sure history browsing doesn't fail"""
127 127
128 128 blocks = ["a=1","a=2","a=3"]
129 129 for b in blocks:
130 130 d = self.fb.execute(b)
131 131
132 132 # d is now the deferred for the last executed block
133 133 d.addCallback(self.historyTests, blocks)
134 134
135 135
136 136 def historyTests(self, result, blocks):
137 137 """historyTests"""
138 138
139 139 assert(len(blocks) >= 3)
140 140 assert(self.fb.get_history_previous("") == blocks[-2])
141 141 assert(self.fb.get_history_previous("") == blocks[-3])
142 142 assert(self.fb.get_history_next() == blocks[-2])
143 143
144 144
145 145 def test_history_returns_none_at_startup(self):
146 146 """test_history_returns_none_at_startup"""
147 147
148 148 assert(self.fb.get_history_previous("")==None)
149 149 assert(self.fb.get_history_next()==None)
150 150
151 151
General Comments 0
You need to be logged in to leave comments. Login now