##// END OF EJS Templates
add '--quick' to irunner args, to prevent loading default config
MinRK -
Show More
@@ -1,440 +1,441 b''
1 1 #!/usr/bin/env python
2 2 """Module for interactively running scripts.
3 3
4 4 This module implements classes for interactively running scripts written for
5 5 any system with a prompt which can be matched by a regexp suitable for
6 6 pexpect. It can be used to run as if they had been typed up interactively, an
7 7 arbitrary series of commands for the target system.
8 8
9 9 The module includes classes ready for IPython (with the default prompts),
10 10 plain Python and SAGE, but making a new one is trivial. To see how to use it,
11 11 simply run the module as a script:
12 12
13 13 ./irunner.py --help
14 14
15 15
16 16 This is an extension of Ken Schutte <kschutte-AT-csail.mit.edu>'s script
17 17 contributed on the ipython-user list:
18 18
19 19 http://scipy.net/pipermail/ipython-user/2006-May/001705.html
20 20
21 21
22 22 NOTES:
23 23
24 24 - This module requires pexpect, available in most linux distros, or which can
25 25 be downloaded from
26 26
27 27 http://pexpect.sourceforge.net
28 28
29 29 - Because pexpect only works under Unix or Windows-Cygwin, this has the same
30 30 limitations. This means that it will NOT work under native windows Python.
31 31 """
32 32
33 33 # Stdlib imports
34 34 import optparse
35 35 import os
36 36 import sys
37 37
38 38 # Third-party modules.
39 39 import pexpect
40 40
41 41 # Global usage strings, to avoid indentation issues when typing it below.
42 42 USAGE = """
43 43 Interactive script runner, type: %s
44 44
45 45 runner [opts] script_name
46 46 """
47 47
48 48 def pexpect_monkeypatch():
49 49 """Patch pexpect to prevent unhandled exceptions at VM teardown.
50 50
51 51 Calling this function will monkeypatch the pexpect.spawn class and modify
52 52 its __del__ method to make it more robust in the face of failures that can
53 53 occur if it is called when the Python VM is shutting down.
54 54
55 55 Since Python may fire __del__ methods arbitrarily late, it's possible for
56 56 them to execute during the teardown of the Python VM itself. At this
57 57 point, various builtin modules have been reset to None. Thus, the call to
58 58 self.close() will trigger an exception because it tries to call os.close(),
59 59 and os is now None.
60 60 """
61 61
62 62 if pexpect.__version__[:3] >= '2.2':
63 63 # No need to patch, fix is already the upstream version.
64 64 return
65 65
66 66 def __del__(self):
67 67 """This makes sure that no system resources are left open.
68 68 Python only garbage collects Python objects. OS file descriptors
69 69 are not Python objects, so they must be handled explicitly.
70 70 If the child file descriptor was opened outside of this class
71 71 (passed to the constructor) then this does not close it.
72 72 """
73 73 if not self.closed:
74 74 try:
75 75 self.close()
76 76 except AttributeError:
77 77 pass
78 78
79 79 pexpect.spawn.__del__ = __del__
80 80
81 81 pexpect_monkeypatch()
82 82
83 83 # The generic runner class
84 84 class InteractiveRunner(object):
85 85 """Class to run a sequence of commands through an interactive program."""
86 86
87 87 def __init__(self,program,prompts,args=None,out=sys.stdout,echo=True):
88 88 """Construct a runner.
89 89
90 90 Inputs:
91 91
92 92 - program: command to execute the given program.
93 93
94 94 - prompts: a list of patterns to match as valid prompts, in the
95 95 format used by pexpect. This basically means that it can be either
96 96 a string (to be compiled as a regular expression) or a list of such
97 97 (it must be a true list, as pexpect does type checks).
98 98
99 99 If more than one prompt is given, the first is treated as the main
100 100 program prompt and the others as 'continuation' prompts, like
101 101 python's. This means that blank lines in the input source are
102 102 ommitted when the first prompt is matched, but are NOT ommitted when
103 103 the continuation one matches, since this is how python signals the
104 104 end of multiline input interactively.
105 105
106 106 Optional inputs:
107 107
108 108 - args(None): optional list of strings to pass as arguments to the
109 109 child program.
110 110
111 111 - out(sys.stdout): if given, an output stream to be used when writing
112 112 output. The only requirement is that it must have a .write() method.
113 113
114 114 Public members not parameterized in the constructor:
115 115
116 116 - delaybeforesend(0): Newer versions of pexpect have a delay before
117 117 sending each new input. For our purposes here, it's typically best
118 118 to just set this to zero, but if you encounter reliability problems
119 119 or want an interactive run to pause briefly at each prompt, just
120 120 increase this value (it is measured in seconds). Note that this
121 121 variable is not honored at all by older versions of pexpect.
122 122 """
123 123
124 124 self.program = program
125 125 self.prompts = prompts
126 126 if args is None: args = []
127 127 self.args = args
128 128 self.out = out
129 129 self.echo = echo
130 130 # Other public members which we don't make as parameters, but which
131 131 # users may occasionally want to tweak
132 132 self.delaybeforesend = 0
133 133
134 134 # Create child process and hold on to it so we don't have to re-create
135 135 # for every single execution call
136 136 c = self.child = pexpect.spawn(self.program,self.args,timeout=None)
137 137 c.delaybeforesend = self.delaybeforesend
138 138 # pexpect hard-codes the terminal size as (24,80) (rows,columns).
139 139 # This causes problems because any line longer than 80 characters gets
140 140 # completely overwrapped on the printed outptut (even though
141 141 # internally the code runs fine). We reset this to 99 rows X 200
142 142 # columns (arbitrarily chosen), which should avoid problems in all
143 143 # reasonable cases.
144 144 c.setwinsize(99,200)
145 145
146 146 def close(self):
147 147 """close child process"""
148 148
149 149 self.child.close()
150 150
151 151 def run_file(self,fname,interact=False,get_output=False):
152 152 """Run the given file interactively.
153 153
154 154 Inputs:
155 155
156 156 -fname: name of the file to execute.
157 157
158 158 See the run_source docstring for the meaning of the optional
159 159 arguments."""
160 160
161 161 fobj = open(fname,'r')
162 162 try:
163 163 out = self.run_source(fobj,interact,get_output)
164 164 finally:
165 165 fobj.close()
166 166 if get_output:
167 167 return out
168 168
169 169 def run_source(self,source,interact=False,get_output=False):
170 170 """Run the given source code interactively.
171 171
172 172 Inputs:
173 173
174 174 - source: a string of code to be executed, or an open file object we
175 175 can iterate over.
176 176
177 177 Optional inputs:
178 178
179 179 - interact(False): if true, start to interact with the running
180 180 program at the end of the script. Otherwise, just exit.
181 181
182 182 - get_output(False): if true, capture the output of the child process
183 183 (filtering the input commands out) and return it as a string.
184 184
185 185 Returns:
186 186 A string containing the process output, but only if requested.
187 187 """
188 188
189 189 # if the source is a string, chop it up in lines so we can iterate
190 190 # over it just as if it were an open file.
191 191 if not isinstance(source,file):
192 192 source = source.splitlines(True)
193 193
194 194 if self.echo:
195 195 # normalize all strings we write to use the native OS line
196 196 # separators.
197 197 linesep = os.linesep
198 198 stdwrite = self.out.write
199 199 write = lambda s: stdwrite(s.replace('\r\n',linesep))
200 200 else:
201 201 # Quiet mode, all writes are no-ops
202 202 write = lambda s: None
203 203
204 204 c = self.child
205 205 prompts = c.compile_pattern_list(self.prompts)
206 206 prompt_idx = c.expect_list(prompts)
207 207
208 208 # Flag whether the script ends normally or not, to know whether we can
209 209 # do anything further with the underlying process.
210 210 end_normal = True
211 211
212 212 # If the output was requested, store it in a list for return at the end
213 213 if get_output:
214 214 output = []
215 215 store_output = output.append
216 216
217 217 for cmd in source:
218 218 # skip blank lines for all matches to the 'main' prompt, while the
219 219 # secondary prompts do not
220 220 if prompt_idx==0 and \
221 221 (cmd.isspace() or cmd.lstrip().startswith('#')):
222 222 write(cmd)
223 223 continue
224 224
225 225 # write('AFTER: '+c.after) # dbg
226 226 write(c.after)
227 227 c.send(cmd)
228 228 try:
229 229 prompt_idx = c.expect_list(prompts)
230 230 except pexpect.EOF:
231 231 # this will happen if the child dies unexpectedly
232 232 write(c.before)
233 233 end_normal = False
234 234 break
235 235
236 236 write(c.before)
237 237
238 238 # With an echoing process, the output we get in c.before contains
239 239 # the command sent, a newline, and then the actual process output
240 240 if get_output:
241 241 store_output(c.before[len(cmd+'\n'):])
242 242 #write('CMD: <<%s>>' % cmd) # dbg
243 243 #write('OUTPUT: <<%s>>' % output[-1]) # dbg
244 244
245 245 self.out.flush()
246 246 if end_normal:
247 247 if interact:
248 248 c.send('\n')
249 249 print '<< Starting interactive mode >>',
250 250 try:
251 251 c.interact()
252 252 except OSError:
253 253 # This is what fires when the child stops. Simply print a
254 254 # newline so the system prompt is aligned. The extra
255 255 # space is there to make sure it gets printed, otherwise
256 256 # OS buffering sometimes just suppresses it.
257 257 write(' \n')
258 258 self.out.flush()
259 259 else:
260 260 if interact:
261 261 e="Further interaction is not possible: child process is dead."
262 262 print >> sys.stderr, e
263 263
264 264 # Leave the child ready for more input later on, otherwise select just
265 265 # hangs on the second invocation.
266 266 if c.isalive():
267 267 c.send('\n')
268 268
269 269 # Return any requested output
270 270 if get_output:
271 271 return ''.join(output)
272 272
273 273 def main(self,argv=None):
274 274 """Run as a command-line script."""
275 275
276 276 parser = optparse.OptionParser(usage=USAGE % self.__class__.__name__)
277 277 newopt = parser.add_option
278 278 newopt('-i','--interact',action='store_true',default=False,
279 279 help='Interact with the program after the script is run.')
280 280
281 281 opts,args = parser.parse_args(argv)
282 282
283 283 if len(args) != 1:
284 284 print >> sys.stderr,"You must supply exactly one file to run."
285 285 sys.exit(1)
286 286
287 287 self.run_file(args[0],opts.interact)
288 288
289 289
290 290 # Specific runners for particular programs
291 291 class IPythonRunner(InteractiveRunner):
292 292 """Interactive IPython runner.
293 293
294 294 This initalizes IPython in 'nocolor' mode for simplicity. This lets us
295 295 avoid having to write a regexp that matches ANSI sequences, though pexpect
296 296 does support them. If anyone contributes patches for ANSI color support,
297 297 they will be welcome.
298 298
299 299 It also sets the prompts manually, since the prompt regexps for
300 300 pexpect need to be matched to the actual prompts, so user-customized
301 301 prompts would break this.
302 302 """
303 303
304 304 def __init__(self,program = 'ipython',args=None,out=sys.stdout,echo=True):
305 305 """New runner, optionally passing the ipython command to use."""
306
307 306 args0 = ['--colors=NoColor',
308 307 '--no-term-title',
309 '--no-autoindent']
308 '--no-autoindent',
309 # '--quick' is important, to prevent loading default config:
310 '--quick']
310 311 if args is None: args = args0
311 312 else: args = args0 + args
312 313 prompts = [r'In \[\d+\]: ',r' \.*: ']
313 314 InteractiveRunner.__init__(self,program,prompts,args,out,echo)
314 315
315 316
316 317 class PythonRunner(InteractiveRunner):
317 318 """Interactive Python runner."""
318 319
319 320 def __init__(self,program='python',args=None,out=sys.stdout,echo=True):
320 321 """New runner, optionally passing the python command to use."""
321 322
322 323 prompts = [r'>>> ',r'\.\.\. ']
323 324 InteractiveRunner.__init__(self,program,prompts,args,out,echo)
324 325
325 326
326 327 class SAGERunner(InteractiveRunner):
327 328 """Interactive SAGE runner.
328 329
329 330 WARNING: this runner only works if you manually configure your SAGE copy
330 331 to use 'colors NoColor' in the ipythonrc config file, since currently the
331 332 prompt matching regexp does not identify color sequences."""
332 333
333 334 def __init__(self,program='sage',args=None,out=sys.stdout,echo=True):
334 335 """New runner, optionally passing the sage command to use."""
335 336
336 337 prompts = ['sage: ',r'\s*\.\.\. ']
337 338 InteractiveRunner.__init__(self,program,prompts,args,out,echo)
338 339
339 340
340 341 class RunnerFactory(object):
341 342 """Code runner factory.
342 343
343 344 This class provides an IPython code runner, but enforces that only one
344 345 runner is ever instantiated. The runner is created based on the extension
345 346 of the first file to run, and it raises an exception if a runner is later
346 347 requested for a different extension type.
347 348
348 349 This ensures that we don't generate example files for doctest with a mix of
349 350 python and ipython syntax.
350 351 """
351 352
352 353 def __init__(self,out=sys.stdout):
353 354 """Instantiate a code runner."""
354 355
355 356 self.out = out
356 357 self.runner = None
357 358 self.runnerClass = None
358 359
359 360 def _makeRunner(self,runnerClass):
360 361 self.runnerClass = runnerClass
361 362 self.runner = runnerClass(out=self.out)
362 363 return self.runner
363 364
364 365 def __call__(self,fname):
365 366 """Return a runner for the given filename."""
366 367
367 368 if fname.endswith('.py'):
368 369 runnerClass = PythonRunner
369 370 elif fname.endswith('.ipy'):
370 371 runnerClass = IPythonRunner
371 372 else:
372 373 raise ValueError('Unknown file type for Runner: %r' % fname)
373 374
374 375 if self.runner is None:
375 376 return self._makeRunner(runnerClass)
376 377 else:
377 378 if runnerClass==self.runnerClass:
378 379 return self.runner
379 380 else:
380 381 e='A runner of type %r can not run file %r' % \
381 382 (self.runnerClass,fname)
382 383 raise ValueError(e)
383 384
384 385
385 386 # Global usage string, to avoid indentation issues if typed in a function def.
386 387 MAIN_USAGE = """
387 388 %prog [options] file_to_run
388 389
389 390 This is an interface to the various interactive runners available in this
390 391 module. If you want to pass specific options to one of the runners, you need
391 392 to first terminate the main options with a '--', and then provide the runner's
392 393 options. For example:
393 394
394 395 irunner.py --python -- --help
395 396
396 397 will pass --help to the python runner. Similarly,
397 398
398 399 irunner.py --ipython -- --interact script.ipy
399 400
400 401 will run the script.ipy file under the IPython runner, and then will start to
401 402 interact with IPython at the end of the script (instead of exiting).
402 403
403 404 The already implemented runners are listed below; adding one for a new program
404 405 is a trivial task, see the source for examples.
405 406
406 407 WARNING: the SAGE runner only works if you manually configure your SAGE copy
407 408 to use 'colors NoColor' in the ipythonrc config file, since currently the
408 409 prompt matching regexp does not identify color sequences.
409 410 """
410 411
411 412 def main():
412 413 """Run as a command-line script."""
413 414
414 415 parser = optparse.OptionParser(usage=MAIN_USAGE)
415 416 newopt = parser.add_option
416 417 parser.set_defaults(mode='ipython')
417 418 newopt('--ipython',action='store_const',dest='mode',const='ipython',
418 419 help='IPython interactive runner (default).')
419 420 newopt('--python',action='store_const',dest='mode',const='python',
420 421 help='Python interactive runner.')
421 422 newopt('--sage',action='store_const',dest='mode',const='sage',
422 423 help='SAGE interactive runner.')
423 424
424 425 opts,args = parser.parse_args()
425 426 runners = dict(ipython=IPythonRunner,
426 427 python=PythonRunner,
427 428 sage=SAGERunner)
428 429
429 430 try:
430 431 ext = os.path.splitext(args[0])[-1]
431 432 except IndexError:
432 433 ext = ''
433 434 modes = {'.ipy':'ipython',
434 435 '.py':'python',
435 436 '.sage':'sage'}
436 437 mode = modes.get(ext,opts.mode)
437 438 runners[mode]().main(args)
438 439
439 440 if __name__ == '__main__':
440 441 main()
General Comments 0
You need to be logged in to leave comments. Login now