##// END OF EJS Templates
- Small extension bug fix (irunner would fail to automatically recognize...
fperez -
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,314 +1,314 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """Module for interactively running scripts.
2 """Module for interactively running scripts.
3
3
4 This module implements classes for interactively running scripts written for
4 This module implements classes for interactively running scripts written for
5 any system with a prompt which can be matched by a regexp suitable for
5 any system with a prompt which can be matched by a regexp suitable for
6 pexpect. It can be used to run as if they had been typed up interactively, an
6 pexpect. It can be used to run as if they had been typed up interactively, an
7 arbitrary series of commands for the target system.
7 arbitrary series of commands for the target system.
8
8
9 The module includes classes ready for IPython (with the default prompts),
9 The module includes classes ready for IPython (with the default prompts),
10 plain Python and SAGE, but making a new one is trivial. To see how to use it,
10 plain Python and SAGE, but making a new one is trivial. To see how to use it,
11 simply run the module as a script:
11 simply run the module as a script:
12
12
13 ./irunner.py --help
13 ./irunner.py --help
14
14
15
15
16 This is an extension of Ken Schutte <kschutte-AT-csail.mit.edu>'s script
16 This is an extension of Ken Schutte <kschutte-AT-csail.mit.edu>'s script
17 contributed on the ipython-user list:
17 contributed on the ipython-user list:
18
18
19 http://scipy.net/pipermail/ipython-user/2006-May/001705.html
19 http://scipy.net/pipermail/ipython-user/2006-May/001705.html
20
20
21
21
22 NOTES:
22 NOTES:
23
23
24 - This module requires pexpect, available in most linux distros, or which can
24 - This module requires pexpect, available in most linux distros, or which can
25 be downloaded from
25 be downloaded from
26
26
27 http://pexpect.sourceforge.net
27 http://pexpect.sourceforge.net
28
28
29 - Because pexpect only works under Unix or Windows-Cygwin, this has the same
29 - Because pexpect only works under Unix or Windows-Cygwin, this has the same
30 limitations. This means that it will NOT work under native windows Python.
30 limitations. This means that it will NOT work under native windows Python.
31 """
31 """
32
32
33 # Stdlib imports
33 # Stdlib imports
34 import optparse
34 import optparse
35 import os
35 import os
36 import sys
36 import sys
37
37
38 # Third-party modules.
38 # Third-party modules.
39 import pexpect
39 import pexpect
40
40
41 # Global usage strings, to avoid indentation issues when typing it below.
41 # Global usage strings, to avoid indentation issues when typing it below.
42 USAGE = """
42 USAGE = """
43 Interactive script runner, type: %s
43 Interactive script runner, type: %s
44
44
45 runner [opts] script_name
45 runner [opts] script_name
46 """
46 """
47
47
48 # The generic runner class
48 # The generic runner class
49 class InteractiveRunner(object):
49 class InteractiveRunner(object):
50 """Class to run a sequence of commands through an interactive program."""
50 """Class to run a sequence of commands through an interactive program."""
51
51
52 def __init__(self,program,prompts,args=None):
52 def __init__(self,program,prompts,args=None):
53 """Construct a runner.
53 """Construct a runner.
54
54
55 Inputs:
55 Inputs:
56
56
57 - program: command to execute the given program.
57 - program: command to execute the given program.
58
58
59 - prompts: a list of patterns to match as valid prompts, in the
59 - prompts: a list of patterns to match as valid prompts, in the
60 format used by pexpect. This basically means that it can be either
60 format used by pexpect. This basically means that it can be either
61 a string (to be compiled as a regular expression) or a list of such
61 a string (to be compiled as a regular expression) or a list of such
62 (it must be a true list, as pexpect does type checks).
62 (it must be a true list, as pexpect does type checks).
63
63
64 If more than one prompt is given, the first is treated as the main
64 If more than one prompt is given, the first is treated as the main
65 program prompt and the others as 'continuation' prompts, like
65 program prompt and the others as 'continuation' prompts, like
66 python's. This means that blank lines in the input source are
66 python's. This means that blank lines in the input source are
67 ommitted when the first prompt is matched, but are NOT ommitted when
67 ommitted when the first prompt is matched, but are NOT ommitted when
68 the continuation one matches, since this is how python signals the
68 the continuation one matches, since this is how python signals the
69 end of multiline input interactively.
69 end of multiline input interactively.
70
70
71 Optional inputs:
71 Optional inputs:
72
72
73 - args(None): optional list of strings to pass as arguments to the
73 - args(None): optional list of strings to pass as arguments to the
74 child program.
74 child program.
75
75
76 Public members not parameterized in the constructor:
76 Public members not parameterized in the constructor:
77
77
78 - delaybeforesend(0): Newer versions of pexpect have a delay before
78 - delaybeforesend(0): Newer versions of pexpect have a delay before
79 sending each new input. For our purposes here, it's typically best
79 sending each new input. For our purposes here, it's typically best
80 to just set this to zero, but if you encounter reliability problems
80 to just set this to zero, but if you encounter reliability problems
81 or want an interactive run to pause briefly at each prompt, just
81 or want an interactive run to pause briefly at each prompt, just
82 increase this value (it is measured in seconds). Note that this
82 increase this value (it is measured in seconds). Note that this
83 variable is not honored at all by older versions of pexpect.
83 variable is not honored at all by older versions of pexpect.
84 """
84 """
85
85
86 self.program = program
86 self.program = program
87 self.prompts = prompts
87 self.prompts = prompts
88 if args is None: args = []
88 if args is None: args = []
89 self.args = args
89 self.args = args
90 # Other public members which we don't make as parameters, but which
90 # Other public members which we don't make as parameters, but which
91 # users may occasionally want to tweak
91 # users may occasionally want to tweak
92 self.delaybeforesend = 0
92 self.delaybeforesend = 0
93
93
94 def run_file(self,fname,interact=False):
94 def run_file(self,fname,interact=False):
95 """Run the given file interactively.
95 """Run the given file interactively.
96
96
97 Inputs:
97 Inputs:
98
98
99 -fname: name of the file to execute.
99 -fname: name of the file to execute.
100
100
101 See the run_source docstring for the meaning of the optional
101 See the run_source docstring for the meaning of the optional
102 arguments."""
102 arguments."""
103
103
104 fobj = open(fname,'r')
104 fobj = open(fname,'r')
105 try:
105 try:
106 self.run_source(fobj,interact)
106 self.run_source(fobj,interact)
107 finally:
107 finally:
108 fobj.close()
108 fobj.close()
109
109
110 def run_source(self,source,interact=False):
110 def run_source(self,source,interact=False):
111 """Run the given source code interactively.
111 """Run the given source code interactively.
112
112
113 Inputs:
113 Inputs:
114
114
115 - source: a string of code to be executed, or an open file object we
115 - source: a string of code to be executed, or an open file object we
116 can iterate over.
116 can iterate over.
117
117
118 Optional inputs:
118 Optional inputs:
119
119
120 - interact(False): if true, start to interact with the running
120 - interact(False): if true, start to interact with the running
121 program at the end of the script. Otherwise, just exit.
121 program at the end of the script. Otherwise, just exit.
122 """
122 """
123
123
124 # if the source is a string, chop it up in lines so we can iterate
124 # if the source is a string, chop it up in lines so we can iterate
125 # over it just as if it were an open file.
125 # over it just as if it were an open file.
126 if not isinstance(source,file):
126 if not isinstance(source,file):
127 source = source.splitlines(True)
127 source = source.splitlines(True)
128
128
129 # grab the true write method of stdout, in case anything later
129 # grab the true write method of stdout, in case anything later
130 # reassigns sys.stdout, so that we really are writing to the true
130 # reassigns sys.stdout, so that we really are writing to the true
131 # stdout and not to something else. We also normalize all strings we
131 # stdout and not to something else. We also normalize all strings we
132 # write to use the native OS line separators.
132 # write to use the native OS line separators.
133 linesep = os.linesep
133 linesep = os.linesep
134 stdwrite = sys.stdout.write
134 stdwrite = sys.stdout.write
135 write = lambda s: stdwrite(s.replace('\r\n',linesep))
135 write = lambda s: stdwrite(s.replace('\r\n',linesep))
136
136
137 c = pexpect.spawn(self.program,self.args,timeout=None)
137 c = pexpect.spawn(self.program,self.args,timeout=None)
138 c.delaybeforesend = self.delaybeforesend
138 c.delaybeforesend = self.delaybeforesend
139
139
140 # pexpect hard-codes the terminal size as (24,80) (rows,columns).
140 # pexpect hard-codes the terminal size as (24,80) (rows,columns).
141 # This causes problems because any line longer than 80 characters gets
141 # This causes problems because any line longer than 80 characters gets
142 # completely overwrapped on the printed outptut (even though
142 # completely overwrapped on the printed outptut (even though
143 # internally the code runs fine). We reset this to 99 rows X 200
143 # internally the code runs fine). We reset this to 99 rows X 200
144 # columns (arbitrarily chosen), which should avoid problems in all
144 # columns (arbitrarily chosen), which should avoid problems in all
145 # reasonable cases.
145 # reasonable cases.
146 c.setwinsize(99,200)
146 c.setwinsize(99,200)
147
147
148 prompts = c.compile_pattern_list(self.prompts)
148 prompts = c.compile_pattern_list(self.prompts)
149
149
150 prompt_idx = c.expect_list(prompts)
150 prompt_idx = c.expect_list(prompts)
151 # Flag whether the script ends normally or not, to know whether we can
151 # Flag whether the script ends normally or not, to know whether we can
152 # do anything further with the underlying process.
152 # do anything further with the underlying process.
153 end_normal = True
153 end_normal = True
154 for cmd in source:
154 for cmd in source:
155 # skip blank lines for all matches to the 'main' prompt, while the
155 # skip blank lines for all matches to the 'main' prompt, while the
156 # secondary prompts do not
156 # secondary prompts do not
157 if prompt_idx==0 and \
157 if prompt_idx==0 and \
158 (cmd.isspace() or cmd.lstrip().startswith('#')):
158 (cmd.isspace() or cmd.lstrip().startswith('#')):
159 print cmd,
159 print cmd,
160 continue
160 continue
161
161
162 write(c.after)
162 write(c.after)
163 c.send(cmd)
163 c.send(cmd)
164 try:
164 try:
165 prompt_idx = c.expect_list(prompts)
165 prompt_idx = c.expect_list(prompts)
166 except pexpect.EOF:
166 except pexpect.EOF:
167 # this will happen if the child dies unexpectedly
167 # this will happen if the child dies unexpectedly
168 write(c.before)
168 write(c.before)
169 end_normal = False
169 end_normal = False
170 break
170 break
171 write(c.before)
171 write(c.before)
172
172
173 if end_normal:
173 if end_normal:
174 if interact:
174 if interact:
175 c.send('\n')
175 c.send('\n')
176 print '<< Starting interactive mode >>',
176 print '<< Starting interactive mode >>',
177 try:
177 try:
178 c.interact()
178 c.interact()
179 except OSError:
179 except OSError:
180 # This is what fires when the child stops. Simply print a
180 # This is what fires when the child stops. Simply print a
181 # newline so the system prompt is aligned. The extra
181 # newline so the system prompt is aligned. The extra
182 # space is there to make sure it gets printed, otherwise
182 # space is there to make sure it gets printed, otherwise
183 # OS buffering sometimes just suppresses it.
183 # OS buffering sometimes just suppresses it.
184 write(' \n')
184 write(' \n')
185 sys.stdout.flush()
185 sys.stdout.flush()
186 else:
186 else:
187 c.close()
187 c.close()
188 else:
188 else:
189 if interact:
189 if interact:
190 e="Further interaction is not possible: child process is dead."
190 e="Further interaction is not possible: child process is dead."
191 print >> sys.stderr, e
191 print >> sys.stderr, e
192
192
193 def main(self,argv=None):
193 def main(self,argv=None):
194 """Run as a command-line script."""
194 """Run as a command-line script."""
195
195
196 parser = optparse.OptionParser(usage=USAGE % self.__class__.__name__)
196 parser = optparse.OptionParser(usage=USAGE % self.__class__.__name__)
197 newopt = parser.add_option
197 newopt = parser.add_option
198 newopt('-i','--interact',action='store_true',default=False,
198 newopt('-i','--interact',action='store_true',default=False,
199 help='Interact with the program after the script is run.')
199 help='Interact with the program after the script is run.')
200
200
201 opts,args = parser.parse_args(argv)
201 opts,args = parser.parse_args(argv)
202
202
203 if len(args) != 1:
203 if len(args) != 1:
204 print >> sys.stderr,"You must supply exactly one file to run."
204 print >> sys.stderr,"You must supply exactly one file to run."
205 sys.exit(1)
205 sys.exit(1)
206
206
207 self.run_file(args[0],opts.interact)
207 self.run_file(args[0],opts.interact)
208
208
209
209
210 # Specific runners for particular programs
210 # Specific runners for particular programs
211 class IPythonRunner(InteractiveRunner):
211 class IPythonRunner(InteractiveRunner):
212 """Interactive IPython runner.
212 """Interactive IPython runner.
213
213
214 This initalizes IPython in 'nocolor' mode for simplicity. This lets us
214 This initalizes IPython in 'nocolor' mode for simplicity. This lets us
215 avoid having to write a regexp that matches ANSI sequences, though pexpect
215 avoid having to write a regexp that matches ANSI sequences, though pexpect
216 does support them. If anyone contributes patches for ANSI color support,
216 does support them. If anyone contributes patches for ANSI color support,
217 they will be welcome.
217 they will be welcome.
218
218
219 It also sets the prompts manually, since the prompt regexps for
219 It also sets the prompts manually, since the prompt regexps for
220 pexpect need to be matched to the actual prompts, so user-customized
220 pexpect need to be matched to the actual prompts, so user-customized
221 prompts would break this.
221 prompts would break this.
222 """
222 """
223
223
224 def __init__(self,program = 'ipython',args=None):
224 def __init__(self,program = 'ipython',args=None):
225 """New runner, optionally passing the ipython command to use."""
225 """New runner, optionally passing the ipython command to use."""
226
226
227 args0 = ['-colors','NoColor',
227 args0 = ['-colors','NoColor',
228 '-pi1','In [\\#]: ',
228 '-pi1','In [\\#]: ',
229 '-pi2',' .\\D.: ']
229 '-pi2',' .\\D.: ']
230 if args is None: args = args0
230 if args is None: args = args0
231 else: args = args0 + args
231 else: args = args0 + args
232 prompts = [r'In \[\d+\]: ',r' \.*: ']
232 prompts = [r'In \[\d+\]: ',r' \.*: ']
233 InteractiveRunner.__init__(self,program,prompts,args)
233 InteractiveRunner.__init__(self,program,prompts,args)
234
234
235
235
236 class PythonRunner(InteractiveRunner):
236 class PythonRunner(InteractiveRunner):
237 """Interactive Python runner."""
237 """Interactive Python runner."""
238
238
239 def __init__(self,program='python',args=None):
239 def __init__(self,program='python',args=None):
240 """New runner, optionally passing the python command to use."""
240 """New runner, optionally passing the python command to use."""
241
241
242 prompts = [r'>>> ',r'\.\.\. ']
242 prompts = [r'>>> ',r'\.\.\. ']
243 InteractiveRunner.__init__(self,program,prompts,args)
243 InteractiveRunner.__init__(self,program,prompts,args)
244
244
245
245
246 class SAGERunner(InteractiveRunner):
246 class SAGERunner(InteractiveRunner):
247 """Interactive SAGE runner.
247 """Interactive SAGE runner.
248
248
249 WARNING: this runner only works if you manually configure your SAGE copy
249 WARNING: this runner only works if you manually configure your SAGE copy
250 to use 'colors NoColor' in the ipythonrc config file, since currently the
250 to use 'colors NoColor' in the ipythonrc config file, since currently the
251 prompt matching regexp does not identify color sequences."""
251 prompt matching regexp does not identify color sequences."""
252
252
253 def __init__(self,program='sage',args=None):
253 def __init__(self,program='sage',args=None):
254 """New runner, optionally passing the sage command to use."""
254 """New runner, optionally passing the sage command to use."""
255
255
256 prompts = ['sage: ',r'\s*\.\.\. ']
256 prompts = ['sage: ',r'\s*\.\.\. ']
257 InteractiveRunner.__init__(self,program,prompts,args)
257 InteractiveRunner.__init__(self,program,prompts,args)
258
258
259 # Global usage string, to avoid indentation issues if typed in a function def.
259 # Global usage string, to avoid indentation issues if typed in a function def.
260 MAIN_USAGE = """
260 MAIN_USAGE = """
261 %prog [options] file_to_run
261 %prog [options] file_to_run
262
262
263 This is an interface to the various interactive runners available in this
263 This is an interface to the various interactive runners available in this
264 module. If you want to pass specific options to one of the runners, you need
264 module. If you want to pass specific options to one of the runners, you need
265 to first terminate the main options with a '--', and then provide the runner's
265 to first terminate the main options with a '--', and then provide the runner's
266 options. For example:
266 options. For example:
267
267
268 irunner.py --python -- --help
268 irunner.py --python -- --help
269
269
270 will pass --help to the python runner. Similarly,
270 will pass --help to the python runner. Similarly,
271
271
272 irunner.py --ipython -- --interact script.ipy
272 irunner.py --ipython -- --interact script.ipy
273
273
274 will run the script.ipy file under the IPython runner, and then will start to
274 will run the script.ipy file under the IPython runner, and then will start to
275 interact with IPython at the end of the script (instead of exiting).
275 interact with IPython at the end of the script (instead of exiting).
276
276
277 The already implemented runners are listed below; adding one for a new program
277 The already implemented runners are listed below; adding one for a new program
278 is a trivial task, see the source for examples.
278 is a trivial task, see the source for examples.
279
279
280 WARNING: the SAGE runner only works if you manually configure your SAGE copy
280 WARNING: the SAGE runner only works if you manually configure your SAGE copy
281 to use 'colors NoColor' in the ipythonrc config file, since currently the
281 to use 'colors NoColor' in the ipythonrc config file, since currently the
282 prompt matching regexp does not identify color sequences.
282 prompt matching regexp does not identify color sequences.
283 """
283 """
284
284
285 def main():
285 def main():
286 """Run as a command-line script."""
286 """Run as a command-line script."""
287
287
288 parser = optparse.OptionParser(usage=MAIN_USAGE)
288 parser = optparse.OptionParser(usage=MAIN_USAGE)
289 newopt = parser.add_option
289 newopt = parser.add_option
290 parser.set_defaults(mode='ipython')
290 parser.set_defaults(mode='ipython')
291 newopt('--ipython',action='store_const',dest='mode',const='ipython',
291 newopt('--ipython',action='store_const',dest='mode',const='ipython',
292 help='IPython interactive runner (default).')
292 help='IPython interactive runner (default).')
293 newopt('--python',action='store_const',dest='mode',const='python',
293 newopt('--python',action='store_const',dest='mode',const='python',
294 help='Python interactive runner.')
294 help='Python interactive runner.')
295 newopt('--sage',action='store_const',dest='mode',const='sage',
295 newopt('--sage',action='store_const',dest='mode',const='sage',
296 help='SAGE interactive runner.')
296 help='SAGE interactive runner.')
297
297
298 opts,args = parser.parse_args()
298 opts,args = parser.parse_args()
299 runners = dict(ipython=IPythonRunner,
299 runners = dict(ipython=IPythonRunner,
300 python=PythonRunner,
300 python=PythonRunner,
301 sage=SAGERunner)
301 sage=SAGERunner)
302
302
303 try:
303 try:
304 ext = os.path.splitext(args[0])
304 ext = os.path.splitext(args[0])[-1]
305 except IndexError:
305 except IndexError:
306 ext = ''
306 ext = ''
307 modes = {'.ipy':'ipython',
307 modes = {'.ipy':'ipython',
308 '.py':'python',
308 '.py':'python',
309 '.sage':'sage'}
309 '.sage':'sage'}
310 mode = modes.get(ext,opts.mode)
310 mode = modes.get(ext,opts.mode)
311 runners[mode]().main(args)
311 runners[mode]().main(args)
312
312
313 if __name__ == '__main__':
313 if __name__ == '__main__':
314 main()
314 main()
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now