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