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