##// END OF EJS Templates
gh-7053: check for OSError during rehashx()
Jeroen Demeyer -
Show More
@@ -1,778 +1,785 b''
1 """Implementation of magic functions for interaction with the OS.
1 """Implementation of magic functions for interaction with the OS.
2
2
3 Note: this module is named 'osm' instead of 'os' to avoid a collision with the
3 Note: this module is named 'osm' instead of 'os' to avoid a collision with the
4 builtin.
4 builtin.
5 """
5 """
6 from __future__ import print_function
6 from __future__ import print_function
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Copyright (c) 2012 The IPython Development Team.
8 # Copyright (c) 2012 The IPython Development Team.
9 #
9 #
10 # Distributed under the terms of the Modified BSD License.
10 # Distributed under the terms of the Modified BSD License.
11 #
11 #
12 # The full license is in the file COPYING.txt, distributed with this software.
12 # The full license is in the file COPYING.txt, distributed with this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 # Stdlib
19 # Stdlib
20 import io
20 import io
21 import os
21 import os
22 import re
22 import re
23 import sys
23 import sys
24 from pprint import pformat
24 from pprint import pformat
25
25
26 # Our own packages
26 # Our own packages
27 from IPython.core import magic_arguments
27 from IPython.core import magic_arguments
28 from IPython.core import oinspect
28 from IPython.core import oinspect
29 from IPython.core import page
29 from IPython.core import page
30 from IPython.core.alias import AliasError, Alias
30 from IPython.core.alias import AliasError, Alias
31 from IPython.core.error import UsageError
31 from IPython.core.error import UsageError
32 from IPython.core.magic import (
32 from IPython.core.magic import (
33 Magics, compress_dhist, magics_class, line_magic, cell_magic, line_cell_magic
33 Magics, compress_dhist, magics_class, line_magic, cell_magic, line_cell_magic
34 )
34 )
35 from IPython.testing.skipdoctest import skip_doctest
35 from IPython.testing.skipdoctest import skip_doctest
36 from IPython.utils.openpy import source_to_unicode
36 from IPython.utils.openpy import source_to_unicode
37 from IPython.utils.path import unquote_filename
37 from IPython.utils.path import unquote_filename
38 from IPython.utils.process import abbrev_cwd
38 from IPython.utils.process import abbrev_cwd
39 from IPython.utils import py3compat
39 from IPython.utils import py3compat
40 from IPython.utils.py3compat import unicode_type
40 from IPython.utils.py3compat import unicode_type
41 from IPython.utils.terminal import set_term_title
41 from IPython.utils.terminal import set_term_title
42
42
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44 # Magic implementation classes
44 # Magic implementation classes
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46 @magics_class
46 @magics_class
47 class OSMagics(Magics):
47 class OSMagics(Magics):
48 """Magics to interact with the underlying OS (shell-type functionality).
48 """Magics to interact with the underlying OS (shell-type functionality).
49 """
49 """
50
50
51 @skip_doctest
51 @skip_doctest
52 @line_magic
52 @line_magic
53 def alias(self, parameter_s=''):
53 def alias(self, parameter_s=''):
54 """Define an alias for a system command.
54 """Define an alias for a system command.
55
55
56 '%alias alias_name cmd' defines 'alias_name' as an alias for 'cmd'
56 '%alias alias_name cmd' defines 'alias_name' as an alias for 'cmd'
57
57
58 Then, typing 'alias_name params' will execute the system command 'cmd
58 Then, typing 'alias_name params' will execute the system command 'cmd
59 params' (from your underlying operating system).
59 params' (from your underlying operating system).
60
60
61 Aliases have lower precedence than magic functions and Python normal
61 Aliases have lower precedence than magic functions and Python normal
62 variables, so if 'foo' is both a Python variable and an alias, the
62 variables, so if 'foo' is both a Python variable and an alias, the
63 alias can not be executed until 'del foo' removes the Python variable.
63 alias can not be executed until 'del foo' removes the Python variable.
64
64
65 You can use the %l specifier in an alias definition to represent the
65 You can use the %l specifier in an alias definition to represent the
66 whole line when the alias is called. For example::
66 whole line when the alias is called. For example::
67
67
68 In [2]: alias bracket echo "Input in brackets: <%l>"
68 In [2]: alias bracket echo "Input in brackets: <%l>"
69 In [3]: bracket hello world
69 In [3]: bracket hello world
70 Input in brackets: <hello world>
70 Input in brackets: <hello world>
71
71
72 You can also define aliases with parameters using %s specifiers (one
72 You can also define aliases with parameters using %s specifiers (one
73 per parameter)::
73 per parameter)::
74
74
75 In [1]: alias parts echo first %s second %s
75 In [1]: alias parts echo first %s second %s
76 In [2]: %parts A B
76 In [2]: %parts A B
77 first A second B
77 first A second B
78 In [3]: %parts A
78 In [3]: %parts A
79 Incorrect number of arguments: 2 expected.
79 Incorrect number of arguments: 2 expected.
80 parts is an alias to: 'echo first %s second %s'
80 parts is an alias to: 'echo first %s second %s'
81
81
82 Note that %l and %s are mutually exclusive. You can only use one or
82 Note that %l and %s are mutually exclusive. You can only use one or
83 the other in your aliases.
83 the other in your aliases.
84
84
85 Aliases expand Python variables just like system calls using ! or !!
85 Aliases expand Python variables just like system calls using ! or !!
86 do: all expressions prefixed with '$' get expanded. For details of
86 do: all expressions prefixed with '$' get expanded. For details of
87 the semantic rules, see PEP-215:
87 the semantic rules, see PEP-215:
88 http://www.python.org/peps/pep-0215.html. This is the library used by
88 http://www.python.org/peps/pep-0215.html. This is the library used by
89 IPython for variable expansion. If you want to access a true shell
89 IPython for variable expansion. If you want to access a true shell
90 variable, an extra $ is necessary to prevent its expansion by
90 variable, an extra $ is necessary to prevent its expansion by
91 IPython::
91 IPython::
92
92
93 In [6]: alias show echo
93 In [6]: alias show echo
94 In [7]: PATH='A Python string'
94 In [7]: PATH='A Python string'
95 In [8]: show $PATH
95 In [8]: show $PATH
96 A Python string
96 A Python string
97 In [9]: show $$PATH
97 In [9]: show $$PATH
98 /usr/local/lf9560/bin:/usr/local/intel/compiler70/ia32/bin:...
98 /usr/local/lf9560/bin:/usr/local/intel/compiler70/ia32/bin:...
99
99
100 You can use the alias facility to acess all of $PATH. See the %rehash
100 You can use the alias facility to acess all of $PATH. See the %rehash
101 and %rehashx functions, which automatically create aliases for the
101 and %rehashx functions, which automatically create aliases for the
102 contents of your $PATH.
102 contents of your $PATH.
103
103
104 If called with no parameters, %alias prints the current alias table."""
104 If called with no parameters, %alias prints the current alias table."""
105
105
106 par = parameter_s.strip()
106 par = parameter_s.strip()
107 if not par:
107 if not par:
108 aliases = sorted(self.shell.alias_manager.aliases)
108 aliases = sorted(self.shell.alias_manager.aliases)
109 # stored = self.shell.db.get('stored_aliases', {} )
109 # stored = self.shell.db.get('stored_aliases', {} )
110 # for k, v in stored:
110 # for k, v in stored:
111 # atab.append(k, v[0])
111 # atab.append(k, v[0])
112
112
113 print("Total number of aliases:", len(aliases))
113 print("Total number of aliases:", len(aliases))
114 sys.stdout.flush()
114 sys.stdout.flush()
115 return aliases
115 return aliases
116
116
117 # Now try to define a new one
117 # Now try to define a new one
118 try:
118 try:
119 alias,cmd = par.split(None, 1)
119 alias,cmd = par.split(None, 1)
120 except TypeError:
120 except TypeError:
121 print(oinspect.getdoc(self.alias))
121 print(oinspect.getdoc(self.alias))
122 return
122 return
123
123
124 try:
124 try:
125 self.shell.alias_manager.define_alias(alias, cmd)
125 self.shell.alias_manager.define_alias(alias, cmd)
126 except AliasError as e:
126 except AliasError as e:
127 print(e)
127 print(e)
128 # end magic_alias
128 # end magic_alias
129
129
130 @line_magic
130 @line_magic
131 def unalias(self, parameter_s=''):
131 def unalias(self, parameter_s=''):
132 """Remove an alias"""
132 """Remove an alias"""
133
133
134 aname = parameter_s.strip()
134 aname = parameter_s.strip()
135 try:
135 try:
136 self.shell.alias_manager.undefine_alias(aname)
136 self.shell.alias_manager.undefine_alias(aname)
137 except ValueError as e:
137 except ValueError as e:
138 print(e)
138 print(e)
139 return
139 return
140
140
141 stored = self.shell.db.get('stored_aliases', {} )
141 stored = self.shell.db.get('stored_aliases', {} )
142 if aname in stored:
142 if aname in stored:
143 print("Removing %stored alias",aname)
143 print("Removing %stored alias",aname)
144 del stored[aname]
144 del stored[aname]
145 self.shell.db['stored_aliases'] = stored
145 self.shell.db['stored_aliases'] = stored
146
146
147 @line_magic
147 @line_magic
148 def rehashx(self, parameter_s=''):
148 def rehashx(self, parameter_s=''):
149 """Update the alias table with all executable files in $PATH.
149 """Update the alias table with all executable files in $PATH.
150
150
151 This version explicitly checks that every entry in $PATH is a file
151 This version explicitly checks that every entry in $PATH is a file
152 with execute access (os.X_OK), so it is much slower than %rehash.
152 with execute access (os.X_OK), so it is much slower than %rehash.
153
153
154 Under Windows, it checks executability as a match against a
154 Under Windows, it checks executability as a match against a
155 '|'-separated string of extensions, stored in the IPython config
155 '|'-separated string of extensions, stored in the IPython config
156 variable win_exec_ext. This defaults to 'exe|com|bat'.
156 variable win_exec_ext. This defaults to 'exe|com|bat'.
157
157
158 This function also resets the root module cache of module completer,
158 This function also resets the root module cache of module completer,
159 used on slow filesystems.
159 used on slow filesystems.
160 """
160 """
161 from IPython.core.alias import InvalidAliasError
161 from IPython.core.alias import InvalidAliasError
162
162
163 # for the benefit of module completer in ipy_completers.py
163 # for the benefit of module completer in ipy_completers.py
164 del self.shell.db['rootmodules_cache']
164 del self.shell.db['rootmodules_cache']
165
165
166 path = [os.path.abspath(os.path.expanduser(p)) for p in
166 path = [os.path.abspath(os.path.expanduser(p)) for p in
167 os.environ.get('PATH','').split(os.pathsep)]
167 os.environ.get('PATH','').split(os.pathsep)]
168 path = filter(os.path.isdir,path)
169
168
170 syscmdlist = []
169 syscmdlist = []
171 # Now define isexec in a cross platform manner.
170 # Now define isexec in a cross platform manner.
172 if os.name == 'posix':
171 if os.name == 'posix':
173 isexec = lambda fname:os.path.isfile(fname) and \
172 isexec = lambda fname:os.path.isfile(fname) and \
174 os.access(fname,os.X_OK)
173 os.access(fname,os.X_OK)
175 else:
174 else:
176 try:
175 try:
177 winext = os.environ['pathext'].replace(';','|').replace('.','')
176 winext = os.environ['pathext'].replace(';','|').replace('.','')
178 except KeyError:
177 except KeyError:
179 winext = 'exe|com|bat|py'
178 winext = 'exe|com|bat|py'
180 if 'py' not in winext:
179 if 'py' not in winext:
181 winext += '|py'
180 winext += '|py'
182 execre = re.compile(r'(.*)\.(%s)$' % winext,re.IGNORECASE)
181 execre = re.compile(r'(.*)\.(%s)$' % winext,re.IGNORECASE)
183 isexec = lambda fname:os.path.isfile(fname) and execre.match(fname)
182 isexec = lambda fname:os.path.isfile(fname) and execre.match(fname)
184 savedir = py3compat.getcwd()
183 savedir = py3compat.getcwd()
185
184
186 # Now walk the paths looking for executables to alias.
185 # Now walk the paths looking for executables to alias.
187 try:
186 try:
188 # write the whole loop for posix/Windows so we don't have an if in
187 # write the whole loop for posix/Windows so we don't have an if in
189 # the innermost part
188 # the innermost part
190 if os.name == 'posix':
189 if os.name == 'posix':
191 for pdir in path:
190 for pdir in path:
191 try:
192 os.chdir(pdir)
192 os.chdir(pdir)
193 for ff in os.listdir(pdir):
193 dirlist = os.listdir(pdir)
194 except OSError:
195 continue
196 for ff in dirlist:
194 if isexec(ff):
197 if isexec(ff):
195 try:
198 try:
196 # Removes dots from the name since ipython
199 # Removes dots from the name since ipython
197 # will assume names with dots to be python.
200 # will assume names with dots to be python.
198 if not self.shell.alias_manager.is_alias(ff):
201 if not self.shell.alias_manager.is_alias(ff):
199 self.shell.alias_manager.define_alias(
202 self.shell.alias_manager.define_alias(
200 ff.replace('.',''), ff)
203 ff.replace('.',''), ff)
201 except InvalidAliasError:
204 except InvalidAliasError:
202 pass
205 pass
203 else:
206 else:
204 syscmdlist.append(ff)
207 syscmdlist.append(ff)
205 else:
208 else:
206 no_alias = Alias.blacklist
209 no_alias = Alias.blacklist
207 for pdir in path:
210 for pdir in path:
211 try:
208 os.chdir(pdir)
212 os.chdir(pdir)
209 for ff in os.listdir(pdir):
213 dirlist = os.listdir(pdir)
214 except OSError:
215 continue
216 for ff in dirlist:
210 base, ext = os.path.splitext(ff)
217 base, ext = os.path.splitext(ff)
211 if isexec(ff) and base.lower() not in no_alias:
218 if isexec(ff) and base.lower() not in no_alias:
212 if ext.lower() == '.exe':
219 if ext.lower() == '.exe':
213 ff = base
220 ff = base
214 try:
221 try:
215 # Removes dots from the name since ipython
222 # Removes dots from the name since ipython
216 # will assume names with dots to be python.
223 # will assume names with dots to be python.
217 self.shell.alias_manager.define_alias(
224 self.shell.alias_manager.define_alias(
218 base.lower().replace('.',''), ff)
225 base.lower().replace('.',''), ff)
219 except InvalidAliasError:
226 except InvalidAliasError:
220 pass
227 pass
221 syscmdlist.append(ff)
228 syscmdlist.append(ff)
222 self.shell.db['syscmdlist'] = syscmdlist
229 self.shell.db['syscmdlist'] = syscmdlist
223 finally:
230 finally:
224 os.chdir(savedir)
231 os.chdir(savedir)
225
232
226 @skip_doctest
233 @skip_doctest
227 @line_magic
234 @line_magic
228 def pwd(self, parameter_s=''):
235 def pwd(self, parameter_s=''):
229 """Return the current working directory path.
236 """Return the current working directory path.
230
237
231 Examples
238 Examples
232 --------
239 --------
233 ::
240 ::
234
241
235 In [9]: pwd
242 In [9]: pwd
236 Out[9]: '/home/tsuser/sprint/ipython'
243 Out[9]: '/home/tsuser/sprint/ipython'
237 """
244 """
238 return py3compat.getcwd()
245 return py3compat.getcwd()
239
246
240 @skip_doctest
247 @skip_doctest
241 @line_magic
248 @line_magic
242 def cd(self, parameter_s=''):
249 def cd(self, parameter_s=''):
243 """Change the current working directory.
250 """Change the current working directory.
244
251
245 This command automatically maintains an internal list of directories
252 This command automatically maintains an internal list of directories
246 you visit during your IPython session, in the variable _dh. The
253 you visit during your IPython session, in the variable _dh. The
247 command %dhist shows this history nicely formatted. You can also
254 command %dhist shows this history nicely formatted. You can also
248 do 'cd -<tab>' to see directory history conveniently.
255 do 'cd -<tab>' to see directory history conveniently.
249
256
250 Usage:
257 Usage:
251
258
252 cd 'dir': changes to directory 'dir'.
259 cd 'dir': changes to directory 'dir'.
253
260
254 cd -: changes to the last visited directory.
261 cd -: changes to the last visited directory.
255
262
256 cd -<n>: changes to the n-th directory in the directory history.
263 cd -<n>: changes to the n-th directory in the directory history.
257
264
258 cd --foo: change to directory that matches 'foo' in history
265 cd --foo: change to directory that matches 'foo' in history
259
266
260 cd -b <bookmark_name>: jump to a bookmark set by %bookmark
267 cd -b <bookmark_name>: jump to a bookmark set by %bookmark
261 (note: cd <bookmark_name> is enough if there is no
268 (note: cd <bookmark_name> is enough if there is no
262 directory <bookmark_name>, but a bookmark with the name exists.)
269 directory <bookmark_name>, but a bookmark with the name exists.)
263 'cd -b <tab>' allows you to tab-complete bookmark names.
270 'cd -b <tab>' allows you to tab-complete bookmark names.
264
271
265 Options:
272 Options:
266
273
267 -q: quiet. Do not print the working directory after the cd command is
274 -q: quiet. Do not print the working directory after the cd command is
268 executed. By default IPython's cd command does print this directory,
275 executed. By default IPython's cd command does print this directory,
269 since the default prompts do not display path information.
276 since the default prompts do not display path information.
270
277
271 Note that !cd doesn't work for this purpose because the shell where
278 Note that !cd doesn't work for this purpose because the shell where
272 !command runs is immediately discarded after executing 'command'.
279 !command runs is immediately discarded after executing 'command'.
273
280
274 Examples
281 Examples
275 --------
282 --------
276 ::
283 ::
277
284
278 In [10]: cd parent/child
285 In [10]: cd parent/child
279 /home/tsuser/parent/child
286 /home/tsuser/parent/child
280 """
287 """
281
288
282 oldcwd = py3compat.getcwd()
289 oldcwd = py3compat.getcwd()
283 numcd = re.match(r'(-)(\d+)$',parameter_s)
290 numcd = re.match(r'(-)(\d+)$',parameter_s)
284 # jump in directory history by number
291 # jump in directory history by number
285 if numcd:
292 if numcd:
286 nn = int(numcd.group(2))
293 nn = int(numcd.group(2))
287 try:
294 try:
288 ps = self.shell.user_ns['_dh'][nn]
295 ps = self.shell.user_ns['_dh'][nn]
289 except IndexError:
296 except IndexError:
290 print('The requested directory does not exist in history.')
297 print('The requested directory does not exist in history.')
291 return
298 return
292 else:
299 else:
293 opts = {}
300 opts = {}
294 elif parameter_s.startswith('--'):
301 elif parameter_s.startswith('--'):
295 ps = None
302 ps = None
296 fallback = None
303 fallback = None
297 pat = parameter_s[2:]
304 pat = parameter_s[2:]
298 dh = self.shell.user_ns['_dh']
305 dh = self.shell.user_ns['_dh']
299 # first search only by basename (last component)
306 # first search only by basename (last component)
300 for ent in reversed(dh):
307 for ent in reversed(dh):
301 if pat in os.path.basename(ent) and os.path.isdir(ent):
308 if pat in os.path.basename(ent) and os.path.isdir(ent):
302 ps = ent
309 ps = ent
303 break
310 break
304
311
305 if fallback is None and pat in ent and os.path.isdir(ent):
312 if fallback is None and pat in ent and os.path.isdir(ent):
306 fallback = ent
313 fallback = ent
307
314
308 # if we have no last part match, pick the first full path match
315 # if we have no last part match, pick the first full path match
309 if ps is None:
316 if ps is None:
310 ps = fallback
317 ps = fallback
311
318
312 if ps is None:
319 if ps is None:
313 print("No matching entry in directory history")
320 print("No matching entry in directory history")
314 return
321 return
315 else:
322 else:
316 opts = {}
323 opts = {}
317
324
318
325
319 else:
326 else:
320 #turn all non-space-escaping backslashes to slashes,
327 #turn all non-space-escaping backslashes to slashes,
321 # for c:\windows\directory\names\
328 # for c:\windows\directory\names\
322 parameter_s = re.sub(r'\\(?! )','/', parameter_s)
329 parameter_s = re.sub(r'\\(?! )','/', parameter_s)
323 opts,ps = self.parse_options(parameter_s,'qb',mode='string')
330 opts,ps = self.parse_options(parameter_s,'qb',mode='string')
324 # jump to previous
331 # jump to previous
325 if ps == '-':
332 if ps == '-':
326 try:
333 try:
327 ps = self.shell.user_ns['_dh'][-2]
334 ps = self.shell.user_ns['_dh'][-2]
328 except IndexError:
335 except IndexError:
329 raise UsageError('%cd -: No previous directory to change to.')
336 raise UsageError('%cd -: No previous directory to change to.')
330 # jump to bookmark if needed
337 # jump to bookmark if needed
331 else:
338 else:
332 if not os.path.isdir(ps) or 'b' in opts:
339 if not os.path.isdir(ps) or 'b' in opts:
333 bkms = self.shell.db.get('bookmarks', {})
340 bkms = self.shell.db.get('bookmarks', {})
334
341
335 if ps in bkms:
342 if ps in bkms:
336 target = bkms[ps]
343 target = bkms[ps]
337 print('(bookmark:%s) -> %s' % (ps, target))
344 print('(bookmark:%s) -> %s' % (ps, target))
338 ps = target
345 ps = target
339 else:
346 else:
340 if 'b' in opts:
347 if 'b' in opts:
341 raise UsageError("Bookmark '%s' not found. "
348 raise UsageError("Bookmark '%s' not found. "
342 "Use '%%bookmark -l' to see your bookmarks." % ps)
349 "Use '%%bookmark -l' to see your bookmarks." % ps)
343
350
344 # strip extra quotes on Windows, because os.chdir doesn't like them
351 # strip extra quotes on Windows, because os.chdir doesn't like them
345 ps = unquote_filename(ps)
352 ps = unquote_filename(ps)
346 # at this point ps should point to the target dir
353 # at this point ps should point to the target dir
347 if ps:
354 if ps:
348 try:
355 try:
349 os.chdir(os.path.expanduser(ps))
356 os.chdir(os.path.expanduser(ps))
350 if hasattr(self.shell, 'term_title') and self.shell.term_title:
357 if hasattr(self.shell, 'term_title') and self.shell.term_title:
351 set_term_title('IPython: ' + abbrev_cwd())
358 set_term_title('IPython: ' + abbrev_cwd())
352 except OSError:
359 except OSError:
353 print(sys.exc_info()[1])
360 print(sys.exc_info()[1])
354 else:
361 else:
355 cwd = py3compat.getcwd()
362 cwd = py3compat.getcwd()
356 dhist = self.shell.user_ns['_dh']
363 dhist = self.shell.user_ns['_dh']
357 if oldcwd != cwd:
364 if oldcwd != cwd:
358 dhist.append(cwd)
365 dhist.append(cwd)
359 self.shell.db['dhist'] = compress_dhist(dhist)[-100:]
366 self.shell.db['dhist'] = compress_dhist(dhist)[-100:]
360
367
361 else:
368 else:
362 os.chdir(self.shell.home_dir)
369 os.chdir(self.shell.home_dir)
363 if hasattr(self.shell, 'term_title') and self.shell.term_title:
370 if hasattr(self.shell, 'term_title') and self.shell.term_title:
364 set_term_title('IPython: ' + '~')
371 set_term_title('IPython: ' + '~')
365 cwd = py3compat.getcwd()
372 cwd = py3compat.getcwd()
366 dhist = self.shell.user_ns['_dh']
373 dhist = self.shell.user_ns['_dh']
367
374
368 if oldcwd != cwd:
375 if oldcwd != cwd:
369 dhist.append(cwd)
376 dhist.append(cwd)
370 self.shell.db['dhist'] = compress_dhist(dhist)[-100:]
377 self.shell.db['dhist'] = compress_dhist(dhist)[-100:]
371 if not 'q' in opts and self.shell.user_ns['_dh']:
378 if not 'q' in opts and self.shell.user_ns['_dh']:
372 print(self.shell.user_ns['_dh'][-1])
379 print(self.shell.user_ns['_dh'][-1])
373
380
374 @line_magic
381 @line_magic
375 def env(self, parameter_s=''):
382 def env(self, parameter_s=''):
376 """List environment variables."""
383 """List environment variables."""
377 if parameter_s.strip():
384 if parameter_s.strip():
378 split = '=' if '=' in parameter_s else ' '
385 split = '=' if '=' in parameter_s else ' '
379 bits = parameter_s.split(split)
386 bits = parameter_s.split(split)
380 if len(bits) == 1:
387 if len(bits) == 1:
381 key = parameter_s.strip()
388 key = parameter_s.strip()
382 if key in os.environ:
389 if key in os.environ:
383 return os.environ[key]
390 return os.environ[key]
384 else:
391 else:
385 err = "Environment does not have key: {0}".format(key)
392 err = "Environment does not have key: {0}".format(key)
386 raise UsageError(err)
393 raise UsageError(err)
387 if len(bits) > 1:
394 if len(bits) > 1:
388 return self.set_env(parameter_s)
395 return self.set_env(parameter_s)
389 return dict(os.environ)
396 return dict(os.environ)
390
397
391 @line_magic
398 @line_magic
392 def set_env(self, parameter_s):
399 def set_env(self, parameter_s):
393 """Set environment variables. Assumptions are that either "val" is a
400 """Set environment variables. Assumptions are that either "val" is a
394 name in the user namespace, or val is something that evaluates to a
401 name in the user namespace, or val is something that evaluates to a
395 string.
402 string.
396
403
397 Usage:\\
404 Usage:\\
398 %set_env var val
405 %set_env var val
399 """
406 """
400 split = '=' if '=' in parameter_s else ' '
407 split = '=' if '=' in parameter_s else ' '
401 bits = parameter_s.split(split, 1)
408 bits = parameter_s.split(split, 1)
402 if not parameter_s.strip() or len(bits)<2:
409 if not parameter_s.strip() or len(bits)<2:
403 raise UsageError("usage is 'set_env var=val'")
410 raise UsageError("usage is 'set_env var=val'")
404 var = bits[0].strip()
411 var = bits[0].strip()
405 val = bits[1].strip()
412 val = bits[1].strip()
406 if re.match(r'.*\s.*', var):
413 if re.match(r'.*\s.*', var):
407 # an environment variable with whitespace is almost certainly
414 # an environment variable with whitespace is almost certainly
408 # not what the user intended. what's more likely is the wrong
415 # not what the user intended. what's more likely is the wrong
409 # split was chosen, ie for "set_env cmd_args A=B", we chose
416 # split was chosen, ie for "set_env cmd_args A=B", we chose
410 # '=' for the split and should have chosen ' '. to get around
417 # '=' for the split and should have chosen ' '. to get around
411 # this, users should just assign directly to os.environ or use
418 # this, users should just assign directly to os.environ or use
412 # standard magic {var} expansion.
419 # standard magic {var} expansion.
413 err = "refusing to set env var with whitespace: '{0}'"
420 err = "refusing to set env var with whitespace: '{0}'"
414 err = err.format(val)
421 err = err.format(val)
415 raise UsageError(err)
422 raise UsageError(err)
416 os.environ[py3compat.cast_bytes_py2(var)] = py3compat.cast_bytes_py2(val)
423 os.environ[py3compat.cast_bytes_py2(var)] = py3compat.cast_bytes_py2(val)
417 print('env: {0}={1}'.format(var,val))
424 print('env: {0}={1}'.format(var,val))
418
425
419 @line_magic
426 @line_magic
420 def pushd(self, parameter_s=''):
427 def pushd(self, parameter_s=''):
421 """Place the current dir on stack and change directory.
428 """Place the current dir on stack and change directory.
422
429
423 Usage:\\
430 Usage:\\
424 %pushd ['dirname']
431 %pushd ['dirname']
425 """
432 """
426
433
427 dir_s = self.shell.dir_stack
434 dir_s = self.shell.dir_stack
428 tgt = os.path.expanduser(unquote_filename(parameter_s))
435 tgt = os.path.expanduser(unquote_filename(parameter_s))
429 cwd = py3compat.getcwd().replace(self.shell.home_dir,'~')
436 cwd = py3compat.getcwd().replace(self.shell.home_dir,'~')
430 if tgt:
437 if tgt:
431 self.cd(parameter_s)
438 self.cd(parameter_s)
432 dir_s.insert(0,cwd)
439 dir_s.insert(0,cwd)
433 return self.shell.magic('dirs')
440 return self.shell.magic('dirs')
434
441
435 @line_magic
442 @line_magic
436 def popd(self, parameter_s=''):
443 def popd(self, parameter_s=''):
437 """Change to directory popped off the top of the stack.
444 """Change to directory popped off the top of the stack.
438 """
445 """
439 if not self.shell.dir_stack:
446 if not self.shell.dir_stack:
440 raise UsageError("%popd on empty stack")
447 raise UsageError("%popd on empty stack")
441 top = self.shell.dir_stack.pop(0)
448 top = self.shell.dir_stack.pop(0)
442 self.cd(top)
449 self.cd(top)
443 print("popd ->",top)
450 print("popd ->",top)
444
451
445 @line_magic
452 @line_magic
446 def dirs(self, parameter_s=''):
453 def dirs(self, parameter_s=''):
447 """Return the current directory stack."""
454 """Return the current directory stack."""
448
455
449 return self.shell.dir_stack
456 return self.shell.dir_stack
450
457
451 @line_magic
458 @line_magic
452 def dhist(self, parameter_s=''):
459 def dhist(self, parameter_s=''):
453 """Print your history of visited directories.
460 """Print your history of visited directories.
454
461
455 %dhist -> print full history\\
462 %dhist -> print full history\\
456 %dhist n -> print last n entries only\\
463 %dhist n -> print last n entries only\\
457 %dhist n1 n2 -> print entries between n1 and n2 (n2 not included)\\
464 %dhist n1 n2 -> print entries between n1 and n2 (n2 not included)\\
458
465
459 This history is automatically maintained by the %cd command, and
466 This history is automatically maintained by the %cd command, and
460 always available as the global list variable _dh. You can use %cd -<n>
467 always available as the global list variable _dh. You can use %cd -<n>
461 to go to directory number <n>.
468 to go to directory number <n>.
462
469
463 Note that most of time, you should view directory history by entering
470 Note that most of time, you should view directory history by entering
464 cd -<TAB>.
471 cd -<TAB>.
465
472
466 """
473 """
467
474
468 dh = self.shell.user_ns['_dh']
475 dh = self.shell.user_ns['_dh']
469 if parameter_s:
476 if parameter_s:
470 try:
477 try:
471 args = map(int,parameter_s.split())
478 args = map(int,parameter_s.split())
472 except:
479 except:
473 self.arg_err(self.dhist)
480 self.arg_err(self.dhist)
474 return
481 return
475 if len(args) == 1:
482 if len(args) == 1:
476 ini,fin = max(len(dh)-(args[0]),0),len(dh)
483 ini,fin = max(len(dh)-(args[0]),0),len(dh)
477 elif len(args) == 2:
484 elif len(args) == 2:
478 ini,fin = args
485 ini,fin = args
479 fin = min(fin, len(dh))
486 fin = min(fin, len(dh))
480 else:
487 else:
481 self.arg_err(self.dhist)
488 self.arg_err(self.dhist)
482 return
489 return
483 else:
490 else:
484 ini,fin = 0,len(dh)
491 ini,fin = 0,len(dh)
485 print('Directory history (kept in _dh)')
492 print('Directory history (kept in _dh)')
486 for i in range(ini, fin):
493 for i in range(ini, fin):
487 print("%d: %s" % (i, dh[i]))
494 print("%d: %s" % (i, dh[i]))
488
495
489 @skip_doctest
496 @skip_doctest
490 @line_magic
497 @line_magic
491 def sc(self, parameter_s=''):
498 def sc(self, parameter_s=''):
492 """Shell capture - run shell command and capture output (DEPRECATED use !).
499 """Shell capture - run shell command and capture output (DEPRECATED use !).
493
500
494 DEPRECATED. Suboptimal, retained for backwards compatibility.
501 DEPRECATED. Suboptimal, retained for backwards compatibility.
495
502
496 You should use the form 'var = !command' instead. Example:
503 You should use the form 'var = !command' instead. Example:
497
504
498 "%sc -l myfiles = ls ~" should now be written as
505 "%sc -l myfiles = ls ~" should now be written as
499
506
500 "myfiles = !ls ~"
507 "myfiles = !ls ~"
501
508
502 myfiles.s, myfiles.l and myfiles.n still apply as documented
509 myfiles.s, myfiles.l and myfiles.n still apply as documented
503 below.
510 below.
504
511
505 --
512 --
506 %sc [options] varname=command
513 %sc [options] varname=command
507
514
508 IPython will run the given command using commands.getoutput(), and
515 IPython will run the given command using commands.getoutput(), and
509 will then update the user's interactive namespace with a variable
516 will then update the user's interactive namespace with a variable
510 called varname, containing the value of the call. Your command can
517 called varname, containing the value of the call. Your command can
511 contain shell wildcards, pipes, etc.
518 contain shell wildcards, pipes, etc.
512
519
513 The '=' sign in the syntax is mandatory, and the variable name you
520 The '=' sign in the syntax is mandatory, and the variable name you
514 supply must follow Python's standard conventions for valid names.
521 supply must follow Python's standard conventions for valid names.
515
522
516 (A special format without variable name exists for internal use)
523 (A special format without variable name exists for internal use)
517
524
518 Options:
525 Options:
519
526
520 -l: list output. Split the output on newlines into a list before
527 -l: list output. Split the output on newlines into a list before
521 assigning it to the given variable. By default the output is stored
528 assigning it to the given variable. By default the output is stored
522 as a single string.
529 as a single string.
523
530
524 -v: verbose. Print the contents of the variable.
531 -v: verbose. Print the contents of the variable.
525
532
526 In most cases you should not need to split as a list, because the
533 In most cases you should not need to split as a list, because the
527 returned value is a special type of string which can automatically
534 returned value is a special type of string which can automatically
528 provide its contents either as a list (split on newlines) or as a
535 provide its contents either as a list (split on newlines) or as a
529 space-separated string. These are convenient, respectively, either
536 space-separated string. These are convenient, respectively, either
530 for sequential processing or to be passed to a shell command.
537 for sequential processing or to be passed to a shell command.
531
538
532 For example::
539 For example::
533
540
534 # Capture into variable a
541 # Capture into variable a
535 In [1]: sc a=ls *py
542 In [1]: sc a=ls *py
536
543
537 # a is a string with embedded newlines
544 # a is a string with embedded newlines
538 In [2]: a
545 In [2]: a
539 Out[2]: 'setup.py\\nwin32_manual_post_install.py'
546 Out[2]: 'setup.py\\nwin32_manual_post_install.py'
540
547
541 # which can be seen as a list:
548 # which can be seen as a list:
542 In [3]: a.l
549 In [3]: a.l
543 Out[3]: ['setup.py', 'win32_manual_post_install.py']
550 Out[3]: ['setup.py', 'win32_manual_post_install.py']
544
551
545 # or as a whitespace-separated string:
552 # or as a whitespace-separated string:
546 In [4]: a.s
553 In [4]: a.s
547 Out[4]: 'setup.py win32_manual_post_install.py'
554 Out[4]: 'setup.py win32_manual_post_install.py'
548
555
549 # a.s is useful to pass as a single command line:
556 # a.s is useful to pass as a single command line:
550 In [5]: !wc -l $a.s
557 In [5]: !wc -l $a.s
551 146 setup.py
558 146 setup.py
552 130 win32_manual_post_install.py
559 130 win32_manual_post_install.py
553 276 total
560 276 total
554
561
555 # while the list form is useful to loop over:
562 # while the list form is useful to loop over:
556 In [6]: for f in a.l:
563 In [6]: for f in a.l:
557 ...: !wc -l $f
564 ...: !wc -l $f
558 ...:
565 ...:
559 146 setup.py
566 146 setup.py
560 130 win32_manual_post_install.py
567 130 win32_manual_post_install.py
561
568
562 Similarly, the lists returned by the -l option are also special, in
569 Similarly, the lists returned by the -l option are also special, in
563 the sense that you can equally invoke the .s attribute on them to
570 the sense that you can equally invoke the .s attribute on them to
564 automatically get a whitespace-separated string from their contents::
571 automatically get a whitespace-separated string from their contents::
565
572
566 In [7]: sc -l b=ls *py
573 In [7]: sc -l b=ls *py
567
574
568 In [8]: b
575 In [8]: b
569 Out[8]: ['setup.py', 'win32_manual_post_install.py']
576 Out[8]: ['setup.py', 'win32_manual_post_install.py']
570
577
571 In [9]: b.s
578 In [9]: b.s
572 Out[9]: 'setup.py win32_manual_post_install.py'
579 Out[9]: 'setup.py win32_manual_post_install.py'
573
580
574 In summary, both the lists and strings used for output capture have
581 In summary, both the lists and strings used for output capture have
575 the following special attributes::
582 the following special attributes::
576
583
577 .l (or .list) : value as list.
584 .l (or .list) : value as list.
578 .n (or .nlstr): value as newline-separated string.
585 .n (or .nlstr): value as newline-separated string.
579 .s (or .spstr): value as space-separated string.
586 .s (or .spstr): value as space-separated string.
580 """
587 """
581
588
582 opts,args = self.parse_options(parameter_s, 'lv')
589 opts,args = self.parse_options(parameter_s, 'lv')
583 # Try to get a variable name and command to run
590 # Try to get a variable name and command to run
584 try:
591 try:
585 # the variable name must be obtained from the parse_options
592 # the variable name must be obtained from the parse_options
586 # output, which uses shlex.split to strip options out.
593 # output, which uses shlex.split to strip options out.
587 var,_ = args.split('=', 1)
594 var,_ = args.split('=', 1)
588 var = var.strip()
595 var = var.strip()
589 # But the command has to be extracted from the original input
596 # But the command has to be extracted from the original input
590 # parameter_s, not on what parse_options returns, to avoid the
597 # parameter_s, not on what parse_options returns, to avoid the
591 # quote stripping which shlex.split performs on it.
598 # quote stripping which shlex.split performs on it.
592 _,cmd = parameter_s.split('=', 1)
599 _,cmd = parameter_s.split('=', 1)
593 except ValueError:
600 except ValueError:
594 var,cmd = '',''
601 var,cmd = '',''
595 # If all looks ok, proceed
602 # If all looks ok, proceed
596 split = 'l' in opts
603 split = 'l' in opts
597 out = self.shell.getoutput(cmd, split=split)
604 out = self.shell.getoutput(cmd, split=split)
598 if 'v' in opts:
605 if 'v' in opts:
599 print('%s ==\n%s' % (var, pformat(out)))
606 print('%s ==\n%s' % (var, pformat(out)))
600 if var:
607 if var:
601 self.shell.user_ns.update({var:out})
608 self.shell.user_ns.update({var:out})
602 else:
609 else:
603 return out
610 return out
604
611
605 @line_cell_magic
612 @line_cell_magic
606 def sx(self, line='', cell=None):
613 def sx(self, line='', cell=None):
607 """Shell execute - run shell command and capture output (!! is short-hand).
614 """Shell execute - run shell command and capture output (!! is short-hand).
608
615
609 %sx command
616 %sx command
610
617
611 IPython will run the given command using commands.getoutput(), and
618 IPython will run the given command using commands.getoutput(), and
612 return the result formatted as a list (split on '\\n'). Since the
619 return the result formatted as a list (split on '\\n'). Since the
613 output is _returned_, it will be stored in ipython's regular output
620 output is _returned_, it will be stored in ipython's regular output
614 cache Out[N] and in the '_N' automatic variables.
621 cache Out[N] and in the '_N' automatic variables.
615
622
616 Notes:
623 Notes:
617
624
618 1) If an input line begins with '!!', then %sx is automatically
625 1) If an input line begins with '!!', then %sx is automatically
619 invoked. That is, while::
626 invoked. That is, while::
620
627
621 !ls
628 !ls
622
629
623 causes ipython to simply issue system('ls'), typing::
630 causes ipython to simply issue system('ls'), typing::
624
631
625 !!ls
632 !!ls
626
633
627 is a shorthand equivalent to::
634 is a shorthand equivalent to::
628
635
629 %sx ls
636 %sx ls
630
637
631 2) %sx differs from %sc in that %sx automatically splits into a list,
638 2) %sx differs from %sc in that %sx automatically splits into a list,
632 like '%sc -l'. The reason for this is to make it as easy as possible
639 like '%sc -l'. The reason for this is to make it as easy as possible
633 to process line-oriented shell output via further python commands.
640 to process line-oriented shell output via further python commands.
634 %sc is meant to provide much finer control, but requires more
641 %sc is meant to provide much finer control, but requires more
635 typing.
642 typing.
636
643
637 3) Just like %sc -l, this is a list with special attributes:
644 3) Just like %sc -l, this is a list with special attributes:
638 ::
645 ::
639
646
640 .l (or .list) : value as list.
647 .l (or .list) : value as list.
641 .n (or .nlstr): value as newline-separated string.
648 .n (or .nlstr): value as newline-separated string.
642 .s (or .spstr): value as whitespace-separated string.
649 .s (or .spstr): value as whitespace-separated string.
643
650
644 This is very useful when trying to use such lists as arguments to
651 This is very useful when trying to use such lists as arguments to
645 system commands."""
652 system commands."""
646
653
647 if cell is None:
654 if cell is None:
648 # line magic
655 # line magic
649 return self.shell.getoutput(line)
656 return self.shell.getoutput(line)
650 else:
657 else:
651 opts,args = self.parse_options(line, '', 'out=')
658 opts,args = self.parse_options(line, '', 'out=')
652 output = self.shell.getoutput(cell)
659 output = self.shell.getoutput(cell)
653 out_name = opts.get('out', opts.get('o'))
660 out_name = opts.get('out', opts.get('o'))
654 if out_name:
661 if out_name:
655 self.shell.user_ns[out_name] = output
662 self.shell.user_ns[out_name] = output
656 else:
663 else:
657 return output
664 return output
658
665
659 system = line_cell_magic('system')(sx)
666 system = line_cell_magic('system')(sx)
660 bang = cell_magic('!')(sx)
667 bang = cell_magic('!')(sx)
661
668
662 @line_magic
669 @line_magic
663 def bookmark(self, parameter_s=''):
670 def bookmark(self, parameter_s=''):
664 """Manage IPython's bookmark system.
671 """Manage IPython's bookmark system.
665
672
666 %bookmark <name> - set bookmark to current dir
673 %bookmark <name> - set bookmark to current dir
667 %bookmark <name> <dir> - set bookmark to <dir>
674 %bookmark <name> <dir> - set bookmark to <dir>
668 %bookmark -l - list all bookmarks
675 %bookmark -l - list all bookmarks
669 %bookmark -d <name> - remove bookmark
676 %bookmark -d <name> - remove bookmark
670 %bookmark -r - remove all bookmarks
677 %bookmark -r - remove all bookmarks
671
678
672 You can later on access a bookmarked folder with::
679 You can later on access a bookmarked folder with::
673
680
674 %cd -b <name>
681 %cd -b <name>
675
682
676 or simply '%cd <name>' if there is no directory called <name> AND
683 or simply '%cd <name>' if there is no directory called <name> AND
677 there is such a bookmark defined.
684 there is such a bookmark defined.
678
685
679 Your bookmarks persist through IPython sessions, but they are
686 Your bookmarks persist through IPython sessions, but they are
680 associated with each profile."""
687 associated with each profile."""
681
688
682 opts,args = self.parse_options(parameter_s,'drl',mode='list')
689 opts,args = self.parse_options(parameter_s,'drl',mode='list')
683 if len(args) > 2:
690 if len(args) > 2:
684 raise UsageError("%bookmark: too many arguments")
691 raise UsageError("%bookmark: too many arguments")
685
692
686 bkms = self.shell.db.get('bookmarks',{})
693 bkms = self.shell.db.get('bookmarks',{})
687
694
688 if 'd' in opts:
695 if 'd' in opts:
689 try:
696 try:
690 todel = args[0]
697 todel = args[0]
691 except IndexError:
698 except IndexError:
692 raise UsageError(
699 raise UsageError(
693 "%bookmark -d: must provide a bookmark to delete")
700 "%bookmark -d: must provide a bookmark to delete")
694 else:
701 else:
695 try:
702 try:
696 del bkms[todel]
703 del bkms[todel]
697 except KeyError:
704 except KeyError:
698 raise UsageError(
705 raise UsageError(
699 "%%bookmark -d: Can't delete bookmark '%s'" % todel)
706 "%%bookmark -d: Can't delete bookmark '%s'" % todel)
700
707
701 elif 'r' in opts:
708 elif 'r' in opts:
702 bkms = {}
709 bkms = {}
703 elif 'l' in opts:
710 elif 'l' in opts:
704 bks = sorted(bkms)
711 bks = sorted(bkms)
705 if bks:
712 if bks:
706 size = max(map(len, bks))
713 size = max(map(len, bks))
707 else:
714 else:
708 size = 0
715 size = 0
709 fmt = '%-'+str(size)+'s -> %s'
716 fmt = '%-'+str(size)+'s -> %s'
710 print('Current bookmarks:')
717 print('Current bookmarks:')
711 for bk in bks:
718 for bk in bks:
712 print(fmt % (bk, bkms[bk]))
719 print(fmt % (bk, bkms[bk]))
713 else:
720 else:
714 if not args:
721 if not args:
715 raise UsageError("%bookmark: You must specify the bookmark name")
722 raise UsageError("%bookmark: You must specify the bookmark name")
716 elif len(args)==1:
723 elif len(args)==1:
717 bkms[args[0]] = py3compat.getcwd()
724 bkms[args[0]] = py3compat.getcwd()
718 elif len(args)==2:
725 elif len(args)==2:
719 bkms[args[0]] = args[1]
726 bkms[args[0]] = args[1]
720 self.shell.db['bookmarks'] = bkms
727 self.shell.db['bookmarks'] = bkms
721
728
722 @line_magic
729 @line_magic
723 def pycat(self, parameter_s=''):
730 def pycat(self, parameter_s=''):
724 """Show a syntax-highlighted file through a pager.
731 """Show a syntax-highlighted file through a pager.
725
732
726 This magic is similar to the cat utility, but it will assume the file
733 This magic is similar to the cat utility, but it will assume the file
727 to be Python source and will show it with syntax highlighting.
734 to be Python source and will show it with syntax highlighting.
728
735
729 This magic command can either take a local filename, an url,
736 This magic command can either take a local filename, an url,
730 an history range (see %history) or a macro as argument ::
737 an history range (see %history) or a macro as argument ::
731
738
732 %pycat myscript.py
739 %pycat myscript.py
733 %pycat 7-27
740 %pycat 7-27
734 %pycat myMacro
741 %pycat myMacro
735 %pycat http://www.example.com/myscript.py
742 %pycat http://www.example.com/myscript.py
736 """
743 """
737 if not parameter_s:
744 if not parameter_s:
738 raise UsageError('Missing filename, URL, input history range, '
745 raise UsageError('Missing filename, URL, input history range, '
739 'or macro.')
746 'or macro.')
740
747
741 try :
748 try :
742 cont = self.shell.find_user_code(parameter_s, skip_encoding_cookie=False)
749 cont = self.shell.find_user_code(parameter_s, skip_encoding_cookie=False)
743 except (ValueError, IOError):
750 except (ValueError, IOError):
744 print("Error: no such file, variable, URL, history range or macro")
751 print("Error: no such file, variable, URL, history range or macro")
745 return
752 return
746
753
747 page.page(self.shell.pycolorize(source_to_unicode(cont)))
754 page.page(self.shell.pycolorize(source_to_unicode(cont)))
748
755
749 @magic_arguments.magic_arguments()
756 @magic_arguments.magic_arguments()
750 @magic_arguments.argument(
757 @magic_arguments.argument(
751 '-a', '--append', action='store_true', default=False,
758 '-a', '--append', action='store_true', default=False,
752 help='Append contents of the cell to an existing file. '
759 help='Append contents of the cell to an existing file. '
753 'The file will be created if it does not exist.'
760 'The file will be created if it does not exist.'
754 )
761 )
755 @magic_arguments.argument(
762 @magic_arguments.argument(
756 'filename', type=unicode_type,
763 'filename', type=unicode_type,
757 help='file to write'
764 help='file to write'
758 )
765 )
759 @cell_magic
766 @cell_magic
760 def writefile(self, line, cell):
767 def writefile(self, line, cell):
761 """Write the contents of the cell to a file.
768 """Write the contents of the cell to a file.
762
769
763 The file will be overwritten unless the -a (--append) flag is specified.
770 The file will be overwritten unless the -a (--append) flag is specified.
764 """
771 """
765 args = magic_arguments.parse_argstring(self.writefile, line)
772 args = magic_arguments.parse_argstring(self.writefile, line)
766 filename = os.path.expanduser(unquote_filename(args.filename))
773 filename = os.path.expanduser(unquote_filename(args.filename))
767
774
768 if os.path.exists(filename):
775 if os.path.exists(filename):
769 if args.append:
776 if args.append:
770 print("Appending to %s" % filename)
777 print("Appending to %s" % filename)
771 else:
778 else:
772 print("Overwriting %s" % filename)
779 print("Overwriting %s" % filename)
773 else:
780 else:
774 print("Writing %s" % filename)
781 print("Writing %s" % filename)
775
782
776 mode = 'a' if args.append else 'w'
783 mode = 'a' if args.append else 'w'
777 with io.open(filename, mode, encoding='utf-8') as f:
784 with io.open(filename, mode, encoding='utf-8') as f:
778 f.write(cell)
785 f.write(cell)
@@ -1,713 +1,734 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Process Controller
2 """IPython Test Process Controller
3
3
4 This module runs one or more subprocesses which will actually run the IPython
4 This module runs one or more subprocesses which will actually run the IPython
5 test suite.
5 test suite.
6
6
7 """
7 """
8
8
9 # Copyright (c) IPython Development Team.
9 # Copyright (c) IPython Development Team.
10 # Distributed under the terms of the Modified BSD License.
10 # Distributed under the terms of the Modified BSD License.
11
11
12 from __future__ import print_function
12 from __future__ import print_function
13
13
14 import argparse
14 import argparse
15 import json
15 import json
16 import multiprocessing.pool
16 import multiprocessing.pool
17 import os
17 import os
18 import stat
18 import re
19 import re
19 import requests
20 import requests
20 import shutil
21 import shutil
21 import signal
22 import signal
22 import sys
23 import sys
23 import subprocess
24 import subprocess
24 import time
25 import time
25
26
26 from .iptest import (
27 from .iptest import (
27 have, test_group_names as py_test_group_names, test_sections, StreamCapturer,
28 have, test_group_names as py_test_group_names, test_sections, StreamCapturer,
28 test_for,
29 test_for,
29 )
30 )
30 from IPython.utils.path import compress_user
31 from IPython.utils.path import compress_user
31 from IPython.utils.py3compat import bytes_to_str
32 from IPython.utils.py3compat import bytes_to_str
32 from IPython.utils.sysinfo import get_sys_info
33 from IPython.utils.sysinfo import get_sys_info
33 from IPython.utils.tempdir import TemporaryDirectory
34 from IPython.utils.tempdir import TemporaryDirectory
34 from IPython.utils.text import strip_ansi
35 from IPython.utils.text import strip_ansi
35
36
36 try:
37 try:
37 # Python >= 3.3
38 # Python >= 3.3
38 from subprocess import TimeoutExpired
39 from subprocess import TimeoutExpired
39 def popen_wait(p, timeout):
40 def popen_wait(p, timeout):
40 return p.wait(timeout)
41 return p.wait(timeout)
41 except ImportError:
42 except ImportError:
42 class TimeoutExpired(Exception):
43 class TimeoutExpired(Exception):
43 pass
44 pass
44 def popen_wait(p, timeout):
45 def popen_wait(p, timeout):
45 """backport of Popen.wait from Python 3"""
46 """backport of Popen.wait from Python 3"""
46 for i in range(int(10 * timeout)):
47 for i in range(int(10 * timeout)):
47 if p.poll() is not None:
48 if p.poll() is not None:
48 return
49 return
49 time.sleep(0.1)
50 time.sleep(0.1)
50 if p.poll() is None:
51 if p.poll() is None:
51 raise TimeoutExpired
52 raise TimeoutExpired
52
53
53 NOTEBOOK_SHUTDOWN_TIMEOUT = 10
54 NOTEBOOK_SHUTDOWN_TIMEOUT = 10
54
55
55 class TestController(object):
56 class TestController(object):
56 """Run tests in a subprocess
57 """Run tests in a subprocess
57 """
58 """
58 #: str, IPython test suite to be executed.
59 #: str, IPython test suite to be executed.
59 section = None
60 section = None
60 #: list, command line arguments to be executed
61 #: list, command line arguments to be executed
61 cmd = None
62 cmd = None
62 #: dict, extra environment variables to set for the subprocess
63 #: dict, extra environment variables to set for the subprocess
63 env = None
64 env = None
64 #: list, TemporaryDirectory instances to clear up when the process finishes
65 #: list, TemporaryDirectory instances to clear up when the process finishes
65 dirs = None
66 dirs = None
66 #: subprocess.Popen instance
67 #: subprocess.Popen instance
67 process = None
68 process = None
68 #: str, process stdout+stderr
69 #: str, process stdout+stderr
69 stdout = None
70 stdout = None
70
71
71 def __init__(self):
72 def __init__(self):
72 self.cmd = []
73 self.cmd = []
73 self.env = {}
74 self.env = {}
74 self.dirs = []
75 self.dirs = []
75
76
76 def setup(self):
77 def setup(self):
77 """Create temporary directories etc.
78 """Create temporary directories etc.
78
79
79 This is only called when we know the test group will be run. Things
80 This is only called when we know the test group will be run. Things
80 created here may be cleaned up by self.cleanup().
81 created here may be cleaned up by self.cleanup().
81 """
82 """
82 pass
83 pass
83
84
84 def launch(self, buffer_output=False, capture_output=False):
85 def launch(self, buffer_output=False, capture_output=False):
85 # print('*** ENV:', self.env) # dbg
86 # print('*** ENV:', self.env) # dbg
86 # print('*** CMD:', self.cmd) # dbg
87 # print('*** CMD:', self.cmd) # dbg
87 env = os.environ.copy()
88 env = os.environ.copy()
88 env.update(self.env)
89 env.update(self.env)
89 if buffer_output:
90 if buffer_output:
90 capture_output = True
91 capture_output = True
91 self.stdout_capturer = c = StreamCapturer(echo=not buffer_output)
92 self.stdout_capturer = c = StreamCapturer(echo=not buffer_output)
92 c.start()
93 c.start()
93 stdout = c.writefd if capture_output else None
94 stdout = c.writefd if capture_output else None
94 stderr = subprocess.STDOUT if capture_output else None
95 stderr = subprocess.STDOUT if capture_output else None
95 self.process = subprocess.Popen(self.cmd, stdout=stdout,
96 self.process = subprocess.Popen(self.cmd, stdout=stdout,
96 stderr=stderr, env=env)
97 stderr=stderr, env=env)
97
98
98 def wait(self):
99 def wait(self):
99 self.process.wait()
100 self.process.wait()
100 self.stdout_capturer.halt()
101 self.stdout_capturer.halt()
101 self.stdout = self.stdout_capturer.get_buffer()
102 self.stdout = self.stdout_capturer.get_buffer()
102 return self.process.returncode
103 return self.process.returncode
103
104
104 def print_extra_info(self):
105 def print_extra_info(self):
105 """Print extra information about this test run.
106 """Print extra information about this test run.
106
107
107 If we're running in parallel and showing the concise view, this is only
108 If we're running in parallel and showing the concise view, this is only
108 called if the test group fails. Otherwise, it's called before the test
109 called if the test group fails. Otherwise, it's called before the test
109 group is started.
110 group is started.
110
111
111 The base implementation does nothing, but it can be overridden by
112 The base implementation does nothing, but it can be overridden by
112 subclasses.
113 subclasses.
113 """
114 """
114 return
115 return
115
116
116 def cleanup_process(self):
117 def cleanup_process(self):
117 """Cleanup on exit by killing any leftover processes."""
118 """Cleanup on exit by killing any leftover processes."""
118 subp = self.process
119 subp = self.process
119 if subp is None or (subp.poll() is not None):
120 if subp is None or (subp.poll() is not None):
120 return # Process doesn't exist, or is already dead.
121 return # Process doesn't exist, or is already dead.
121
122
122 try:
123 try:
123 print('Cleaning up stale PID: %d' % subp.pid)
124 print('Cleaning up stale PID: %d' % subp.pid)
124 subp.kill()
125 subp.kill()
125 except: # (OSError, WindowsError) ?
126 except: # (OSError, WindowsError) ?
126 # This is just a best effort, if we fail or the process was
127 # This is just a best effort, if we fail or the process was
127 # really gone, ignore it.
128 # really gone, ignore it.
128 pass
129 pass
129 else:
130 else:
130 for i in range(10):
131 for i in range(10):
131 if subp.poll() is None:
132 if subp.poll() is None:
132 time.sleep(0.1)
133 time.sleep(0.1)
133 else:
134 else:
134 break
135 break
135
136
136 if subp.poll() is None:
137 if subp.poll() is None:
137 # The process did not die...
138 # The process did not die...
138 print('... failed. Manual cleanup may be required.')
139 print('... failed. Manual cleanup may be required.')
139
140
140 def cleanup(self):
141 def cleanup(self):
141 "Kill process if it's still alive, and clean up temporary directories"
142 "Kill process if it's still alive, and clean up temporary directories"
142 self.cleanup_process()
143 self.cleanup_process()
143 for td in self.dirs:
144 for td in self.dirs:
144 td.cleanup()
145 td.cleanup()
145
146
146 __del__ = cleanup
147 __del__ = cleanup
147
148
148
149
149 class PyTestController(TestController):
150 class PyTestController(TestController):
150 """Run Python tests using IPython.testing.iptest"""
151 """Run Python tests using IPython.testing.iptest"""
151 #: str, Python command to execute in subprocess
152 #: str, Python command to execute in subprocess
152 pycmd = None
153 pycmd = None
153
154
154 def __init__(self, section, options):
155 def __init__(self, section, options):
155 """Create new test runner."""
156 """Create new test runner."""
156 TestController.__init__(self)
157 TestController.__init__(self)
157 self.section = section
158 self.section = section
158 # pycmd is put into cmd[2] in PyTestController.launch()
159 # pycmd is put into cmd[2] in PyTestController.launch()
159 self.cmd = [sys.executable, '-c', None, section]
160 self.cmd = [sys.executable, '-c', None, section]
160 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
161 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
161 self.options = options
162 self.options = options
162
163
163 def setup(self):
164 def setup(self):
164 ipydir = TemporaryDirectory()
165 ipydir = TemporaryDirectory()
165 self.dirs.append(ipydir)
166 self.dirs.append(ipydir)
166 self.env['IPYTHONDIR'] = ipydir.name
167 self.env['IPYTHONDIR'] = ipydir.name
167 self.workingdir = workingdir = TemporaryDirectory()
168 self.workingdir = workingdir = TemporaryDirectory()
168 self.dirs.append(workingdir)
169 self.dirs.append(workingdir)
169 self.env['IPTEST_WORKING_DIR'] = workingdir.name
170 self.env['IPTEST_WORKING_DIR'] = workingdir.name
170 # This means we won't get odd effects from our own matplotlib config
171 # This means we won't get odd effects from our own matplotlib config
171 self.env['MPLCONFIGDIR'] = workingdir.name
172 self.env['MPLCONFIGDIR'] = workingdir.name
172
173
174 # Add a non-accessible directory to PATH (see gh-7053)
175 noaccess = os.path.join(self.workingdir.name, "_no_access_")
176 self.noaccess = noaccess
177 os.mkdir(noaccess, 0)
178
179 PATH = os.environ.get('PATH', '')
180 if PATH:
181 PATH = noaccess + os.pathsep + PATH
182 else:
183 PATH = noaccess
184 self.env['PATH'] = PATH
185
173 # From options:
186 # From options:
174 if self.options.xunit:
187 if self.options.xunit:
175 self.add_xunit()
188 self.add_xunit()
176 if self.options.coverage:
189 if self.options.coverage:
177 self.add_coverage()
190 self.add_coverage()
178 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
191 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
179 self.cmd.extend(self.options.extra_args)
192 self.cmd.extend(self.options.extra_args)
180
193
194 def cleanup(self):
195 """
196 Make the non-accessible directory created in setup() accessible
197 again, otherwise deleting the workingdir will fail.
198 """
199 os.chmod(self.noaccess, stat.S_IRWXU)
200 TestController.cleanup(self)
201
181 @property
202 @property
182 def will_run(self):
203 def will_run(self):
183 try:
204 try:
184 return test_sections[self.section].will_run
205 return test_sections[self.section].will_run
185 except KeyError:
206 except KeyError:
186 return True
207 return True
187
208
188 def add_xunit(self):
209 def add_xunit(self):
189 xunit_file = os.path.abspath(self.section + '.xunit.xml')
210 xunit_file = os.path.abspath(self.section + '.xunit.xml')
190 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
211 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
191
212
192 def add_coverage(self):
213 def add_coverage(self):
193 try:
214 try:
194 sources = test_sections[self.section].includes
215 sources = test_sections[self.section].includes
195 except KeyError:
216 except KeyError:
196 sources = ['IPython']
217 sources = ['IPython']
197
218
198 coverage_rc = ("[run]\n"
219 coverage_rc = ("[run]\n"
199 "data_file = {data_file}\n"
220 "data_file = {data_file}\n"
200 "source =\n"
221 "source =\n"
201 " {source}\n"
222 " {source}\n"
202 ).format(data_file=os.path.abspath('.coverage.'+self.section),
223 ).format(data_file=os.path.abspath('.coverage.'+self.section),
203 source="\n ".join(sources))
224 source="\n ".join(sources))
204 config_file = os.path.join(self.workingdir.name, '.coveragerc')
225 config_file = os.path.join(self.workingdir.name, '.coveragerc')
205 with open(config_file, 'w') as f:
226 with open(config_file, 'w') as f:
206 f.write(coverage_rc)
227 f.write(coverage_rc)
207
228
208 self.env['COVERAGE_PROCESS_START'] = config_file
229 self.env['COVERAGE_PROCESS_START'] = config_file
209 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
230 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
210
231
211 def launch(self, buffer_output=False):
232 def launch(self, buffer_output=False):
212 self.cmd[2] = self.pycmd
233 self.cmd[2] = self.pycmd
213 super(PyTestController, self).launch(buffer_output=buffer_output)
234 super(PyTestController, self).launch(buffer_output=buffer_output)
214
235
215
236
216 js_prefix = 'js/'
237 js_prefix = 'js/'
217
238
218 def get_js_test_dir():
239 def get_js_test_dir():
219 import IPython.html.tests as t
240 import IPython.html.tests as t
220 return os.path.join(os.path.dirname(t.__file__), '')
241 return os.path.join(os.path.dirname(t.__file__), '')
221
242
222 def all_js_groups():
243 def all_js_groups():
223 import glob
244 import glob
224 test_dir = get_js_test_dir()
245 test_dir = get_js_test_dir()
225 all_subdirs = glob.glob(test_dir + '[!_]*/')
246 all_subdirs = glob.glob(test_dir + '[!_]*/')
226 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs]
247 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs]
227
248
228 class JSController(TestController):
249 class JSController(TestController):
229 """Run CasperJS tests """
250 """Run CasperJS tests """
230
251
231 requirements = ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3',
252 requirements = ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3',
232 'jsonschema']
253 'jsonschema']
233
254
234 def __init__(self, section, xunit=True, engine='phantomjs', url=None):
255 def __init__(self, section, xunit=True, engine='phantomjs', url=None):
235 """Create new test runner."""
256 """Create new test runner."""
236 TestController.__init__(self)
257 TestController.__init__(self)
237 self.engine = engine
258 self.engine = engine
238 self.section = section
259 self.section = section
239 self.xunit = xunit
260 self.xunit = xunit
240 self.url = url
261 self.url = url
241 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
262 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
242 js_test_dir = get_js_test_dir()
263 js_test_dir = get_js_test_dir()
243 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
264 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
244 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
265 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
245 self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
266 self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
246
267
247 def setup(self):
268 def setup(self):
248 self.ipydir = TemporaryDirectory()
269 self.ipydir = TemporaryDirectory()
249 self.nbdir = TemporaryDirectory()
270 self.nbdir = TemporaryDirectory()
250 self.dirs.append(self.ipydir)
271 self.dirs.append(self.ipydir)
251 self.dirs.append(self.nbdir)
272 self.dirs.append(self.nbdir)
252 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
273 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
253 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
274 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
254
275
255 if self.xunit:
276 if self.xunit:
256 self.add_xunit()
277 self.add_xunit()
257
278
258 # If a url was specified, use that for the testing.
279 # If a url was specified, use that for the testing.
259 if self.url:
280 if self.url:
260 try:
281 try:
261 alive = requests.get(self.url).status_code == 200
282 alive = requests.get(self.url).status_code == 200
262 except:
283 except:
263 alive = False
284 alive = False
264
285
265 if alive:
286 if alive:
266 self.cmd.append("--url=%s" % self.url)
287 self.cmd.append("--url=%s" % self.url)
267 else:
288 else:
268 raise Exception('Could not reach "%s".' % self.url)
289 raise Exception('Could not reach "%s".' % self.url)
269 else:
290 else:
270 # start the ipython notebook, so we get the port number
291 # start the ipython notebook, so we get the port number
271 self.server_port = 0
292 self.server_port = 0
272 self._init_server()
293 self._init_server()
273 if self.server_port:
294 if self.server_port:
274 self.cmd.append("--port=%i" % self.server_port)
295 self.cmd.append("--port=%i" % self.server_port)
275 else:
296 else:
276 # don't launch tests if the server didn't start
297 # don't launch tests if the server didn't start
277 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
298 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
278
299
279 def add_xunit(self):
300 def add_xunit(self):
280 xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml')
301 xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml')
281 self.cmd.append('--xunit=%s' % xunit_file)
302 self.cmd.append('--xunit=%s' % xunit_file)
282
303
283 def launch(self, buffer_output):
304 def launch(self, buffer_output):
284 # If the engine is SlimerJS, we need to buffer the output because
305 # If the engine is SlimerJS, we need to buffer the output because
285 # SlimerJS does not support exit codes, so CasperJS always returns 0.
306 # SlimerJS does not support exit codes, so CasperJS always returns 0.
286 if self.engine == 'slimerjs' and not buffer_output:
307 if self.engine == 'slimerjs' and not buffer_output:
287 return super(JSController, self).launch(capture_output=True)
308 return super(JSController, self).launch(capture_output=True)
288
309
289 else:
310 else:
290 return super(JSController, self).launch(buffer_output=buffer_output)
311 return super(JSController, self).launch(buffer_output=buffer_output)
291
312
292 def wait(self, *pargs, **kwargs):
313 def wait(self, *pargs, **kwargs):
293 """Wait for the JSController to finish"""
314 """Wait for the JSController to finish"""
294 ret = super(JSController, self).wait(*pargs, **kwargs)
315 ret = super(JSController, self).wait(*pargs, **kwargs)
295 # If this is a SlimerJS controller, check the captured stdout for
316 # If this is a SlimerJS controller, check the captured stdout for
296 # errors. Otherwise, just return the return code.
317 # errors. Otherwise, just return the return code.
297 if self.engine == 'slimerjs':
318 if self.engine == 'slimerjs':
298 stdout = bytes_to_str(self.stdout)
319 stdout = bytes_to_str(self.stdout)
299 if ret != 0:
320 if ret != 0:
300 # This could still happen e.g. if it's stopped by SIGINT
321 # This could still happen e.g. if it's stopped by SIGINT
301 return ret
322 return ret
302 return bool(self.slimer_failure.search(strip_ansi(stdout)))
323 return bool(self.slimer_failure.search(strip_ansi(stdout)))
303 else:
324 else:
304 return ret
325 return ret
305
326
306 def print_extra_info(self):
327 def print_extra_info(self):
307 print("Running tests with notebook directory %r" % self.nbdir.name)
328 print("Running tests with notebook directory %r" % self.nbdir.name)
308
329
309 @property
330 @property
310 def will_run(self):
331 def will_run(self):
311 should_run = all(have[a] for a in self.requirements + [self.engine])
332 should_run = all(have[a] for a in self.requirements + [self.engine])
312 return should_run
333 return should_run
313
334
314 def _init_server(self):
335 def _init_server(self):
315 "Start the notebook server in a separate process"
336 "Start the notebook server in a separate process"
316 self.server_command = command = [sys.executable,
337 self.server_command = command = [sys.executable,
317 '-m', 'IPython.html',
338 '-m', 'IPython.html',
318 '--no-browser',
339 '--no-browser',
319 '--ipython-dir', self.ipydir.name,
340 '--ipython-dir', self.ipydir.name,
320 '--notebook-dir', self.nbdir.name,
341 '--notebook-dir', self.nbdir.name,
321 ]
342 ]
322 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
343 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
323 # which run afoul of ipc's maximum path length.
344 # which run afoul of ipc's maximum path length.
324 if sys.platform.startswith('linux'):
345 if sys.platform.startswith('linux'):
325 command.append('--KernelManager.transport=ipc')
346 command.append('--KernelManager.transport=ipc')
326 self.stream_capturer = c = StreamCapturer()
347 self.stream_capturer = c = StreamCapturer()
327 c.start()
348 c.start()
328 env = os.environ.copy()
349 env = os.environ.copy()
329 if self.engine == 'phantomjs':
350 if self.engine == 'phantomjs':
330 env['IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS'] = '1'
351 env['IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS'] = '1'
331 self.server = subprocess.Popen(command,
352 self.server = subprocess.Popen(command,
332 stdout=c.writefd,
353 stdout=c.writefd,
333 stderr=subprocess.STDOUT,
354 stderr=subprocess.STDOUT,
334 cwd=self.nbdir.name,
355 cwd=self.nbdir.name,
335 env=env,
356 env=env,
336 )
357 )
337 self.server_info_file = os.path.join(self.ipydir.name,
358 self.server_info_file = os.path.join(self.ipydir.name,
338 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
359 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
339 )
360 )
340 self._wait_for_server()
361 self._wait_for_server()
341
362
342 def _wait_for_server(self):
363 def _wait_for_server(self):
343 """Wait 30 seconds for the notebook server to start"""
364 """Wait 30 seconds for the notebook server to start"""
344 for i in range(300):
365 for i in range(300):
345 if self.server.poll() is not None:
366 if self.server.poll() is not None:
346 return self._failed_to_start()
367 return self._failed_to_start()
347 if os.path.exists(self.server_info_file):
368 if os.path.exists(self.server_info_file):
348 try:
369 try:
349 self._load_server_info()
370 self._load_server_info()
350 except ValueError:
371 except ValueError:
351 # If the server is halfway through writing the file, we may
372 # If the server is halfway through writing the file, we may
352 # get invalid JSON; it should be ready next iteration.
373 # get invalid JSON; it should be ready next iteration.
353 pass
374 pass
354 else:
375 else:
355 return
376 return
356 time.sleep(0.1)
377 time.sleep(0.1)
357 print("Notebook server-info file never arrived: %s" % self.server_info_file,
378 print("Notebook server-info file never arrived: %s" % self.server_info_file,
358 file=sys.stderr
379 file=sys.stderr
359 )
380 )
360
381
361 def _failed_to_start(self):
382 def _failed_to_start(self):
362 """Notebook server exited prematurely"""
383 """Notebook server exited prematurely"""
363 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
384 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
364 print("Notebook failed to start: ", file=sys.stderr)
385 print("Notebook failed to start: ", file=sys.stderr)
365 print(self.server_command)
386 print(self.server_command)
366 print(captured, file=sys.stderr)
387 print(captured, file=sys.stderr)
367
388
368 def _load_server_info(self):
389 def _load_server_info(self):
369 """Notebook server started, load connection info from JSON"""
390 """Notebook server started, load connection info from JSON"""
370 with open(self.server_info_file) as f:
391 with open(self.server_info_file) as f:
371 info = json.load(f)
392 info = json.load(f)
372 self.server_port = info['port']
393 self.server_port = info['port']
373
394
374 def cleanup(self):
395 def cleanup(self):
375 try:
396 try:
376 self.server.terminate()
397 self.server.terminate()
377 except OSError:
398 except OSError:
378 # already dead
399 # already dead
379 pass
400 pass
380 # wait 10s for the server to shutdown
401 # wait 10s for the server to shutdown
381 try:
402 try:
382 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
403 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
383 except TimeoutExpired:
404 except TimeoutExpired:
384 # server didn't terminate, kill it
405 # server didn't terminate, kill it
385 try:
406 try:
386 print("Failed to terminate notebook server, killing it.",
407 print("Failed to terminate notebook server, killing it.",
387 file=sys.stderr
408 file=sys.stderr
388 )
409 )
389 self.server.kill()
410 self.server.kill()
390 except OSError:
411 except OSError:
391 # already dead
412 # already dead
392 pass
413 pass
393 # wait another 10s
414 # wait another 10s
394 try:
415 try:
395 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
416 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
396 except TimeoutExpired:
417 except TimeoutExpired:
397 print("Notebook server still running (%s)" % self.server_info_file,
418 print("Notebook server still running (%s)" % self.server_info_file,
398 file=sys.stderr
419 file=sys.stderr
399 )
420 )
400
421
401 self.stream_capturer.halt()
422 self.stream_capturer.halt()
402 TestController.cleanup(self)
423 TestController.cleanup(self)
403
424
404
425
405 def prepare_controllers(options):
426 def prepare_controllers(options):
406 """Returns two lists of TestController instances, those to run, and those
427 """Returns two lists of TestController instances, those to run, and those
407 not to run."""
428 not to run."""
408 testgroups = options.testgroups
429 testgroups = options.testgroups
409 if testgroups:
430 if testgroups:
410 if 'js' in testgroups:
431 if 'js' in testgroups:
411 js_testgroups = all_js_groups()
432 js_testgroups = all_js_groups()
412 else:
433 else:
413 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
434 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
414
435
415 py_testgroups = [g for g in testgroups if g not in ['js'] + js_testgroups]
436 py_testgroups = [g for g in testgroups if g not in ['js'] + js_testgroups]
416 else:
437 else:
417 py_testgroups = py_test_group_names
438 py_testgroups = py_test_group_names
418 if not options.all:
439 if not options.all:
419 js_testgroups = []
440 js_testgroups = []
420 test_sections['parallel'].enabled = False
441 test_sections['parallel'].enabled = False
421 else:
442 else:
422 js_testgroups = all_js_groups()
443 js_testgroups = all_js_groups()
423
444
424 engine = 'slimerjs' if options.slimerjs else 'phantomjs'
445 engine = 'slimerjs' if options.slimerjs else 'phantomjs'
425 c_js = [JSController(name, xunit=options.xunit, engine=engine, url=options.url) for name in js_testgroups]
446 c_js = [JSController(name, xunit=options.xunit, engine=engine, url=options.url) for name in js_testgroups]
426 c_py = [PyTestController(name, options) for name in py_testgroups]
447 c_py = [PyTestController(name, options) for name in py_testgroups]
427
448
428 controllers = c_py + c_js
449 controllers = c_py + c_js
429 to_run = [c for c in controllers if c.will_run]
450 to_run = [c for c in controllers if c.will_run]
430 not_run = [c for c in controllers if not c.will_run]
451 not_run = [c for c in controllers if not c.will_run]
431 return to_run, not_run
452 return to_run, not_run
432
453
433 def do_run(controller, buffer_output=True):
454 def do_run(controller, buffer_output=True):
434 """Setup and run a test controller.
455 """Setup and run a test controller.
435
456
436 If buffer_output is True, no output is displayed, to avoid it appearing
457 If buffer_output is True, no output is displayed, to avoid it appearing
437 interleaved. In this case, the caller is responsible for displaying test
458 interleaved. In this case, the caller is responsible for displaying test
438 output on failure.
459 output on failure.
439
460
440 Returns
461 Returns
441 -------
462 -------
442 controller : TestController
463 controller : TestController
443 The same controller as passed in, as a convenience for using map() type
464 The same controller as passed in, as a convenience for using map() type
444 APIs.
465 APIs.
445 exitcode : int
466 exitcode : int
446 The exit code of the test subprocess. Non-zero indicates failure.
467 The exit code of the test subprocess. Non-zero indicates failure.
447 """
468 """
448 try:
469 try:
449 try:
470 try:
450 controller.setup()
471 controller.setup()
451 if not buffer_output:
472 if not buffer_output:
452 controller.print_extra_info()
473 controller.print_extra_info()
453 controller.launch(buffer_output=buffer_output)
474 controller.launch(buffer_output=buffer_output)
454 except Exception:
475 except Exception:
455 import traceback
476 import traceback
456 traceback.print_exc()
477 traceback.print_exc()
457 return controller, 1 # signal failure
478 return controller, 1 # signal failure
458
479
459 exitcode = controller.wait()
480 exitcode = controller.wait()
460 return controller, exitcode
481 return controller, exitcode
461
482
462 except KeyboardInterrupt:
483 except KeyboardInterrupt:
463 return controller, -signal.SIGINT
484 return controller, -signal.SIGINT
464 finally:
485 finally:
465 controller.cleanup()
486 controller.cleanup()
466
487
467 def report():
488 def report():
468 """Return a string with a summary report of test-related variables."""
489 """Return a string with a summary report of test-related variables."""
469 inf = get_sys_info()
490 inf = get_sys_info()
470 out = []
491 out = []
471 def _add(name, value):
492 def _add(name, value):
472 out.append((name, value))
493 out.append((name, value))
473
494
474 _add('IPython version', inf['ipython_version'])
495 _add('IPython version', inf['ipython_version'])
475 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
496 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
476 _add('IPython package', compress_user(inf['ipython_path']))
497 _add('IPython package', compress_user(inf['ipython_path']))
477 _add('Python version', inf['sys_version'].replace('\n',''))
498 _add('Python version', inf['sys_version'].replace('\n',''))
478 _add('sys.executable', compress_user(inf['sys_executable']))
499 _add('sys.executable', compress_user(inf['sys_executable']))
479 _add('Platform', inf['platform'])
500 _add('Platform', inf['platform'])
480
501
481 width = max(len(n) for (n,v) in out)
502 width = max(len(n) for (n,v) in out)
482 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
503 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
483
504
484 avail = []
505 avail = []
485 not_avail = []
506 not_avail = []
486
507
487 for k, is_avail in have.items():
508 for k, is_avail in have.items():
488 if is_avail:
509 if is_avail:
489 avail.append(k)
510 avail.append(k)
490 else:
511 else:
491 not_avail.append(k)
512 not_avail.append(k)
492
513
493 if avail:
514 if avail:
494 out.append('\nTools and libraries available at test time:\n')
515 out.append('\nTools and libraries available at test time:\n')
495 avail.sort()
516 avail.sort()
496 out.append(' ' + ' '.join(avail)+'\n')
517 out.append(' ' + ' '.join(avail)+'\n')
497
518
498 if not_avail:
519 if not_avail:
499 out.append('\nTools and libraries NOT available at test time:\n')
520 out.append('\nTools and libraries NOT available at test time:\n')
500 not_avail.sort()
521 not_avail.sort()
501 out.append(' ' + ' '.join(not_avail)+'\n')
522 out.append(' ' + ' '.join(not_avail)+'\n')
502
523
503 return ''.join(out)
524 return ''.join(out)
504
525
505 def run_iptestall(options):
526 def run_iptestall(options):
506 """Run the entire IPython test suite by calling nose and trial.
527 """Run the entire IPython test suite by calling nose and trial.
507
528
508 This function constructs :class:`IPTester` instances for all IPython
529 This function constructs :class:`IPTester` instances for all IPython
509 modules and package and then runs each of them. This causes the modules
530 modules and package and then runs each of them. This causes the modules
510 and packages of IPython to be tested each in their own subprocess using
531 and packages of IPython to be tested each in their own subprocess using
511 nose.
532 nose.
512
533
513 Parameters
534 Parameters
514 ----------
535 ----------
515
536
516 All parameters are passed as attributes of the options object.
537 All parameters are passed as attributes of the options object.
517
538
518 testgroups : list of str
539 testgroups : list of str
519 Run only these sections of the test suite. If empty, run all the available
540 Run only these sections of the test suite. If empty, run all the available
520 sections.
541 sections.
521
542
522 fast : int or None
543 fast : int or None
523 Run the test suite in parallel, using n simultaneous processes. If None
544 Run the test suite in parallel, using n simultaneous processes. If None
524 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
545 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
525
546
526 inc_slow : bool
547 inc_slow : bool
527 Include slow tests, like IPython.parallel. By default, these tests aren't
548 Include slow tests, like IPython.parallel. By default, these tests aren't
528 run.
549 run.
529
550
530 slimerjs : bool
551 slimerjs : bool
531 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
552 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
532
553
533 url : unicode
554 url : unicode
534 Address:port to use when running the JS tests.
555 Address:port to use when running the JS tests.
535
556
536 xunit : bool
557 xunit : bool
537 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
558 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
538
559
539 coverage : bool or str
560 coverage : bool or str
540 Measure code coverage from tests. True will store the raw coverage data,
561 Measure code coverage from tests. True will store the raw coverage data,
541 or pass 'html' or 'xml' to get reports.
562 or pass 'html' or 'xml' to get reports.
542
563
543 extra_args : list
564 extra_args : list
544 Extra arguments to pass to the test subprocesses, e.g. '-v'
565 Extra arguments to pass to the test subprocesses, e.g. '-v'
545 """
566 """
546 to_run, not_run = prepare_controllers(options)
567 to_run, not_run = prepare_controllers(options)
547
568
548 def justify(ltext, rtext, width=70, fill='-'):
569 def justify(ltext, rtext, width=70, fill='-'):
549 ltext += ' '
570 ltext += ' '
550 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
571 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
551 return ltext + rtext
572 return ltext + rtext
552
573
553 # Run all test runners, tracking execution time
574 # Run all test runners, tracking execution time
554 failed = []
575 failed = []
555 t_start = time.time()
576 t_start = time.time()
556
577
557 print()
578 print()
558 if options.fast == 1:
579 if options.fast == 1:
559 # This actually means sequential, i.e. with 1 job
580 # This actually means sequential, i.e. with 1 job
560 for controller in to_run:
581 for controller in to_run:
561 print('Test group:', controller.section)
582 print('Test group:', controller.section)
562 sys.stdout.flush() # Show in correct order when output is piped
583 sys.stdout.flush() # Show in correct order when output is piped
563 controller, res = do_run(controller, buffer_output=False)
584 controller, res = do_run(controller, buffer_output=False)
564 if res:
585 if res:
565 failed.append(controller)
586 failed.append(controller)
566 if res == -signal.SIGINT:
587 if res == -signal.SIGINT:
567 print("Interrupted")
588 print("Interrupted")
568 break
589 break
569 print()
590 print()
570
591
571 else:
592 else:
572 # Run tests concurrently
593 # Run tests concurrently
573 try:
594 try:
574 pool = multiprocessing.pool.ThreadPool(options.fast)
595 pool = multiprocessing.pool.ThreadPool(options.fast)
575 for (controller, res) in pool.imap_unordered(do_run, to_run):
596 for (controller, res) in pool.imap_unordered(do_run, to_run):
576 res_string = 'OK' if res == 0 else 'FAILED'
597 res_string = 'OK' if res == 0 else 'FAILED'
577 print(justify('Test group: ' + controller.section, res_string))
598 print(justify('Test group: ' + controller.section, res_string))
578 if res:
599 if res:
579 controller.print_extra_info()
600 controller.print_extra_info()
580 print(bytes_to_str(controller.stdout))
601 print(bytes_to_str(controller.stdout))
581 failed.append(controller)
602 failed.append(controller)
582 if res == -signal.SIGINT:
603 if res == -signal.SIGINT:
583 print("Interrupted")
604 print("Interrupted")
584 break
605 break
585 except KeyboardInterrupt:
606 except KeyboardInterrupt:
586 return
607 return
587
608
588 for controller in not_run:
609 for controller in not_run:
589 print(justify('Test group: ' + controller.section, 'NOT RUN'))
610 print(justify('Test group: ' + controller.section, 'NOT RUN'))
590
611
591 t_end = time.time()
612 t_end = time.time()
592 t_tests = t_end - t_start
613 t_tests = t_end - t_start
593 nrunners = len(to_run)
614 nrunners = len(to_run)
594 nfail = len(failed)
615 nfail = len(failed)
595 # summarize results
616 # summarize results
596 print('_'*70)
617 print('_'*70)
597 print('Test suite completed for system with the following information:')
618 print('Test suite completed for system with the following information:')
598 print(report())
619 print(report())
599 took = "Took %.3fs." % t_tests
620 took = "Took %.3fs." % t_tests
600 print('Status: ', end='')
621 print('Status: ', end='')
601 if not failed:
622 if not failed:
602 print('OK (%d test groups).' % nrunners, took)
623 print('OK (%d test groups).' % nrunners, took)
603 else:
624 else:
604 # If anything went wrong, point out what command to rerun manually to
625 # If anything went wrong, point out what command to rerun manually to
605 # see the actual errors and individual summary
626 # see the actual errors and individual summary
606 failed_sections = [c.section for c in failed]
627 failed_sections = [c.section for c in failed]
607 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
628 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
608 nrunners, ', '.join(failed_sections)), took)
629 nrunners, ', '.join(failed_sections)), took)
609 print()
630 print()
610 print('You may wish to rerun these, with:')
631 print('You may wish to rerun these, with:')
611 print(' iptest', *failed_sections)
632 print(' iptest', *failed_sections)
612 print()
633 print()
613
634
614 if options.coverage:
635 if options.coverage:
615 from coverage import coverage
636 from coverage import coverage
616 cov = coverage(data_file='.coverage')
637 cov = coverage(data_file='.coverage')
617 cov.combine()
638 cov.combine()
618 cov.save()
639 cov.save()
619
640
620 # Coverage HTML report
641 # Coverage HTML report
621 if options.coverage == 'html':
642 if options.coverage == 'html':
622 html_dir = 'ipy_htmlcov'
643 html_dir = 'ipy_htmlcov'
623 shutil.rmtree(html_dir, ignore_errors=True)
644 shutil.rmtree(html_dir, ignore_errors=True)
624 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
645 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
625 sys.stdout.flush()
646 sys.stdout.flush()
626
647
627 # Custom HTML reporter to clean up module names.
648 # Custom HTML reporter to clean up module names.
628 from coverage.html import HtmlReporter
649 from coverage.html import HtmlReporter
629 class CustomHtmlReporter(HtmlReporter):
650 class CustomHtmlReporter(HtmlReporter):
630 def find_code_units(self, morfs):
651 def find_code_units(self, morfs):
631 super(CustomHtmlReporter, self).find_code_units(morfs)
652 super(CustomHtmlReporter, self).find_code_units(morfs)
632 for cu in self.code_units:
653 for cu in self.code_units:
633 nameparts = cu.name.split(os.sep)
654 nameparts = cu.name.split(os.sep)
634 if 'IPython' not in nameparts:
655 if 'IPython' not in nameparts:
635 continue
656 continue
636 ix = nameparts.index('IPython')
657 ix = nameparts.index('IPython')
637 cu.name = '.'.join(nameparts[ix:])
658 cu.name = '.'.join(nameparts[ix:])
638
659
639 # Reimplement the html_report method with our custom reporter
660 # Reimplement the html_report method with our custom reporter
640 cov._harvest_data()
661 cov._harvest_data()
641 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
662 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
642 html_title='IPython test coverage',
663 html_title='IPython test coverage',
643 )
664 )
644 reporter = CustomHtmlReporter(cov, cov.config)
665 reporter = CustomHtmlReporter(cov, cov.config)
645 reporter.report(None)
666 reporter.report(None)
646 print('done.')
667 print('done.')
647
668
648 # Coverage XML report
669 # Coverage XML report
649 elif options.coverage == 'xml':
670 elif options.coverage == 'xml':
650 cov.xml_report(outfile='ipy_coverage.xml')
671 cov.xml_report(outfile='ipy_coverage.xml')
651
672
652 if failed:
673 if failed:
653 # Ensure that our exit code indicates failure
674 # Ensure that our exit code indicates failure
654 sys.exit(1)
675 sys.exit(1)
655
676
656 argparser = argparse.ArgumentParser(description='Run IPython test suite')
677 argparser = argparse.ArgumentParser(description='Run IPython test suite')
657 argparser.add_argument('testgroups', nargs='*',
678 argparser.add_argument('testgroups', nargs='*',
658 help='Run specified groups of tests. If omitted, run '
679 help='Run specified groups of tests. If omitted, run '
659 'all tests.')
680 'all tests.')
660 argparser.add_argument('--all', action='store_true',
681 argparser.add_argument('--all', action='store_true',
661 help='Include slow tests not run by default.')
682 help='Include slow tests not run by default.')
662 argparser.add_argument('--slimerjs', action='store_true',
683 argparser.add_argument('--slimerjs', action='store_true',
663 help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
684 help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
664 argparser.add_argument('--url', help="URL to use for the JS tests.")
685 argparser.add_argument('--url', help="URL to use for the JS tests.")
665 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
686 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
666 help='Run test sections in parallel. This starts as many '
687 help='Run test sections in parallel. This starts as many '
667 'processes as you have cores, or you can specify a number.')
688 'processes as you have cores, or you can specify a number.')
668 argparser.add_argument('--xunit', action='store_true',
689 argparser.add_argument('--xunit', action='store_true',
669 help='Produce Xunit XML results')
690 help='Produce Xunit XML results')
670 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
691 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
671 help="Measure test coverage. Specify 'html' or "
692 help="Measure test coverage. Specify 'html' or "
672 "'xml' to get reports.")
693 "'xml' to get reports.")
673 argparser.add_argument('--subproc-streams', default='capture',
694 argparser.add_argument('--subproc-streams', default='capture',
674 help="What to do with stdout/stderr from subprocesses. "
695 help="What to do with stdout/stderr from subprocesses. "
675 "'capture' (default), 'show' and 'discard' are the options.")
696 "'capture' (default), 'show' and 'discard' are the options.")
676
697
677 def default_options():
698 def default_options():
678 """Get an argparse Namespace object with the default arguments, to pass to
699 """Get an argparse Namespace object with the default arguments, to pass to
679 :func:`run_iptestall`.
700 :func:`run_iptestall`.
680 """
701 """
681 options = argparser.parse_args([])
702 options = argparser.parse_args([])
682 options.extra_args = []
703 options.extra_args = []
683 return options
704 return options
684
705
685 def main():
706 def main():
686 # iptest doesn't work correctly if the working directory is the
707 # iptest doesn't work correctly if the working directory is the
687 # root of the IPython source tree. Tell the user to avoid
708 # root of the IPython source tree. Tell the user to avoid
688 # frustration.
709 # frustration.
689 if os.path.exists(os.path.join(os.getcwd(),
710 if os.path.exists(os.path.join(os.getcwd(),
690 'IPython', 'testing', '__main__.py')):
711 'IPython', 'testing', '__main__.py')):
691 print("Don't run iptest from the IPython source directory",
712 print("Don't run iptest from the IPython source directory",
692 file=sys.stderr)
713 file=sys.stderr)
693 sys.exit(1)
714 sys.exit(1)
694 # Arguments after -- should be passed through to nose. Argparse treats
715 # Arguments after -- should be passed through to nose. Argparse treats
695 # everything after -- as regular positional arguments, so we separate them
716 # everything after -- as regular positional arguments, so we separate them
696 # first.
717 # first.
697 try:
718 try:
698 ix = sys.argv.index('--')
719 ix = sys.argv.index('--')
699 except ValueError:
720 except ValueError:
700 to_parse = sys.argv[1:]
721 to_parse = sys.argv[1:]
701 extra_args = []
722 extra_args = []
702 else:
723 else:
703 to_parse = sys.argv[1:ix]
724 to_parse = sys.argv[1:ix]
704 extra_args = sys.argv[ix+1:]
725 extra_args = sys.argv[ix+1:]
705
726
706 options = argparser.parse_args(to_parse)
727 options = argparser.parse_args(to_parse)
707 options.extra_args = extra_args
728 options.extra_args = extra_args
708
729
709 run_iptestall(options)
730 run_iptestall(options)
710
731
711
732
712 if __name__ == '__main__':
733 if __name__ == '__main__':
713 main()
734 main()
General Comments 0
You need to be logged in to leave comments. Login now