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