##// END OF EJS Templates
Update irunner - needs work on pexpect to work in Python 3.
Thomas Kluyver -
Show More
@@ -1,440 +1,440
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
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: we carry a copy of pexpect to reduce the need for
39 39 # external dependencies, but our import checks for a system version first.
40 40 from IPython.external import pexpect
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 if not isinstance(source,file):
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 >>',
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 >> sys.stderr, e
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 >> sys.stderr,"You must supply exactly one file to run."
286 286 sys.exit(1)
287 287
288 288 self.run_file(args[0],opts.interact)
289 289
290 290
291 291 # Specific runners for particular programs
292 292 class IPythonRunner(InteractiveRunner):
293 293 """Interactive IPython runner.
294 294
295 295 This initalizes IPython in 'nocolor' mode for simplicity. This lets us
296 296 avoid having to write a regexp that matches ANSI sequences, though pexpect
297 297 does support them. If anyone contributes patches for ANSI color support,
298 298 they will be welcome.
299 299
300 300 It also sets the prompts manually, since the prompt regexps for
301 301 pexpect need to be matched to the actual prompts, so user-customized
302 302 prompts would break this.
303 303 """
304 304
305 305 def __init__(self,program = 'ipython',args=None,out=sys.stdout,echo=True):
306 306 """New runner, optionally passing the ipython command to use."""
307 307 args0 = ['--colors=NoColor',
308 308 '--no-term-title',
309 309 '--no-autoindent',
310 310 # '--quick' is important, to prevent loading default config:
311 311 '--quick']
312 312 if args is None: args = args0
313 313 else: args = args0 + args
314 314 prompts = [r'In \[\d+\]: ',r' \.*: ']
315 315 InteractiveRunner.__init__(self,program,prompts,args,out,echo)
316 316
317 317
318 318 class PythonRunner(InteractiveRunner):
319 319 """Interactive Python runner."""
320 320
321 321 def __init__(self,program='python',args=None,out=sys.stdout,echo=True):
322 322 """New runner, optionally passing the python command to use."""
323 323
324 324 prompts = [r'>>> ',r'\.\.\. ']
325 325 InteractiveRunner.__init__(self,program,prompts,args,out,echo)
326 326
327 327
328 328 class SAGERunner(InteractiveRunner):
329 329 """Interactive SAGE runner.
330 330
331 331 WARNING: this runner only works if you manually adjust your SAGE
332 332 configuration so that the 'color' option in the configuration file is set to
333 333 'NoColor', because currently the prompt matching regexp does not identify
334 334 color sequences."""
335 335
336 336 def __init__(self,program='sage',args=None,out=sys.stdout,echo=True):
337 337 """New runner, optionally passing the sage command to use."""
338 338
339 339 prompts = ['sage: ',r'\s*\.\.\. ']
340 340 InteractiveRunner.__init__(self,program,prompts,args,out,echo)
341 341
342 342
343 343 class RunnerFactory(object):
344 344 """Code runner factory.
345 345
346 346 This class provides an IPython code runner, but enforces that only one
347 347 runner is ever instantiated. The runner is created based on the extension
348 348 of the first file to run, and it raises an exception if a runner is later
349 349 requested for a different extension type.
350 350
351 351 This ensures that we don't generate example files for doctest with a mix of
352 352 python and ipython syntax.
353 353 """
354 354
355 355 def __init__(self,out=sys.stdout):
356 356 """Instantiate a code runner."""
357 357
358 358 self.out = out
359 359 self.runner = None
360 360 self.runnerClass = None
361 361
362 362 def _makeRunner(self,runnerClass):
363 363 self.runnerClass = runnerClass
364 364 self.runner = runnerClass(out=self.out)
365 365 return self.runner
366 366
367 367 def __call__(self,fname):
368 368 """Return a runner for the given filename."""
369 369
370 370 if fname.endswith('.py'):
371 371 runnerClass = PythonRunner
372 372 elif fname.endswith('.ipy'):
373 373 runnerClass = IPythonRunner
374 374 else:
375 375 raise ValueError('Unknown file type for Runner: %r' % fname)
376 376
377 377 if self.runner is None:
378 378 return self._makeRunner(runnerClass)
379 379 else:
380 380 if runnerClass==self.runnerClass:
381 381 return self.runner
382 382 else:
383 383 e='A runner of type %r can not run file %r' % \
384 384 (self.runnerClass,fname)
385 385 raise ValueError(e)
386 386
387 387
388 388 # Global usage string, to avoid indentation issues if typed in a function def.
389 389 MAIN_USAGE = """
390 390 %prog [options] file_to_run
391 391
392 392 This is an interface to the various interactive runners available in this
393 393 module. If you want to pass specific options to one of the runners, you need
394 394 to first terminate the main options with a '--', and then provide the runner's
395 395 options. For example:
396 396
397 397 irunner.py --python -- --help
398 398
399 399 will pass --help to the python runner. Similarly,
400 400
401 401 irunner.py --ipython -- --interact script.ipy
402 402
403 403 will run the script.ipy file under the IPython runner, and then will start to
404 404 interact with IPython at the end of the script (instead of exiting).
405 405
406 406 The already implemented runners are listed below; adding one for a new program
407 407 is a trivial task, see the source for examples.
408 408 """
409 409
410 410 def main():
411 411 """Run as a command-line script."""
412 412
413 413 parser = optparse.OptionParser(usage=MAIN_USAGE)
414 414 newopt = parser.add_option
415 415 newopt('--ipython',action='store_const',dest='mode',const='ipython',
416 416 help='IPython interactive runner (default).')
417 417 newopt('--python',action='store_const',dest='mode',const='python',
418 418 help='Python interactive runner.')
419 419 newopt('--sage',action='store_const',dest='mode',const='sage',
420 420 help='SAGE interactive runner.')
421 421
422 422 opts,args = parser.parse_args()
423 423 runners = dict(ipython=IPythonRunner,
424 424 python=PythonRunner,
425 425 sage=SAGERunner)
426 426
427 427 try:
428 428 ext = os.path.splitext(args[0])[-1]
429 429 except IndexError:
430 430 ext = ''
431 431 modes = {'.ipy':'ipython',
432 432 '.py':'python',
433 433 '.sage':'sage'}
434 434 mode = modes.get(ext,"ipython")
435 435 if opts.mode:
436 436 mode = opts.mode
437 437 runners[mode]().main(args)
438 438
439 439 if __name__ == '__main__':
440 440 main()
@@ -1,14 +1,18
1 1 import os.path
2 2 from setuptools import setup
3 3
4 from setupbase import (setup_args, find_scripts, find_packages, find_package_data)
4 from setupbase import (setup_args,
5 find_scripts,
6 find_packages,
7 find_package_data,
8 )
5 9
6 10 setup_args['entry_points'] = find_scripts(True, suffix='3')
7 11 setup_args['packages'] = find_packages()
8 12 setup_args['package_data'] = find_package_data()
9 13
10 14 def main():
11 15 setup(use_2to3 = True, **setup_args)
12 16
13 17 if __name__ == "__main__":
14 18 main()
General Comments 0
You need to be logged in to leave comments. Login now