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