swc-installation-test-2.py: Add minimum browser requirements for IPython
[swc-setup-installation-test.git] / swc-installation-test-2.py
1 #!/usr/bin/env python
2
3 """Test script to check for required functionality.
4
5 Execute this code at the command line by typing:
6
7   python swc-installation-test-2.py
8
9 Run the script and follow the instructions it prints at the end.
10
11 This script requires at least Python 2.6.  You can check the version
12 of Python that you have installed with 'swc-installation-test-1.py'.
13
14 By default, this script will test for all the dependencies your
15 instructor thinks you need.  If you want to test for a different set
16 of packages, you can list them on the command line.  For example:
17
18   python swc-installation-test-2.py git virtual-editor
19
20 This is useful if the original test told you to install a more recent
21 version of a particular dependency, and you just want to re-test that
22 dependency.
23 """
24
25 from __future__ import print_function  # for Python 2.6 compatibility
26
27 import distutils.ccompiler as _distutils_ccompiler
28 import fnmatch as _fnmatch
29 try:  # Python 2.7 and 3.x
30     import importlib as _importlib
31 except ImportError:  # Python 2.6 and earlier
32     class _Importlib (object):
33         """Minimal workarounds for functions we need
34         """
35         @staticmethod
36         def import_module(name):
37             module = __import__(name)
38             for n in name.split('.')[1:]:
39                 module = getattr(module, n)
40             return module
41     _importlib = _Importlib()
42 import logging as _logging
43 import os as _os
44 import platform as _platform
45 import re as _re
46 import shlex as _shlex
47 import subprocess as _subprocess
48 import sys as _sys
49 try:  # Python 3.x
50     import urllib.parse as _urllib_parse
51 except ImportError:  # Python 2.x
52     import urllib as _urllib_parse  # for quote()
53
54
55 if not hasattr(_shlex, 'quote'):  # Python versions older than 3.3
56     # Use the undocumented pipes.quote()
57     import pipes as _pipes
58     _shlex.quote = _pipes.quote
59
60
61 __version__ = '0.1'
62
63 # Comment out any entries you don't need
64 CHECKS = [
65 # Shell
66     'virtual-shell',
67 # Editors
68     'virtual-editor',
69 # Browsers
70     'virtual-browser',
71 # Version control
72     'git',
73     'hg',              # Command line tool
74     #'mercurial',       # Python package
75     'EasyMercurial',
76 # Build tools and packaging
77     'make',
78     'virtual-pypi-installer',
79     'setuptools',
80     #'xcode',
81 # Testing
82     'nosetests',       # Command line tool
83     'nose',            # Python package
84     'py.test',         # Command line tool
85     'pytest',          # Python package
86 # SQL
87     'sqlite3',         # Command line tool
88     'sqlite3-python',  # Python package
89 # Python
90     'python',
91     'ipython',         # Command line tool
92     'IPython',         # Python package
93     'argparse',        # Useful for utility scripts
94     'numpy',
95     'scipy',
96     'matplotlib',
97     'pandas',
98     'sympy',
99     'Cython',
100     'networkx',
101     'mayavi.mlab',
102     ]
103
104 CHECKER = {}
105
106 _ROOT_PATH = _os.sep
107 if _platform.system() == 'win32':
108     _ROOT_PATH = 'c:\\'
109
110
111 class InvalidCheck (KeyError):
112     def __init__(self, check):
113         super(InvalidCheck, self).__init__(check)
114         self.check = check
115
116     def __str__(self):
117         return self.check
118
119
120 class DependencyError (Exception):
121     _default_url = 'http://software-carpentry.org/setup/'
122     _setup_urls = {  # (system, version, package) glob pairs
123         ('*', '*', 'Cython'): 'http://docs.cython.org/src/quickstart/install.html',
124         ('Linux', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-linux',
125         ('Darwin', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-mac',
126         ('Windows', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-windows',
127         ('*', '*', 'EasyMercurial'): 'http://easyhg.org/download.html',
128         ('*', '*', 'argparse'): 'https://pypi.python.org/pypi/argparse#installation',
129         ('*', '*', 'ash'): 'http://www.in-ulm.de/~mascheck/various/ash/',
130         ('*', '*', 'bash'): 'http://www.gnu.org/software/bash/manual/html_node/Basic-Installation.html#Basic-Installation',
131         ('Linux', '*', 'chromium'): 'http://code.google.com/p/chromium/wiki/LinuxBuildInstructions',
132         ('Darwin', '*', 'chromium'): 'http://code.google.com/p/chromium/wiki/MacBuildInstructions',
133         ('Windows', '*', 'chromium'): 'http://www.chromium.org/developers/how-tos/build-instructions-windows',
134         ('*', '*', 'chromium'): 'http://www.chromium.org/developers/how-tos',
135         ('Windows', '*', 'emacs'): 'http://www.gnu.org/software/emacs/windows/Installing-Emacs.html',
136         ('*', '*', 'emacs'): 'http://www.gnu.org/software/emacs/#Obtaining',
137         ('*', '*', 'firefox'): 'http://www.mozilla.org/en-US/firefox/new/',
138         ('Linux', '*', 'gedit'): 'http://www.linuxfromscratch.org/blfs/view/svn/gnome/gedit.html',
139         ('*', '*', 'git'): 'http://git-scm.com/downloads',
140         ('*', '*', 'google-chrome'): 'https://www.google.com/intl/en/chrome/browser/',
141         ('*', '*', 'hg'): 'http://mercurial.selenic.com/',
142         ('*', '*', 'mercurial'): 'http://mercurial.selenic.com/',
143         ('*', '*', 'IPython'): 'http://ipython.org/install.html',
144         ('*', '*', 'ipython'): 'http://ipython.org/install.html',
145         ('*', '*', 'jinja'): 'http://jinja.pocoo.org/docs/intro/#installation',
146         ('*', '*', 'kate'): 'http://kate-editor.org/get-it/',
147         ('*', '*', 'make'): 'http://www.gnu.org/software/make/',
148         ('Darwin', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#building-on-osx',
149         ('Windows', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#installing-on-windows',
150         ('*', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#installing',
151         ('*', '*', 'mayavi.mlab'): 'http://docs.enthought.com/mayavi/mayavi/installation.html',
152         ('*', '*', 'nano'): 'http://www.nano-editor.org/dist/latest/faq.html#3',
153         ('*', '*', 'networkx'): 'http://networkx.github.com/documentation/latest/install.html#installing',
154         ('*', '*', 'nose'): 'https://nose.readthedocs.org/en/latest/#installation-and-quick-start',
155         ('*', '*', 'nosetests'): 'https://nose.readthedocs.org/en/latest/#installation-and-quick-start',
156         ('*', '*', 'notepad++'): 'http://notepad-plus-plus.org/download/v6.3.html',
157         ('*', '*', 'numpy'): 'http://docs.scipy.org/doc/numpy/user/install.html',
158         ('*', '*', 'pandas'): 'http://pandas.pydata.org/pandas-docs/stable/install.html',
159         ('*', '*', 'pip'): 'http://www.pip-installer.org/en/latest/installing.html',
160         ('*', '*', 'pytest'): 'http://pytest.org/latest/getting-started.html',
161         ('*', '*', 'python'): 'http://www.python.org/download/releases/2.7.3/#download',
162         ('*', '*', 'pyzmq'): 'https://github.com/zeromq/pyzmq/wiki/Building-and-Installing-PyZMQ',
163         ('*', '*', 'py.test'): 'http://pytest.org/latest/getting-started.html',
164         ('Linux', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Linux',
165         ('Darwin', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Mac_OS_X',
166         ('Windows', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Windows',
167         ('*', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy',
168         ('*', '*', 'setuptools'): 'https://pypi.python.org/pypi/setuptools#installation-instructions',
169         ('*', '*', 'sqlite3'): 'http://www.sqlite.org/download.html',
170         ('*', '*', 'sublime-text'): 'http://www.sublimetext.com/2',
171         ('*', '*', 'sympy'): 'http://docs.sympy.org/dev/install.html',
172         ('Darwin', '*', 'textmate'): 'http://macromates.com/',
173         ('Darwin', '*', 'textwrangler'): 'http://www.barebones.com/products/textwrangler/download.html',
174         ('*', '*', 'tornado'): 'http://www.tornadoweb.org/',
175         ('*', '*', 'vim'): 'http://www.vim.org/download.php',
176         ('Darwin', '*', 'xcode'): 'https://developer.apple.com/xcode/',
177         ('*', '*', 'xemacs'): 'http://www.us.xemacs.org/Install/',
178         ('*', '*', 'zsh'): 'http://www.zsh.org/',
179         }
180
181     def _get_message(self):
182         return self._message
183     def _set_message(self, message):
184         self._message = message
185     message = property(_get_message, _set_message)
186
187     def __init__(self, checker, message, causes=None):
188         super(DependencyError, self).__init__(message)
189         self.checker = checker
190         self.message = message
191         if causes is None:
192             causes = []
193         self.causes = causes
194
195     def get_url(self):
196         system = _platform.system()
197         version = None
198         for pversion in (
199             'linux_distribution',
200             'mac_ver',
201             'win32_ver',
202             ):
203             value = getattr(_platform, pversion)()
204             if value[0]:
205                 version = value[0]
206                 break
207         package = self.checker.name
208         for (s,v,p),url in self._setup_urls.items():
209             if (_fnmatch.fnmatch(system, s) and
210                     _fnmatch.fnmatch(version, v) and
211                     _fnmatch.fnmatch(package, p)):
212                 return url
213         return self._default_url
214
215     def __str__(self):
216         url = self.get_url()
217         lines = [
218             'check for {0} failed:'.format(self.checker.full_name()),
219             '  ' + self.message,
220             '  For instructions on installing an up-to-date version, see',
221             '  ' + url,
222             ]
223         if self.causes:
224             lines.append('  causes:')
225             for cause in self.causes:
226                 lines.extend('  ' + line for line in str(cause).splitlines())
227         return '\n'.join(lines)
228
229
230 def check(checks=None):
231     successes = []
232     failures = []
233     if not checks:
234         checks = CHECKS
235     for check in checks:
236         try:
237             checker = CHECKER[check]
238         except KeyError as e:
239             raise InvalidCheck(check)# from e
240         _sys.stdout.write('check {0}...\t'.format(checker.full_name()))
241         try:
242             version = checker.check()
243         except DependencyError as e:
244             failures.append(e)
245             _sys.stdout.write('fail\n')
246         else:
247             _sys.stdout.write('pass\n')
248             successes.append((checker, version))
249     if successes:
250         print('\nSuccesses:\n')
251         for checker,version in successes:
252             print('{0} {1}'.format(
253                     checker.full_name(),
254                     version or 'unknown'))
255     if failures:
256         print('\nFailures:')
257         printed = []
258         for failure in failures:
259             if failure not in printed:
260                 print()
261                 print(failure)
262                 printed.append(failure)
263         return False
264     return True
265
266
267 class Dependency (object):
268     def __init__(self, name, long_name=None, minimum_version=None,
269                  version_delimiter='.', and_dependencies=None,
270                  or_dependencies=None):
271         self.name = name
272         self.long_name = long_name or name
273         self.minimum_version = minimum_version
274         self.version_delimiter = version_delimiter
275         if not and_dependencies:
276             and_dependencies = []
277         self.and_dependencies = and_dependencies
278         if not or_dependencies:
279             or_dependencies = []
280         self.or_dependencies = or_dependencies
281         self._check_error = None
282
283     def __str__(self):
284         return '<{0} {1}>'.format(type(self).__name__, self.name)
285
286     def full_name(self):
287         if self.name == self.long_name:
288             return self.name
289         else:
290             return '{0} ({1})'.format(self.long_name, self.name)
291
292     def check(self):
293         if self._check_error:
294             raise self._check_error
295         try:
296             self._check_dependencies()
297             return self._check()
298         except DependencyError as e:
299             self._check_error = e  # cache for future calls
300             raise
301
302     def _check_dependencies(self):
303         for dependency in self.and_dependencies:
304             if not hasattr(dependency, 'check'):
305                 dependency = CHECKER[dependency]
306             try:
307                 dependency.check()
308             except DependencyError as e:
309                 raise DependencyError(
310                     checker=self,
311                     message=(
312                         'some dependencies for {0} were not satisfied'
313                         ).format(self.full_name()),
314                     causes=[e])
315         self.or_pass = None
316         or_errors = []
317         for dependency in self.or_dependencies:
318             if not hasattr(dependency, 'check'):
319                 dependency = CHECKER[dependency]
320             try:
321                 version = dependency.check()
322             except DependencyError as e:
323                 or_errors.append(e)
324             else:
325                 self.or_pass = {
326                     'dependency': dependency,
327                     'version': version,
328                     }
329                 break  # no need to test other dependencies
330         if self.or_dependencies and not self.or_pass:
331             raise DependencyError(
332                 checker=self,
333                 message=(
334                     '{0} requires at least one of the following dependencies'
335                     ).format(self.full_name()),
336                     causes=or_errors)
337
338     def _check(self):
339         version = self._get_version()
340         parsed_version = None
341         if hasattr(self, '_get_parsed_version'):
342             parsed_version = self._get_parsed_version()
343         if self.minimum_version:
344             self._check_version(version=version, parsed_version=parsed_version)
345         return version
346
347     def _get_version(self):
348         raise NotImplementedError(self)
349
350     def _minimum_version_string(self):
351         return self.version_delimiter.join(
352             str(part) for part in self.minimum_version)
353
354     def _check_version(self, version, parsed_version=None):
355         if not parsed_version:
356             parsed_version = self._parse_version(version=version)
357         if not parsed_version or parsed_version < self.minimum_version:
358             raise DependencyError(
359                 checker=self,
360                 message='outdated version of {0}: {1} (need >= {2})'.format(
361                     self.full_name(), version, self._minimum_version_string()))
362
363     def _parse_version(self, version):
364         if not version:
365             return None
366         parsed_version = []
367         for part in version.split(self.version_delimiter):
368             try:
369                 parsed_version.append(int(part))
370             except ValueError as e:
371                 raise DependencyError(
372                     checker=self,
373                     message=(
374                         'unparsable {0!r} in version {1} of {2}, (need >= {3})'
375                         ).format(
376                         part, version, self.full_name(),
377                         self._minimum_version_string()))# from e
378         return tuple(parsed_version)
379
380
381 class VirtualDependency (Dependency):
382     def _check(self):
383         return '{0} {1}'.format(
384             self.or_pass['dependency'].full_name(),
385             self.or_pass['version'])
386
387
388 class CommandDependency (Dependency):
389     exe_extension = _distutils_ccompiler.new_compiler().exe_extension
390
391     def __init__(self, command, paths=None, version_options=('--version',),
392                  stdin=None, version_regexp=None, version_stream='stdout',
393                  **kwargs):
394         if 'name' not in kwargs:
395             kwargs['name'] = command
396         super(CommandDependency, self).__init__(**kwargs)
397         self.command = command
398         self.paths = paths
399         self.version_options = version_options
400         self.stdin = None
401         if not version_regexp:
402             regexp = r'([\d][\d{0}]*[\d])'.format(self.version_delimiter)
403             version_regexp = _re.compile(regexp)
404         self.version_regexp = version_regexp
405         self.version_stream = version_stream
406
407     def _get_command_version_stream(self, command=None, stdin=None,
408                                     expect=(0,)):
409         if command is None:
410             command = self.command + (self.exe_extension or '')
411         if not stdin:
412             stdin = self.stdin
413         if stdin:
414             popen_stdin = _subprocess.PIPE
415         else:
416             popen_stdin = None
417         try:
418             p = _subprocess.Popen(
419                 [command] + list(self.version_options), stdin=popen_stdin,
420                 stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
421                 universal_newlines=True)
422         except OSError as e:
423             raise DependencyError(
424                 checker=self,
425                 message="could not find '{0}' executable".format(command),
426                 )# from e
427         stdout,stderr = p.communicate(stdin)
428         status = p.wait()
429         if status not in expect:
430             lines = [
431                 "failed to execute: {0} {1}".format(
432                     command,
433                     ' '.join(_shlex.quote(arg)
434                              for arg in self.version_options)),
435                 'status: {0}'.format(status),
436                 ]
437             for name,string in [('stdout', stdout), ('stderr', stderr)]:
438                 if string:
439                     lines.extend([name + ':', string])
440             raise DependencyError(checker=self, message='\n'.join(lines))
441         for name,string in [('stdout', stdout), ('stderr', stderr)]:
442             if name == self.version_stream:
443                 if not string:
444                     raise DependencyError(
445                         checker=self,
446                         message='empty version stream on {0} for {1}'.format(
447                             self.version_stream, command))
448                 return string
449         raise NotImplementedError(self.version_stream)
450
451     def _get_version_stream(self, **kwargs):
452         paths = [self.command + (self.exe_extension or '')]
453         if self.exe_extension:
454             paths.append(self.command)  # also look at the extension-less path
455         if self.paths:
456             paths.extend(self.paths)
457         or_errors = []
458         for path in paths:
459             try:
460                 return self._get_command_version_stream(command=path, **kwargs)
461             except DependencyError as e:
462                 or_errors.append(e)
463         raise DependencyError(
464             checker=self,
465             message='errors finding {0} version'.format(
466                 self.full_name()),
467             causes=or_errors)
468
469     def _get_version(self):
470         version_stream = self._get_version_stream()
471         match = self.version_regexp.search(version_stream)
472         if not match:
473             raise DependencyError(
474                 checker=self,
475                 message='no version string in output:\n{0}'.format(
476                     version_stream))
477         return match.group(1)
478
479
480 class PathCommandDependency (CommandDependency):
481     """A command that doesn't support --version or equivalent options
482
483     On some operating systems (e.g. OS X), a command's executable may
484     be hard to find, or not exist in the PATH.  Work around that by
485     just checking for the existence of a characteristic file or
486     directory.  Since the characteristic path may depend on OS,
487     installed version, etc., take a list of paths, and succeed if any
488     of them exists.
489     """
490     def _get_command_version_stream(self, *args, **kwargs):
491         raise NotImplementedError()
492
493     def _get_version_stream(self, *args, **kwargs):
494         raise NotImplementedError()
495
496     def _get_version(self):
497         for path in self.paths:
498             if _os.path.exists(path):
499                 return None
500         raise DependencyError(
501             checker=self,
502             message=(
503                 'nothing exists at any of the expected paths for {0}:\n    {1}'
504                 ).format(
505                 self.full_name(),
506                 '\n    '.join(p for p in self.paths)))
507
508
509 class UserTaskDependency (Dependency):
510     "Prompt the user to complete a task and check for success"
511     def __init__(self, prompt, **kwargs):
512         super(UserTaskDependency, self).__init__(**kwargs)
513         self.prompt = prompt
514
515     def _check(self):
516         if _sys.version_info >= (3, ):
517             result = input(self.prompt)
518         else:  # Python 2.x
519             result = raw_input(self.prompt)
520         return self._check_result(result)
521
522     def _check_result(self, result):
523         raise NotImplementedError()
524
525
526 class EditorTaskDependency (UserTaskDependency):
527     def __init__(self, **kwargs):
528         self.path = _os.path.expanduser(_os.path.join(
529                 '~', 'swc-installation-test.txt'))
530         self.contents = 'Hello, world!'
531         super(EditorTaskDependency, self).__init__(
532             prompt=(
533                 'Open your favorite text editor and create the file\n'
534                 '  {0}\n'
535                 'containing the line:\n'
536                 '  {1}\n'
537                 'Press enter here after you have done this.\n'
538                 'You may remove the file after you have finished testing.'
539                 ).format(self.path, self.contents),
540             **kwargs)
541
542     def _check_result(self, result):
543         message = None
544         try:
545             with open(self.path, 'r') as f:
546                 contents = f.read()
547         except IOError as e:
548             raise DependencyError(
549                 checker=self,
550                 message='could not open {0!r}: {1}'.format(self.path, e)
551                 )# from e
552         if contents.strip() != self.contents:
553             raise DependencyError(
554                 checker=self,
555                 message=(
556                     'file contents ({0!r}) did not match the expected {1!r}'
557                     ).format(contents, self.contents))
558
559
560 class MakeDependency (CommandDependency):
561     makefile = '\n'.join([
562             'all:',
563             '\t@echo "MAKE_VERSION=$(MAKE_VERSION)"',
564             '\t@echo "MAKE=$(MAKE)"',
565             '',
566             ])
567
568     def _get_version(self):
569         try:
570             return super(MakeDependency, self)._get_version()
571         except DependencyError as e:
572             version_options = self.version_options
573             self.version_options = ['-f', '-']
574             try:
575                 stream = self._get_version_stream(stdin=self.makefile)
576                 info = {}
577                 for line in stream.splitlines():
578                     try:
579                         key,value = line.split('=', 1)
580                     except ValueError as ve:
581                         raise e# from NotImplementedError(stream)
582                     info[key] = value
583                 if info.get('MAKE_VERSION', None):
584                     return info['MAKE_VERSION']
585                 elif info.get('MAKE', None):
586                     return None
587                 raise e
588             finally:
589                 self.version_options = version_options
590
591
592 class EasyInstallDependency (CommandDependency):
593     def _get_version(self):
594         try:
595             return super(EasyInstallDependency, self)._get_version()
596         except DependencyError as e:
597             version_stream = self.version_stream
598             try:
599                 self.version_stream = 'stderr'
600                 stream = self._get_version_stream(expect=(1,))
601                 if 'option --version not recognized' in stream:
602                     return 'unknown (possibly Setuptools?)'
603             finally:
604                 self.version_stream = version_stream
605
606
607 class PythonDependency (Dependency):
608     def __init__(self, name='python', long_name='Python version',
609                  minimum_version=(2, 6), **kwargs):
610         super(PythonDependency, self).__init__(
611             name=name, long_name=long_name, minimum_version=minimum_version,
612             **kwargs)
613
614     def _get_version(self):
615         return _sys.version
616
617     def _get_parsed_version(self):
618         return _sys.version_info
619
620
621 class PythonPackageDependency (Dependency):
622     def __init__(self, package, **kwargs):
623         if 'name' not in kwargs:
624             kwargs['name'] = package
625         if 'and_dependencies' not in kwargs:
626             kwargs['and_dependencies'] = []
627         if 'python' not in kwargs['and_dependencies']:
628             kwargs['and_dependencies'].append('python')
629         super(PythonPackageDependency, self).__init__(**kwargs)
630         self.package = package
631
632     def _get_version(self):
633         package = self._get_package(self.package)
634         return self._get_version_from_package(package)
635
636     def _get_package(self, package):
637         try:
638             return _importlib.import_module(package)
639         except ImportError as e:
640             raise DependencyError(
641                 checker=self,
642                 message="could not import the '{0}' package for {1}".format(
643                     package, self.full_name()),
644                 )# from e
645
646     def _get_version_from_package(self, package):
647         try:
648             version = package.__version__
649         except AttributeError:
650             version = None
651         return version
652
653
654 class MercurialPythonPackage (PythonPackageDependency):
655     def _get_version(self):
656         try:  # mercurial >= 1.2
657             package = _importlib.import_module('mercurial.util')
658         except ImportError as e:  # mercurial <= 1.1.2
659             package = self._get_package('mercurial.version')
660             return package.get_version()
661         else:
662             return package.version()
663
664
665 class TornadoPythonPackage (PythonPackageDependency):
666     def _get_version_from_package(self, package):
667         return package.version
668
669     def _get_parsed_version(self):
670         package = self._get_package(self.package)
671         return package.version_info
672
673
674 class SQLitePythonPackage (PythonPackageDependency):
675     def _get_version_from_package(self, package):
676         return _sys.version
677
678     def _get_parsed_version(self):
679         return _sys.version_info
680
681
682 def _program_files_paths(*args):
683     "Utility for generating MS Windows search paths"
684     pf = _os.environ.get('ProgramFiles', '/usr/bin')
685     pfx86 = _os.environ.get('ProgramFiles(x86)', pf)
686     paths = [_os.path.join(pf, *args)]
687     if pfx86 != pf:
688         paths.append(_os.path.join(pfx86, *args))
689     return paths
690
691
692 CHECKER['python'] = PythonDependency()
693
694
695 for command,long_name,minimum_version,paths in [
696         ('sh', 'Bourne Shell', None, None),
697         ('ash', 'Almquist Shell', None, None),
698         ('bash', 'Bourne Again Shell', None, None),
699         ('csh', 'C Shell', None, None),
700         ('ksh', 'KornShell', None, None),
701         ('dash', 'Debian Almquist Shell', None, None),
702         ('tcsh', 'TENEX C Shell', None, None),
703         ('zsh', 'Z Shell', None, None),
704         ('git', 'Git', (1, 7, 0), None),
705         ('hg', 'Mercurial', (2, 0, 0), None),
706         ('EasyMercurial', None, (1, 3), None),
707         ('pip', None, None, None),
708         ('sqlite3', 'SQLite 3', None, None),
709         ('nosetests', 'Nose', (1, 0, 0), None),
710         ('ipython', 'IPython script', (1, 0), None),
711         ('emacs', 'Emacs', None, None),
712         ('xemacs', 'XEmacs', None, None),
713         ('vim', 'Vim', None, None),
714         ('vi', None, None, None),
715         ('nano', 'Nano', None, None),
716         ('gedit', None, None, None),
717         ('kate', 'Kate', None, None),
718         ('notepad++', 'Notepad++', None,
719          _program_files_paths('Notepad++', 'notepad++.exe')),
720         ('firefox', 'Firefox', None,
721          _program_files_paths('Mozilla Firefox', 'firefox.exe')),
722         ('google-chrome', 'Google Chrome', None,
723          _program_files_paths('Google', 'Chrome', 'Application', 'chrome.exe')
724          ),
725         ('chromium', 'Chromium', None, None),
726         ]:
727     if not long_name:
728         long_name = command
729     CHECKER[command] = CommandDependency(
730         command=command, paths=paths, long_name=long_name,
731         minimum_version=minimum_version)
732 del command, long_name, minimum_version, paths  # cleanup namespace
733
734
735 CHECKER['make'] = MakeDependency(command='make', minimum_version=None)
736
737
738 CHECKER['easy_install'] = EasyInstallDependency(
739     command='easy_install', long_name='Setuptools easy_install',
740     minimum_version=None)
741
742
743 CHECKER['py.test'] = CommandDependency(
744     command='py.test', version_stream='stderr',
745     minimum_version=None)
746
747
748 for paths,name,long_name in [
749         ([_os.path.join(_ROOT_PATH, 'Applications', 'Sublime Text 2.app')],
750          'sublime-text', 'Sublime Text'),
751         ([_os.path.join(_ROOT_PATH, 'Applications', 'TextMate.app')],
752          'textmate', 'TextMate'),
753         ([_os.path.join(_ROOT_PATH, 'Applications', 'TextWrangler.app')],
754          'textwrangler', 'TextWrangler'),
755         ([_os.path.join(_ROOT_PATH, 'Applications', 'Safari.app')],
756          'safari', 'Safari'),
757         ([_os.path.join(_ROOT_PATH, 'Applications', 'Xcode.app'),  # OS X >=1.7
758           _os.path.join(_ROOT_PATH, 'Developer', 'Applications', 'Xcode.app'
759                         )  # OS X 1.6,
760           ],
761          'xcode', 'Xcode'),
762         ]:
763     if not long_name:
764         long_name = name
765     CHECKER[name] = PathCommandDependency(
766         command=None, paths=paths, name=name, long_name=long_name)
767 del paths, name, long_name  # cleanup namespace
768
769
770 for package,name,long_name,minimum_version,and_dependencies in [
771         ('nose', None, 'Nose Python package',
772          CHECKER['nosetests'].minimum_version, None),
773         ('pytest', None, 'pytest Python package',
774          CHECKER['py.test'].minimum_version, None),
775         ('jinja2', 'jinja', 'Jinja', (2, 6), None),
776         ('zmq', 'pyzmq', 'PyZMQ', (2, 1, 4), None),
777         ('IPython', None, 'IPython Python package',
778          CHECKER['ipython'].minimum_version, [
779              'jinja',
780              'tornado',
781              'pyzmq',
782              VirtualDependency(
783                  name='virtual-browser-ipython',
784                  long_name='IPython-compatible web browser',
785                  or_dependencies=[
786                      CommandDependency(
787                          command=CHECKER['firefox'].command,
788                          paths=CHECKER['firefox'].paths,
789                          name='{0}-for-ipython'.format(
790                              CHECKER['firefox'].name),
791                          long_name='{0} for IPython'.format(
792                              CHECKER['firefox'].long_name),
793                          minimum_version=(6, 0)),
794                      CommandDependency(
795                          command=CHECKER['google-chrome'].command,
796                          paths=CHECKER['google-chrome'].paths,
797                          name='{0}-for-ipython'.format(
798                              CHECKER['google-chrome'].name),
799                          long_name='{0} for IPython'.format(
800                              CHECKER['google-chrome'].long_name),
801                          minimum_version=(13, 0)),
802                      CommandDependency(
803                          command=CHECKER['chromium'].command,
804                          paths=CHECKER['chromium'].paths,
805                          name='{0}-for-ipython'.format(
806                              CHECKER['chromium'].name),
807                          long_name='{0} for IPython'.format(
808                              CHECKER['chromium'].long_name),
809                          minimum_version=(13, 0)),
810                      PathCommandDependency(
811                          command=CHECKER['safari'].command,
812                          paths=CHECKER['safari'].paths,
813                          name='{0}-for-ipython'.format(
814                              CHECKER['safari'].name),
815                          long_name='{0} for IPython'.format(
816                              CHECKER['safari'].long_name),
817                          minimum_version=(5, 0)),
818                  ]),
819          ]),
820         ('argparse', None, 'Argparse', None, None),
821         ('numpy', None, 'NumPy', None, None),
822         ('scipy', None, 'SciPy', None, None),
823         ('matplotlib', None, 'Matplotlib', None, None),
824         ('pandas', None, 'Pandas', (0, 8), None),
825         ('sympy', None, 'SymPy', None, None),
826         ('Cython', None, None, None, None),
827         ('networkx', None, 'NetworkX', None, None),
828         ('mayavi.mlab', None, 'MayaVi', None, None),
829         ('setuptools', None, 'Setuptools', None, None),
830         ]:
831     if not name:
832         name = package
833     if not long_name:
834         long_name = name
835     kwargs = {}
836     if and_dependencies:
837         kwargs['and_dependencies'] = and_dependencies
838     CHECKER[name] = PythonPackageDependency(
839         package=package, name=name, long_name=long_name,
840         minimum_version=minimum_version, **kwargs)
841 # cleanup namespace
842 del package, name, long_name, minimum_version, and_dependencies, kwargs
843
844
845 CHECKER['mercurial'] = MercurialPythonPackage(
846     package='mercurial.util', name='mercurial',
847     long_name='Mercurial Python package',
848     minimum_version=CHECKER['hg'].minimum_version)
849
850
851 CHECKER['tornado'] = TornadoPythonPackage(
852     package='tornado', name='tornado', long_name='Tornado', minimum_version=(2, 0))
853
854
855 CHECKER['sqlite3-python'] = SQLitePythonPackage(
856     package='sqlite3', name='sqlite3-python',
857     long_name='SQLite Python package',
858     minimum_version=CHECKER['sqlite3'].minimum_version)
859
860
861 CHECKER['other-editor'] = EditorTaskDependency(
862     name='other-editor', long_name='')
863
864
865 for name,long_name,dependencies in [
866         ('virtual-shell', 'command line shell', (
867             'bash',
868             'dash',
869             'ash',
870             'zsh',
871             'ksh',
872             'csh',
873             'tcsh',
874             'sh',
875             )),
876         ('virtual-editor', 'text/code editor', (
877             'emacs',
878             'xemacs',
879             'vim',
880             'vi',
881             'nano',
882             'gedit',
883             'kate',
884             'notepad++',
885             'sublime-text',
886             'textmate',
887             'textwrangler',
888             'other-editor',  # last because it requires user interaction
889             )),
890         ('virtual-browser', 'web browser', (
891             'firefox',
892             'google-chrome',
893             'chromium',
894             'safari',
895             )),
896         ('virtual-pypi-installer', 'PyPI installer', (
897             'pip',
898             'easy_install',
899             )),
900         ]:
901     CHECKER[name] = VirtualDependency(
902         name=name, long_name=long_name, or_dependencies=dependencies)
903 del name, long_name, dependencies  # cleanup namespace
904
905
906 def _print_info(key, value, indent=19):
907     print('{0}{1}: {2}'.format(key, ' '*(indent-len(key)), value))
908
909 def print_system_info():
910     print("If you do not understand why the above failures occurred,")
911     print("copy and send the *entire* output (all info above and summary")
912     print("below) to the instructor for help.")
913     print()
914     print('==================')
915     print('System information')
916     print('==================')
917     _print_info('os.name', _os.name)
918     _print_info('os.uname', _platform.uname())
919     _print_info('platform', _sys.platform)
920     _print_info('platform+', _platform.platform())
921     for pversion in (
922             'linux_distribution',
923             'mac_ver',
924             'win32_ver',
925             ):
926         value = getattr(_platform, pversion)()
927         if value[0]:
928             _print_info(pversion, value)
929     _print_info('prefix', _sys.prefix)
930     _print_info('exec_prefix', _sys.exec_prefix)
931     _print_info('executable', _sys.executable)
932     _print_info('version_info', _sys.version_info)
933     _print_info('version', _sys.version)
934     _print_info('environment', '')
935     for key,value in sorted(_os.environ.items()):
936         print('  {0}={1}'.format(key, value))
937     print('==================')
938
939 def print_suggestions(instructor_fallback=True):
940     print()
941     print('For suggestions on installing missing packages, see')
942     print('http://software-carpentry.org/setup/')
943     print('')
944     print('For instructings on installing a particular package,')
945     print('see the failure message for that package printed above.')
946     if instructor_fallback:
947         print('')
948         print('For help, email the *entire* output of this script to')
949         print('your instructor.')
950
951
952 if __name__ == '__main__':
953     import optparse as _optparse
954
955     parser = _optparse.OptionParser(usage='%prog [options] [check...]')
956     epilog = __doc__
957     parser.format_epilog = lambda formatter: '\n' + epilog
958     parser.add_option(
959         '-v', '--verbose', action='store_true',
960         help=('print additional information to help troubleshoot '
961               'installation issues'))
962     options,args = parser.parse_args()
963     try:
964         passed = check(args)
965     except InvalidCheck as e:
966         print("I don't know how to check for {0!r}".format(e.check))
967         print('I do know how to check for:')
968         for key,checker in sorted(CHECKER.items()):
969             if checker.long_name != checker.name:
970                 print('  {0} {1}({2})'.format(
971                         key, ' '*(20-len(key)), checker.long_name))
972             else:
973                 print('  {0}'.format(key))
974         _sys.exit(1)
975     if not passed:
976         if options.verbose:
977             print()
978             print_system_info()
979             print_suggestions(instructor_fallback=True)
980         _sys.exit(1)