##// END OF EJS Templates
Clean up formatting sys info for test report
Thomas Kluyver -
Show More
@@ -1,422 +1,435
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Process Controller
3 3
4 4 This module runs one or more subprocesses which will actually run the IPython
5 5 test suite.
6 6
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2009-2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19 from __future__ import print_function
20 20
21 21 import argparse
22 22 import multiprocessing.pool
23 23 import os
24 24 import shutil
25 25 import signal
26 26 import sys
27 27 import subprocess
28 28 import time
29 29
30 30 from .iptest import have, test_group_names, test_sections
31 from IPython.utils.path import compress_user
31 32 from IPython.utils.py3compat import bytes_to_str
32 from IPython.utils.sysinfo import brief_sys_info
33 from IPython.utils.sysinfo import get_sys_info
33 34 from IPython.utils.tempdir import TemporaryDirectory
34 35
35 36
36 37 class TestController(object):
37 38 """Run tests in a subprocess
38 39 """
39 40 #: str, IPython test suite to be executed.
40 41 section = None
41 42 #: list, command line arguments to be executed
42 43 cmd = None
43 44 #: dict, extra environment variables to set for the subprocess
44 45 env = None
45 46 #: list, TemporaryDirectory instances to clear up when the process finishes
46 47 dirs = None
47 48 #: subprocess.Popen instance
48 49 process = None
49 50 #: str, process stdout+stderr
50 51 stdout = None
51 52 #: bool, whether to capture process stdout & stderr
52 53 buffer_output = False
53 54
54 55 def __init__(self):
55 56 self.cmd = []
56 57 self.env = {}
57 58 self.dirs = []
58 59
59 60 @property
60 61 def will_run(self):
61 62 """Override in subclasses to check for dependencies."""
62 63 return False
63 64
64 65 def launch(self):
65 66 # print('*** ENV:', self.env) # dbg
66 67 # print('*** CMD:', self.cmd) # dbg
67 68 env = os.environ.copy()
68 69 env.update(self.env)
69 70 output = subprocess.PIPE if self.buffer_output else None
70 71 stdout = subprocess.STDOUT if self.buffer_output else None
71 72 self.process = subprocess.Popen(self.cmd, stdout=output,
72 73 stderr=stdout, env=env)
73 74
74 75 def wait(self):
75 76 self.stdout, _ = self.process.communicate()
76 77 return self.process.returncode
77 78
78 79 def cleanup_process(self):
79 80 """Cleanup on exit by killing any leftover processes."""
80 81 subp = self.process
81 82 if subp is None or (subp.poll() is not None):
82 83 return # Process doesn't exist, or is already dead.
83 84
84 85 try:
85 86 print('Cleaning up stale PID: %d' % subp.pid)
86 87 subp.kill()
87 88 except: # (OSError, WindowsError) ?
88 89 # This is just a best effort, if we fail or the process was
89 90 # really gone, ignore it.
90 91 pass
91 92 else:
92 93 for i in range(10):
93 94 if subp.poll() is None:
94 95 time.sleep(0.1)
95 96 else:
96 97 break
97 98
98 99 if subp.poll() is None:
99 100 # The process did not die...
100 101 print('... failed. Manual cleanup may be required.')
101 102
102 103 def cleanup(self):
103 104 "Kill process if it's still alive, and clean up temporary directories"
104 105 self.cleanup_process()
105 106 for td in self.dirs:
106 107 td.cleanup()
107 108
108 109 __del__ = cleanup
109 110
110 111 class PyTestController(TestController):
111 112 """Run Python tests using IPython.testing.iptest"""
112 113 #: str, Python command to execute in subprocess
113 114 pycmd = None
114 115
115 116 def __init__(self, section):
116 117 """Create new test runner."""
117 118 TestController.__init__(self)
118 119 self.section = section
119 120 # pycmd is put into cmd[2] in PyTestController.launch()
120 121 self.cmd = [sys.executable, '-c', None, section]
121 122 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
122 123 ipydir = TemporaryDirectory()
123 124 self.dirs.append(ipydir)
124 125 self.env['IPYTHONDIR'] = ipydir.name
125 126 self.workingdir = workingdir = TemporaryDirectory()
126 127 self.dirs.append(workingdir)
127 128 self.env['IPTEST_WORKING_DIR'] = workingdir.name
128 129 # This means we won't get odd effects from our own matplotlib config
129 130 self.env['MPLCONFIGDIR'] = workingdir.name
130 131
131 132 @property
132 133 def will_run(self):
133 134 try:
134 135 return test_sections[self.section].will_run
135 136 except KeyError:
136 137 return True
137 138
138 139 def add_xunit(self):
139 140 xunit_file = os.path.abspath(self.section + '.xunit.xml')
140 141 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
141 142
142 143 def add_coverage(self):
143 144 try:
144 145 sources = test_sections[self.section].includes
145 146 except KeyError:
146 147 sources = ['IPython']
147 148
148 149 coverage_rc = ("[run]\n"
149 150 "data_file = {data_file}\n"
150 151 "source =\n"
151 152 " {source}\n"
152 153 ).format(data_file=os.path.abspath('.coverage.'+self.section),
153 154 source="\n ".join(sources))
154 155 config_file = os.path.join(self.workingdir.name, '.coveragerc')
155 156 with open(config_file, 'w') as f:
156 157 f.write(coverage_rc)
157 158
158 159 self.env['COVERAGE_PROCESS_START'] = config_file
159 160 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
160 161
161 162 def launch(self):
162 163 self.cmd[2] = self.pycmd
163 164 super(PyTestController, self).launch()
164 165
165 166
166 167 def prepare_py_test_controllers(inc_slow=False):
167 168 """Returns an ordered list of PyTestController instances to be run."""
168 169 to_run, not_run = [], []
169 170 if not inc_slow:
170 171 test_sections['parallel'].enabled = False
171 172
172 173 for name in test_group_names:
173 174 controller = PyTestController(name)
174 175 if controller.will_run:
175 176 to_run.append(controller)
176 177 else:
177 178 not_run.append(controller)
178 179 return to_run, not_run
179 180
180 181 def configure_controllers(controllers, xunit=False, coverage=False, extra_args=()):
181 182 """Apply options for a collection of TestController objects."""
182 183 for controller in controllers:
183 184 if xunit:
184 185 controller.add_xunit()
185 186 if coverage:
186 187 controller.add_coverage()
187 188 controller.cmd.extend(extra_args)
188 189
189 190 def do_run(controller):
190 191 try:
191 192 try:
192 193 controller.launch()
193 194 except Exception:
194 195 import traceback
195 196 traceback.print_exc()
196 197 return controller, 1 # signal failure
197 198
198 199 exitcode = controller.wait()
199 200 return controller, exitcode
200 201
201 202 except KeyboardInterrupt:
202 203 return controller, -signal.SIGINT
203 204 finally:
204 205 controller.cleanup()
205 206
206 207 def report():
207 208 """Return a string with a summary report of test-related variables."""
208
209 out = [ brief_sys_info(), '\n']
209 inf = get_sys_info()
210 out = []
211 def _add(name, value):
212 out.append((name, value))
213
214 _add('IPython version', inf['ipython_version'])
215 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
216 _add('IPython package', compress_user(inf['ipython_path']))
217 _add('Python version', inf['sys_version'].replace('\n',''))
218 _add('sys.executable', compress_user(inf['sys_executable']))
219 _add('Platform', inf['platform'])
220
221 width = max(len(n) for (n,v) in out)
222 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
210 223
211 224 avail = []
212 225 not_avail = []
213 226
214 227 for k, is_avail in have.items():
215 228 if is_avail:
216 229 avail.append(k)
217 230 else:
218 231 not_avail.append(k)
219 232
220 233 if avail:
221 234 out.append('\nTools and libraries available at test time:\n')
222 235 avail.sort()
223 236 out.append(' ' + ' '.join(avail)+'\n')
224 237
225 238 if not_avail:
226 239 out.append('\nTools and libraries NOT available at test time:\n')
227 240 not_avail.sort()
228 241 out.append(' ' + ' '.join(not_avail)+'\n')
229 242
230 243 return ''.join(out)
231 244
232 245 def run_iptestall(options):
233 246 """Run the entire IPython test suite by calling nose and trial.
234 247
235 248 This function constructs :class:`IPTester` instances for all IPython
236 249 modules and package and then runs each of them. This causes the modules
237 250 and packages of IPython to be tested each in their own subprocess using
238 251 nose.
239 252
240 253 Parameters
241 254 ----------
242 255
243 256 All parameters are passed as attributes of the options object.
244 257
245 258 testgroups : list of str
246 259 Run only these sections of the test suite. If empty, run all the available
247 260 sections.
248 261
249 262 fast : int or None
250 263 Run the test suite in parallel, using n simultaneous processes. If None
251 264 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
252 265
253 266 inc_slow : bool
254 267 Include slow tests, like IPython.parallel. By default, these tests aren't
255 268 run.
256 269
257 270 xunit : bool
258 271 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
259 272
260 273 coverage : bool or str
261 274 Measure code coverage from tests. True will store the raw coverage data,
262 275 or pass 'html' or 'xml' to get reports.
263 276
264 277 extra_args : list
265 278 Extra arguments to pass to the test subprocesses, e.g. '-v'
266 279 """
267 280 if options.fast != 1:
268 281 # If running in parallel, capture output so it doesn't get interleaved
269 282 TestController.buffer_output = True
270 283
271 284 if options.testgroups:
272 285 to_run = [PyTestController(name) for name in options.testgroups]
273 286 not_run = []
274 287 else:
275 288 to_run, not_run = prepare_py_test_controllers(inc_slow=options.all)
276 289
277 290 configure_controllers(to_run, xunit=options.xunit, coverage=options.coverage,
278 291 extra_args=options.extra_args)
279 292
280 293 def justify(ltext, rtext, width=70, fill='-'):
281 294 ltext += ' '
282 295 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
283 296 return ltext + rtext
284 297
285 298 # Run all test runners, tracking execution time
286 299 failed = []
287 300 t_start = time.time()
288 301
289 302 print()
290 303 if options.fast == 1:
291 304 # This actually means sequential, i.e. with 1 job
292 305 for controller in to_run:
293 306 print('IPython test group:', controller.section)
294 307 sys.stdout.flush() # Show in correct order when output is piped
295 308 controller, res = do_run(controller)
296 309 if res:
297 310 failed.append(controller)
298 311 if res == -signal.SIGINT:
299 312 print("Interrupted")
300 313 break
301 314 print()
302 315
303 316 else:
304 317 # Run tests concurrently
305 318 try:
306 319 pool = multiprocessing.pool.ThreadPool(options.fast)
307 320 for (controller, res) in pool.imap_unordered(do_run, to_run):
308 321 res_string = 'OK' if res == 0 else 'FAILED'
309 322 print(justify('IPython test group: ' + controller.section, res_string))
310 323 if res:
311 324 print(bytes_to_str(controller.stdout))
312 325 failed.append(controller)
313 326 if res == -signal.SIGINT:
314 327 print("Interrupted")
315 328 break
316 329 except KeyboardInterrupt:
317 330 return
318 331
319 332 for controller in not_run:
320 333 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
321 334
322 335 t_end = time.time()
323 336 t_tests = t_end - t_start
324 337 nrunners = len(to_run)
325 338 nfail = len(failed)
326 339 # summarize results
327 340 print('_'*70)
328 341 print('Test suite completed for system with the following information:')
329 342 print(report())
330 343 took = "Took %.3fs." % t_tests
331 344 print('Status: ', end='')
332 345 if not failed:
333 346 print('OK (%d test groups).' % nrunners, took)
334 347 else:
335 348 # If anything went wrong, point out what command to rerun manually to
336 349 # see the actual errors and individual summary
337 350 failed_sections = [c.section for c in failed]
338 351 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
339 352 nrunners, ', '.join(failed_sections)), took)
340 353 print()
341 354 print('You may wish to rerun these, with:')
342 355 print(' iptest', *failed_sections)
343 356 print()
344 357
345 358 if options.coverage:
346 359 from coverage import coverage
347 360 cov = coverage(data_file='.coverage')
348 361 cov.combine()
349 362 cov.save()
350 363
351 364 # Coverage HTML report
352 365 if options.coverage == 'html':
353 366 html_dir = 'ipy_htmlcov'
354 367 shutil.rmtree(html_dir, ignore_errors=True)
355 368 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
356 369 sys.stdout.flush()
357 370
358 371 # Custom HTML reporter to clean up module names.
359 372 from coverage.html import HtmlReporter
360 373 class CustomHtmlReporter(HtmlReporter):
361 374 def find_code_units(self, morfs):
362 375 super(CustomHtmlReporter, self).find_code_units(morfs)
363 376 for cu in self.code_units:
364 377 nameparts = cu.name.split(os.sep)
365 378 if 'IPython' not in nameparts:
366 379 continue
367 380 ix = nameparts.index('IPython')
368 381 cu.name = '.'.join(nameparts[ix:])
369 382
370 383 # Reimplement the html_report method with our custom reporter
371 384 cov._harvest_data()
372 385 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
373 386 html_title='IPython test coverage',
374 387 )
375 388 reporter = CustomHtmlReporter(cov, cov.config)
376 389 reporter.report(None)
377 390 print('done.')
378 391
379 392 # Coverage XML report
380 393 elif options.coverage == 'xml':
381 394 cov.xml_report(outfile='ipy_coverage.xml')
382 395
383 396 if failed:
384 397 # Ensure that our exit code indicates failure
385 398 sys.exit(1)
386 399
387 400
388 401 def main():
389 402 # Arguments after -- should be passed through to nose. Argparse treats
390 403 # everything after -- as regular positional arguments, so we separate them
391 404 # first.
392 405 try:
393 406 ix = sys.argv.index('--')
394 407 except ValueError:
395 408 to_parse = sys.argv[1:]
396 409 extra_args = []
397 410 else:
398 411 to_parse = sys.argv[1:ix]
399 412 extra_args = sys.argv[ix+1:]
400 413
401 414 parser = argparse.ArgumentParser(description='Run IPython test suite')
402 415 parser.add_argument('testgroups', nargs='*',
403 416 help='Run specified groups of tests. If omitted, run '
404 417 'all tests.')
405 418 parser.add_argument('--all', action='store_true',
406 419 help='Include slow tests not run by default.')
407 420 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
408 421 help='Run test sections in parallel.')
409 422 parser.add_argument('--xunit', action='store_true',
410 423 help='Produce Xunit XML results')
411 424 parser.add_argument('--coverage', nargs='?', const=True, default=False,
412 425 help="Measure test coverage. Specify 'html' or "
413 426 "'xml' to get reports.")
414 427
415 428 options = parser.parse_args(to_parse)
416 429 options.extra_args = extra_args
417 430
418 431 run_iptestall(options)
419 432
420 433
421 434 if __name__ == '__main__':
422 435 main()
@@ -1,564 +1,571
1 1 # encoding: utf-8
2 2 """
3 3 Utilities for path handling.
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2008-2011 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import os
18 18 import sys
19 19 import errno
20 20 import shutil
21 21 import random
22 22 import tempfile
23 23 import warnings
24 24 from hashlib import md5
25 25 import glob
26 26
27 27 import IPython
28 28 from IPython.testing.skipdoctest import skip_doctest
29 29 from IPython.utils.process import system
30 30 from IPython.utils.importstring import import_item
31 31 from IPython.utils import py3compat
32 32 #-----------------------------------------------------------------------------
33 33 # Code
34 34 #-----------------------------------------------------------------------------
35 35
36 36 fs_encoding = sys.getfilesystemencoding()
37 37
38 38 def _get_long_path_name(path):
39 39 """Dummy no-op."""
40 40 return path
41 41
42 42 def _writable_dir(path):
43 43 """Whether `path` is a directory, to which the user has write access."""
44 44 return os.path.isdir(path) and os.access(path, os.W_OK)
45 45
46 46 if sys.platform == 'win32':
47 47 @skip_doctest
48 48 def _get_long_path_name(path):
49 49 """Get a long path name (expand ~) on Windows using ctypes.
50 50
51 51 Examples
52 52 --------
53 53
54 54 >>> get_long_path_name('c:\\docume~1')
55 55 u'c:\\\\Documents and Settings'
56 56
57 57 """
58 58 try:
59 59 import ctypes
60 60 except ImportError:
61 61 raise ImportError('you need to have ctypes installed for this to work')
62 62 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
63 63 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p,
64 64 ctypes.c_uint ]
65 65
66 66 buf = ctypes.create_unicode_buffer(260)
67 67 rv = _GetLongPathName(path, buf, 260)
68 68 if rv == 0 or rv > 260:
69 69 return path
70 70 else:
71 71 return buf.value
72 72
73 73
74 74 def get_long_path_name(path):
75 75 """Expand a path into its long form.
76 76
77 77 On Windows this expands any ~ in the paths. On other platforms, it is
78 78 a null operation.
79 79 """
80 80 return _get_long_path_name(path)
81 81
82 82
83 83 def unquote_filename(name, win32=(sys.platform=='win32')):
84 84 """ On Windows, remove leading and trailing quotes from filenames.
85 85 """
86 86 if win32:
87 87 if name.startswith(("'", '"')) and name.endswith(("'", '"')):
88 88 name = name[1:-1]
89 89 return name
90 90
91 def compress_user(path):
92 """Reverse of :func:`os.path.expanduser`
93 """
94 home = os.path.expanduser('~')
95 if path.startswith(home):
96 path = "~" + path[len(home):]
97 return path
91 98
92 99 def get_py_filename(name, force_win32=None):
93 100 """Return a valid python filename in the current directory.
94 101
95 102 If the given name is not a file, it adds '.py' and searches again.
96 103 Raises IOError with an informative message if the file isn't found.
97 104
98 105 On Windows, apply Windows semantics to the filename. In particular, remove
99 106 any quoting that has been applied to it. This option can be forced for
100 107 testing purposes.
101 108 """
102 109
103 110 name = os.path.expanduser(name)
104 111 if force_win32 is None:
105 112 win32 = (sys.platform == 'win32')
106 113 else:
107 114 win32 = force_win32
108 115 name = unquote_filename(name, win32=win32)
109 116 if not os.path.isfile(name) and not name.endswith('.py'):
110 117 name += '.py'
111 118 if os.path.isfile(name):
112 119 return name
113 120 else:
114 121 raise IOError('File `%r` not found.' % name)
115 122
116 123
117 124 def filefind(filename, path_dirs=None):
118 125 """Find a file by looking through a sequence of paths.
119 126
120 127 This iterates through a sequence of paths looking for a file and returns
121 128 the full, absolute path of the first occurence of the file. If no set of
122 129 path dirs is given, the filename is tested as is, after running through
123 130 :func:`expandvars` and :func:`expanduser`. Thus a simple call::
124 131
125 132 filefind('myfile.txt')
126 133
127 134 will find the file in the current working dir, but::
128 135
129 136 filefind('~/myfile.txt')
130 137
131 138 Will find the file in the users home directory. This function does not
132 139 automatically try any paths, such as the cwd or the user's home directory.
133 140
134 141 Parameters
135 142 ----------
136 143 filename : str
137 144 The filename to look for.
138 145 path_dirs : str, None or sequence of str
139 146 The sequence of paths to look for the file in. If None, the filename
140 147 need to be absolute or be in the cwd. If a string, the string is
141 148 put into a sequence and the searched. If a sequence, walk through
142 149 each element and join with ``filename``, calling :func:`expandvars`
143 150 and :func:`expanduser` before testing for existence.
144 151
145 152 Returns
146 153 -------
147 154 Raises :exc:`IOError` or returns absolute path to file.
148 155 """
149 156
150 157 # If paths are quoted, abspath gets confused, strip them...
151 158 filename = filename.strip('"').strip("'")
152 159 # If the input is an absolute path, just check it exists
153 160 if os.path.isabs(filename) and os.path.isfile(filename):
154 161 return filename
155 162
156 163 if path_dirs is None:
157 164 path_dirs = ("",)
158 165 elif isinstance(path_dirs, basestring):
159 166 path_dirs = (path_dirs,)
160 167
161 168 for path in path_dirs:
162 169 if path == '.': path = os.getcwdu()
163 170 testname = expand_path(os.path.join(path, filename))
164 171 if os.path.isfile(testname):
165 172 return os.path.abspath(testname)
166 173
167 174 raise IOError("File %r does not exist in any of the search paths: %r" %
168 175 (filename, path_dirs) )
169 176
170 177
171 178 class HomeDirError(Exception):
172 179 pass
173 180
174 181
175 182 def get_home_dir(require_writable=False):
176 183 """Return the 'home' directory, as a unicode string.
177 184
178 185 Uses os.path.expanduser('~'), and checks for writability.
179 186
180 187 See stdlib docs for how this is determined.
181 188 $HOME is first priority on *ALL* platforms.
182 189
183 190 Parameters
184 191 ----------
185 192
186 193 require_writable : bool [default: False]
187 194 if True:
188 195 guarantees the return value is a writable directory, otherwise
189 196 raises HomeDirError
190 197 if False:
191 198 The path is resolved, but it is not guaranteed to exist or be writable.
192 199 """
193 200
194 201 homedir = os.path.expanduser('~')
195 202 # Next line will make things work even when /home/ is a symlink to
196 203 # /usr/home as it is on FreeBSD, for example
197 204 homedir = os.path.realpath(homedir)
198 205
199 206 if not _writable_dir(homedir) and os.name == 'nt':
200 207 # expanduser failed, use the registry to get the 'My Documents' folder.
201 208 try:
202 209 import _winreg as wreg
203 210 key = wreg.OpenKey(
204 211 wreg.HKEY_CURRENT_USER,
205 212 "Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
206 213 )
207 214 homedir = wreg.QueryValueEx(key,'Personal')[0]
208 215 key.Close()
209 216 except:
210 217 pass
211 218
212 219 if (not require_writable) or _writable_dir(homedir):
213 220 return py3compat.cast_unicode(homedir, fs_encoding)
214 221 else:
215 222 raise HomeDirError('%s is not a writable dir, '
216 223 'set $HOME environment variable to override' % homedir)
217 224
218 225 def get_xdg_dir():
219 226 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None.
220 227
221 228 This is only for non-OS X posix (Linux,Unix,etc.) systems.
222 229 """
223 230
224 231 env = os.environ
225 232
226 233 if os.name == 'posix' and sys.platform != 'darwin':
227 234 # Linux, Unix, AIX, etc.
228 235 # use ~/.config if empty OR not set
229 236 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
230 237 if xdg and _writable_dir(xdg):
231 238 return py3compat.cast_unicode(xdg, fs_encoding)
232 239
233 240 return None
234 241
235 242
236 243 def get_xdg_cache_dir():
237 244 """Return the XDG_CACHE_HOME, if it is defined and exists, else None.
238 245
239 246 This is only for non-OS X posix (Linux,Unix,etc.) systems.
240 247 """
241 248
242 249 env = os.environ
243 250
244 251 if os.name == 'posix' and sys.platform != 'darwin':
245 252 # Linux, Unix, AIX, etc.
246 253 # use ~/.cache if empty OR not set
247 254 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache')
248 255 if xdg and _writable_dir(xdg):
249 256 return py3compat.cast_unicode(xdg, fs_encoding)
250 257
251 258 return None
252 259
253 260
254 261 def get_ipython_dir():
255 262 """Get the IPython directory for this platform and user.
256 263
257 264 This uses the logic in `get_home_dir` to find the home directory
258 265 and then adds .ipython to the end of the path.
259 266 """
260 267
261 268 env = os.environ
262 269 pjoin = os.path.join
263 270
264 271
265 272 ipdir_def = '.ipython'
266 273 xdg_def = 'ipython'
267 274
268 275 home_dir = get_home_dir()
269 276 xdg_dir = get_xdg_dir()
270 277
271 278 # import pdb; pdb.set_trace() # dbg
272 279 if 'IPYTHON_DIR' in env:
273 280 warnings.warn('The environment variable IPYTHON_DIR is deprecated. '
274 281 'Please use IPYTHONDIR instead.')
275 282 ipdir = env.get('IPYTHONDIR', env.get('IPYTHON_DIR', None))
276 283 if ipdir is None:
277 284 # not set explicitly, use XDG_CONFIG_HOME or HOME
278 285 home_ipdir = pjoin(home_dir, ipdir_def)
279 286 if xdg_dir:
280 287 # use XDG, as long as the user isn't already
281 288 # using $HOME/.ipython and *not* XDG/ipython
282 289
283 290 xdg_ipdir = pjoin(xdg_dir, xdg_def)
284 291
285 292 if _writable_dir(xdg_ipdir) or not _writable_dir(home_ipdir):
286 293 ipdir = xdg_ipdir
287 294
288 295 if ipdir is None:
289 296 # not using XDG
290 297 ipdir = home_ipdir
291 298
292 299 ipdir = os.path.normpath(os.path.expanduser(ipdir))
293 300
294 301 if os.path.exists(ipdir) and not _writable_dir(ipdir):
295 302 # ipdir exists, but is not writable
296 303 warnings.warn("IPython dir '%s' is not a writable location,"
297 304 " using a temp directory."%ipdir)
298 305 ipdir = tempfile.mkdtemp()
299 306 elif not os.path.exists(ipdir):
300 307 parent = os.path.dirname(ipdir)
301 308 if not _writable_dir(parent):
302 309 # ipdir does not exist and parent isn't writable
303 310 warnings.warn("IPython parent '%s' is not a writable location,"
304 311 " using a temp directory."%parent)
305 312 ipdir = tempfile.mkdtemp()
306 313
307 314 return py3compat.cast_unicode(ipdir, fs_encoding)
308 315
309 316
310 317 def get_ipython_cache_dir():
311 318 """Get the cache directory it is created if it does not exist."""
312 319 xdgdir = get_xdg_cache_dir()
313 320 if xdgdir is None:
314 321 return get_ipython_dir()
315 322 ipdir = os.path.join(xdgdir, "ipython")
316 323 if not os.path.exists(ipdir) and _writable_dir(xdgdir):
317 324 os.makedirs(ipdir)
318 325 elif not _writable_dir(xdgdir):
319 326 return get_ipython_dir()
320 327
321 328 return py3compat.cast_unicode(ipdir, fs_encoding)
322 329
323 330
324 331 def get_ipython_package_dir():
325 332 """Get the base directory where IPython itself is installed."""
326 333 ipdir = os.path.dirname(IPython.__file__)
327 334 return py3compat.cast_unicode(ipdir, fs_encoding)
328 335
329 336
330 337 def get_ipython_module_path(module_str):
331 338 """Find the path to an IPython module in this version of IPython.
332 339
333 340 This will always find the version of the module that is in this importable
334 341 IPython package. This will always return the path to the ``.py``
335 342 version of the module.
336 343 """
337 344 if module_str == 'IPython':
338 345 return os.path.join(get_ipython_package_dir(), '__init__.py')
339 346 mod = import_item(module_str)
340 347 the_path = mod.__file__.replace('.pyc', '.py')
341 348 the_path = the_path.replace('.pyo', '.py')
342 349 return py3compat.cast_unicode(the_path, fs_encoding)
343 350
344 351 def locate_profile(profile='default'):
345 352 """Find the path to the folder associated with a given profile.
346 353
347 354 I.e. find $IPYTHONDIR/profile_whatever.
348 355 """
349 356 from IPython.core.profiledir import ProfileDir, ProfileDirError
350 357 try:
351 358 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
352 359 except ProfileDirError:
353 360 # IOError makes more sense when people are expecting a path
354 361 raise IOError("Couldn't find profile %r" % profile)
355 362 return pd.location
356 363
357 364 def expand_path(s):
358 365 """Expand $VARS and ~names in a string, like a shell
359 366
360 367 :Examples:
361 368
362 369 In [2]: os.environ['FOO']='test'
363 370
364 371 In [3]: expand_path('variable FOO is $FOO')
365 372 Out[3]: 'variable FOO is test'
366 373 """
367 374 # This is a pretty subtle hack. When expand user is given a UNC path
368 375 # on Windows (\\server\share$\%username%), os.path.expandvars, removes
369 376 # the $ to get (\\server\share\%username%). I think it considered $
370 377 # alone an empty var. But, we need the $ to remains there (it indicates
371 378 # a hidden share).
372 379 if os.name=='nt':
373 380 s = s.replace('$\\', 'IPYTHON_TEMP')
374 381 s = os.path.expandvars(os.path.expanduser(s))
375 382 if os.name=='nt':
376 383 s = s.replace('IPYTHON_TEMP', '$\\')
377 384 return s
378 385
379 386
380 387 def unescape_glob(string):
381 388 """Unescape glob pattern in `string`."""
382 389 def unescape(s):
383 390 for pattern in '*[]!?':
384 391 s = s.replace(r'\{0}'.format(pattern), pattern)
385 392 return s
386 393 return '\\'.join(map(unescape, string.split('\\\\')))
387 394
388 395
389 396 def shellglob(args):
390 397 """
391 398 Do glob expansion for each element in `args` and return a flattened list.
392 399
393 400 Unmatched glob pattern will remain as-is in the returned list.
394 401
395 402 """
396 403 expanded = []
397 404 # Do not unescape backslash in Windows as it is interpreted as
398 405 # path separator:
399 406 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x
400 407 for a in args:
401 408 expanded.extend(glob.glob(a) or [unescape(a)])
402 409 return expanded
403 410
404 411
405 412 def target_outdated(target,deps):
406 413 """Determine whether a target is out of date.
407 414
408 415 target_outdated(target,deps) -> 1/0
409 416
410 417 deps: list of filenames which MUST exist.
411 418 target: single filename which may or may not exist.
412 419
413 420 If target doesn't exist or is older than any file listed in deps, return
414 421 true, otherwise return false.
415 422 """
416 423 try:
417 424 target_time = os.path.getmtime(target)
418 425 except os.error:
419 426 return 1
420 427 for dep in deps:
421 428 dep_time = os.path.getmtime(dep)
422 429 if dep_time > target_time:
423 430 #print "For target",target,"Dep failed:",dep # dbg
424 431 #print "times (dep,tar):",dep_time,target_time # dbg
425 432 return 1
426 433 return 0
427 434
428 435
429 436 def target_update(target,deps,cmd):
430 437 """Update a target with a given command given a list of dependencies.
431 438
432 439 target_update(target,deps,cmd) -> runs cmd if target is outdated.
433 440
434 441 This is just a wrapper around target_outdated() which calls the given
435 442 command if target is outdated."""
436 443
437 444 if target_outdated(target,deps):
438 445 system(cmd)
439 446
440 447 def filehash(path):
441 448 """Make an MD5 hash of a file, ignoring any differences in line
442 449 ending characters."""
443 450 with open(path, "rU") as f:
444 451 return md5(py3compat.str_to_bytes(f.read())).hexdigest()
445 452
446 453 # If the config is unmodified from the default, we'll just delete it.
447 454 # These are consistent for 0.10.x, thankfully. We're not going to worry about
448 455 # older versions.
449 456 old_config_md5 = {'ipy_user_conf.py': 'fc108bedff4b9a00f91fa0a5999140d3',
450 457 'ipythonrc': '12a68954f3403eea2eec09dc8fe5a9b5'}
451 458
452 459 def check_for_old_config(ipython_dir=None):
453 460 """Check for old config files, and present a warning if they exist.
454 461
455 462 A link to the docs of the new config is included in the message.
456 463
457 464 This should mitigate confusion with the transition to the new
458 465 config system in 0.11.
459 466 """
460 467 if ipython_dir is None:
461 468 ipython_dir = get_ipython_dir()
462 469
463 470 old_configs = ['ipy_user_conf.py', 'ipythonrc', 'ipython_config.py']
464 471 warned = False
465 472 for cfg in old_configs:
466 473 f = os.path.join(ipython_dir, cfg)
467 474 if os.path.exists(f):
468 475 if filehash(f) == old_config_md5.get(cfg, ''):
469 476 os.unlink(f)
470 477 else:
471 478 warnings.warn("Found old IPython config file %r (modified by user)"%f)
472 479 warned = True
473 480
474 481 if warned:
475 482 warnings.warn("""
476 483 The IPython configuration system has changed as of 0.11, and these files will
477 484 be ignored. See http://ipython.github.com/ipython-doc/dev/config for details
478 485 of the new config system.
479 486 To start configuring IPython, do `ipython profile create`, and edit
480 487 `ipython_config.py` in <ipython_dir>/profile_default.
481 488 If you need to leave the old config files in place for an older version of
482 489 IPython and want to suppress this warning message, set
483 490 `c.InteractiveShellApp.ignore_old_config=True` in the new config.""")
484 491
485 492 def get_security_file(filename, profile='default'):
486 493 """Return the absolute path of a security file given by filename and profile
487 494
488 495 This allows users and developers to find security files without
489 496 knowledge of the IPython directory structure. The search path
490 497 will be ['.', profile.security_dir]
491 498
492 499 Parameters
493 500 ----------
494 501
495 502 filename : str
496 503 The file to be found. If it is passed as an absolute path, it will
497 504 simply be returned.
498 505 profile : str [default: 'default']
499 506 The name of the profile to search. Leaving this unspecified
500 507 The file to be found. If it is passed as an absolute path, fname will
501 508 simply be returned.
502 509
503 510 Returns
504 511 -------
505 512 Raises :exc:`IOError` if file not found or returns absolute path to file.
506 513 """
507 514 # import here, because profiledir also imports from utils.path
508 515 from IPython.core.profiledir import ProfileDir
509 516 try:
510 517 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
511 518 except Exception:
512 519 # will raise ProfileDirError if no such profile
513 520 raise IOError("Profile %r not found")
514 521 return filefind(filename, ['.', pd.security_dir])
515 522
516 523
517 524 ENOLINK = 1998
518 525
519 526 def link(src, dst):
520 527 """Hard links ``src`` to ``dst``, returning 0 or errno.
521 528
522 529 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
523 530 supported by the operating system.
524 531 """
525 532
526 533 if not hasattr(os, "link"):
527 534 return ENOLINK
528 535 link_errno = 0
529 536 try:
530 537 os.link(src, dst)
531 538 except OSError as e:
532 539 link_errno = e.errno
533 540 return link_errno
534 541
535 542
536 543 def link_or_copy(src, dst):
537 544 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
538 545
539 546 Attempts to maintain the semantics of ``shutil.copy``.
540 547
541 548 Because ``os.link`` does not overwrite files, a unique temporary file
542 549 will be used if the target already exists, then that file will be moved
543 550 into place.
544 551 """
545 552
546 553 if os.path.isdir(dst):
547 554 dst = os.path.join(dst, os.path.basename(src))
548 555
549 556 link_errno = link(src, dst)
550 557 if link_errno == errno.EEXIST:
551 558 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
552 559 try:
553 560 link_or_copy(src, new_dst)
554 561 except:
555 562 try:
556 563 os.remove(new_dst)
557 564 except OSError:
558 565 pass
559 566 raise
560 567 os.rename(new_dst, dst)
561 568 elif link_errno != 0:
562 569 # Either link isn't supported, or the filesystem doesn't support
563 570 # linking, or 'src' and 'dst' are on different filesystems.
564 571 shutil.copy(src, dst)
@@ -1,197 +1,171
1 1 # encoding: utf-8
2 2 """
3 3 Utilities for getting information about IPython and the system it's running in.
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2008-2011 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import os
18 18 import platform
19 19 import pprint
20 20 import sys
21 21 import subprocess
22 22
23 23 from IPython.core import release
24 24 from IPython.utils import py3compat, _sysinfo, encoding
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Code
28 28 #-----------------------------------------------------------------------------
29 29
30 30 def pkg_commit_hash(pkg_path):
31 31 """Get short form of commit hash given directory `pkg_path`
32 32
33 33 We get the commit hash from (in order of preference):
34 34
35 35 * IPython.utils._sysinfo.commit
36 36 * git output, if we are in a git repository
37 37
38 38 If these fail, we return a not-found placeholder tuple
39 39
40 40 Parameters
41 41 ----------
42 42 pkg_path : str
43 43 directory containing package
44 44 only used for getting commit from active repo
45 45
46 46 Returns
47 47 -------
48 48 hash_from : str
49 49 Where we got the hash from - description
50 50 hash_str : str
51 51 short form of hash
52 52 """
53 53 # Try and get commit from written commit text file
54 54 if _sysinfo.commit:
55 55 return "installation", _sysinfo.commit
56 56
57 57 # maybe we are in a repository
58 58 proc = subprocess.Popen('git rev-parse --short HEAD',
59 59 stdout=subprocess.PIPE,
60 60 stderr=subprocess.PIPE,
61 61 cwd=pkg_path, shell=True)
62 62 repo_commit, _ = proc.communicate()
63 63 if repo_commit:
64 64 return 'repository', repo_commit.strip()
65 65 return '(none found)', '<not found>'
66 66
67 67
68 68 def pkg_info(pkg_path):
69 69 """Return dict describing the context of this package
70 70
71 71 Parameters
72 72 ----------
73 73 pkg_path : str
74 74 path containing __init__.py for package
75 75
76 76 Returns
77 77 -------
78 78 context : dict
79 79 with named parameters of interest
80 80 """
81 81 src, hsh = pkg_commit_hash(pkg_path)
82 82 return dict(
83 83 ipython_version=release.version,
84 84 ipython_path=pkg_path,
85 85 codename=release.codename,
86 86 commit_source=src,
87 87 commit_hash=hsh,
88 88 sys_version=sys.version,
89 89 sys_executable=sys.executable,
90 90 sys_platform=sys.platform,
91 91 platform=platform.platform(),
92 92 os_name=os.name,
93 93 default_encoding=encoding.DEFAULT_ENCODING,
94 94 )
95 95
96 def get_sys_info():
97 """Return useful information about IPython and the system, as a dict."""
98 p = os.path
99 path = p.dirname(p.abspath(p.join(__file__, '..')))
100 return pkg_info(path)
96 101
97 102 @py3compat.doctest_refactor_print
98 103 def sys_info():
99 104 """Return useful information about IPython and the system, as a string.
100 105
101 106 Examples
102 107 --------
103 108 ::
104 109
105 110 In [2]: print sys_info()
106 111 {'commit_hash': '144fdae', # random
107 112 'commit_source': 'repository',
108 113 'ipython_path': '/home/fperez/usr/lib/python2.6/site-packages/IPython',
109 114 'ipython_version': '0.11.dev',
110 115 'os_name': 'posix',
111 116 'platform': 'Linux-2.6.35-22-generic-i686-with-Ubuntu-10.10-maverick',
112 117 'sys_executable': '/usr/bin/python',
113 118 'sys_platform': 'linux2',
114 119 'sys_version': '2.6.6 (r266:84292, Sep 15 2010, 15:52:39) \\n[GCC 4.4.5]'}
115 """
116 p = os.path
117 path = p.dirname(p.abspath(p.join(__file__, '..')))
118 return pprint.pformat(pkg_info(path))
119
120 def _compress_user(path):
121 """Reverse of :func:`os.path.expanduser`
122 120 """
123 home = os.path.expanduser('~')
124 if path.startswith(home):
125 path = "~" + path[len(home):]
126 return path
127
128 def brief_sys_info():
129 """Return summary information about IPython and the system, as a string.
130 """
131 p = os.path
132 path = p.dirname(p.abspath(p.join(__file__, '..')))
133 inf = pkg_info(path)
134 out = []
135 def _add(name, value):
136 out.append((name, value))
137
138 _add('IPython version', inf['ipython_version'])
139 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
140 _add('IPython package', _compress_user(inf['ipython_path']))
141 _add('Python version', inf['sys_version'].replace('\n',''))
142 _add('sys.executable', _compress_user(inf['sys_executable']))
143 _add('Platform', inf['platform'])
144
145 width = max(len(n) for (n,v) in out)
146 return '\n'.join("{:<{width}}: {}".format(n, v, width=width) for (n,v) in out)
147
121 return pprint.pformat(get_sys_info())
148 122
149 123 def _num_cpus_unix():
150 124 """Return the number of active CPUs on a Unix system."""
151 125 return os.sysconf("SC_NPROCESSORS_ONLN")
152 126
153 127
154 128 def _num_cpus_darwin():
155 129 """Return the number of active CPUs on a Darwin system."""
156 130 p = subprocess.Popen(['sysctl','-n','hw.ncpu'],stdout=subprocess.PIPE)
157 131 return p.stdout.read()
158 132
159 133
160 134 def _num_cpus_windows():
161 135 """Return the number of active CPUs on a Windows system."""
162 136 return os.environ.get("NUMBER_OF_PROCESSORS")
163 137
164 138
165 139 def num_cpus():
166 140 """Return the effective number of CPUs in the system as an integer.
167 141
168 142 This cross-platform function makes an attempt at finding the total number of
169 143 available CPUs in the system, as returned by various underlying system and
170 144 python calls.
171 145
172 146 If it can't find a sensible answer, it returns 1 (though an error *may* make
173 147 it return a large positive number that's actually incorrect).
174 148 """
175 149
176 150 # Many thanks to the Parallel Python project (http://www.parallelpython.com)
177 151 # for the names of the keys we needed to look up for this function. This
178 152 # code was inspired by their equivalent function.
179 153
180 154 ncpufuncs = {'Linux':_num_cpus_unix,
181 155 'Darwin':_num_cpus_darwin,
182 156 'Windows':_num_cpus_windows,
183 157 # On Vista, python < 2.5.2 has a bug and returns 'Microsoft'
184 158 # See http://bugs.python.org/issue1082 for details.
185 159 'Microsoft':_num_cpus_windows,
186 160 }
187 161
188 162 ncpufunc = ncpufuncs.get(platform.system(),
189 163 # default to unix version (Solaris, AIX, etc)
190 164 _num_cpus_unix)
191 165
192 166 try:
193 167 ncpus = max(1,int(ncpufunc()))
194 168 except:
195 169 ncpus = 1
196 170 return ncpus
197 171
General Comments 0
You need to be logged in to leave comments. Login now