swc-installation-test-2.py: Link directly to install docs
[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                 close_fds=True, shell=False, 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                 return string
442         raise NotImplementedError(self.version_stream)
443
444     def _get_version(self):
445         version_stream = self._get_version_stream()
446         match = self.version_regexp.search(version_stream)
447         if not match:
448             raise DependencyError(
449                 checker=self,
450                 message='no version string in output:\n{0}'.format(
451                     version_stream))
452         return match.group(1)
453
454
455 for command,long_name,minimum_version in [
456         ('sh', 'Bourne Shell', None),
457         ('ash', 'Almquist Shell', None),
458         ('bash', 'Bourne Again Shell', None),
459         ('csh', 'C Shell', None),
460         ('ksh', 'KornShell', None),
461         ('dash', 'Debian Almquist Shell', None),
462         ('tcsh', 'TENEX C Shell', None),
463         ('zsh', 'Z Shell', None),
464         ('git', 'Git', (1, 7, 0)),
465         ('hg', 'Mercurial', (2, 0, 0)),
466         ('EasyMercurial', None, (1, 3)),
467         ('pip', None, None),
468         ('sqlite3', 'SQLite 3', None),
469         ('nosetests', 'Nose', (1, 0, 0)),
470         ('ipython', 'IPython script', (0, 13)),
471         ('emacs', 'Emacs', None),
472         ('xemacs', 'XEmacs', None),
473         ('vim', 'Vim', None),
474         ('vi', None, None),
475         ('nano', 'Nano', None),
476         ('gedit', None, None),
477         ('kate', 'Kate', None),
478         ('notepad++', 'Notepad++', None),
479         ('firefox', 'Firefox', None),
480         ('google-chrome', 'Google Chrome', None),
481         ('chromium', 'Chromium', None),
482         ]:
483     if not long_name:
484         long_name = command
485     CHECKER[command] = CommandDependency(
486         command=command, long_name=long_name, minimum_version=minimum_version)
487 del command, long_name, minimum_version  # cleanup namespace
488
489
490 class MakeDependency (CommandDependency):
491     makefile = '\n'.join([
492             'all:',
493             '\t@echo "MAKE_VERSION=$(MAKE_VERSION)"',
494             '\t@echo "MAKE=$(MAKE)"',
495             '',
496             ])
497
498     def _get_version(self):
499         try:
500             return super(MakeDependency, self)._get_version()
501         except DependencyError as e:
502             version_options = self.version_options
503             self.version_options = ['-f', '-']
504             try:
505                 stream = self._get_version_stream(stdin=self.makefile)
506                 info = {}
507                 for line in stream.splitlines():
508                     try:
509                         key,value = line.split('=', 1)
510                     except ValueError as ve:
511                         raise e# from NotImplementedError(stream)
512                     info[key] = value
513                 if info.get('MAKE_VERSION', None):
514                     return info['MAKE_VERSION']
515                 elif info.get('MAKE', None):
516                     return None
517                 raise e
518             finally:
519                 self.version_options = version_options
520
521
522 CHECKER['make'] = MakeDependency(command='make', minimum_version=None)
523
524
525 class EasyInstallDependency (CommandDependency):
526     def _get_version(self):
527         try:
528             return super(EasyInstallDependency, self)._get_version()
529         except DependencyError as e:
530             version_stream = self.version_stream
531             try:
532                 self.version_stream = 'stderr'
533                 stream = self._get_version_stream(expect=(1,))
534                 if 'option --version not recognized' in stream:
535                     return 'unknown (possibly Setuptools?)'
536             finally:
537                 self.version_stream = version_stream
538
539
540 CHECKER['easy_install'] = EasyInstallDependency(
541     command='easy_install', long_name='Setuptools easy_install',
542     minimum_version=None)
543
544
545 class PathCommandDependency (CommandDependency):
546     """A command that doesn't support --version or equivalent options
547
548     On some operating systems (e.g. OS X), a command's executable may
549     be hard to find, or not exist in the PATH.  Work around that by
550     just checking for the existence of a characteristic file or
551     directory.  Since the characteristic path may depend on OS,
552     installed version, etc., take a list of paths, and succeed if any
553     of them exists.
554     """
555     def __init__(self, paths, **kwargs):
556         super(PathCommandDependency, self).__init__(self, **kwargs)
557         self.paths = paths
558
559     def _get_version_stream(self, *args, **kwargs):
560         raise NotImplementedError()
561
562     def _get_version(self):
563         for path in self.paths:
564             if _os.path.exists(path):
565                 return None
566         raise DependencyError(
567             checker=self,
568             message=(
569                 'nothing exists at any of the expected paths for {0}:\n    {1}'
570                 ).format(
571                 self.full_name(),
572                 '\n    '.join(p for p in self.paths)))
573
574
575 for paths,name,long_name in [
576         ([_os.path.join(_os.sep, 'Applications', 'Sublime Text 2.app')],
577          'sublime-text', 'Sublime Text'),
578         ([_os.path.join(_os.sep, 'Applications', 'TextMate.app')],
579          'textmate', 'TextMate'),
580         ([_os.path.join(_os.sep, 'Applications', 'TextWrangler.app')],
581          'textwrangler', 'TextWrangler'),
582         ([_os.path.join(_os.sep, 'Applications', 'Xcode.app'),  # OS X >=1.7
583           _os.path.join(_os.sep, 'Developer', 'Applications', 'Xcode.app'
584                         )  # OS X 1.6,
585           ],
586          'xcode', 'Xcode'),
587         ]:
588     if not long_name:
589         long_name = name
590     CHECKER[name] = PathCommandDependency(
591         paths=paths, name=name, long_name=long_name)
592 del paths, name, long_name  # cleanup namespace
593
594
595 class PythonPackageDependency (Dependency):
596     def __init__(self, package, **kwargs):
597         if 'name' not in kwargs:
598             kwargs['name'] = package
599         if 'and_dependencies' not in kwargs:
600             kwargs['and_dependencies'] = []
601         if 'python' not in kwargs['and_dependencies']:
602             kwargs['and_dependencies'].append('python')
603         super(PythonPackageDependency, self).__init__(**kwargs)
604         self.package = package
605
606     def _get_version(self):
607         package = self._get_package(self.package)
608         return self._get_version_from_package(package)
609
610     def _get_package(self, package):
611         try:
612             return _importlib.import_module(package)
613         except ImportError as e:
614             raise DependencyError(
615                 checker=self,
616                 message="could not import the '{0}' package for {1}".format(
617                     package, self.full_name()),
618                 )# from e
619
620     def _get_version_from_package(self, package):
621         try:
622             version = package.__version__
623         except AttributeError:
624             version = None
625         return version
626
627
628 for package,name,long_name,minimum_version,and_dependencies in [
629         ('nose', None, 'Nose Python package',
630          CHECKER['nosetests'].minimum_version, None),
631         ('jinja2', 'jinja', 'Jinja', (2, 6), None),
632         ('zmq', 'pyzmq', 'PyZMQ', (2, 1, 4), None),
633         ('IPython', None, 'IPython Python package',
634          CHECKER['ipython'].minimum_version, ['jinja', 'tornado', 'pyzmq']),
635         ('argparse', None, 'Argparse', None, None),
636         ('numpy', None, 'NumPy', None, None),
637         ('scipy', None, 'SciPy', None, None),
638         ('matplotlib', None, 'Matplotlib', None, None),
639         ('pandas', None, 'Pandas', (0, 8), None),
640         ('sympy', None, 'SymPy', None, None),
641         ('Cython', None, None, None, None),
642         ('networkx', None, 'NetworkX', None, None),
643         ('mayavi.mlab', None, 'MayaVi', None, None),
644         ('setuptools', None, 'Setuptools', None, None),
645         ]:
646     if not name:
647         name = package
648     if not long_name:
649         long_name = name
650     kwargs = {}
651     if and_dependencies:
652         kwargs['and_dependencies'] = and_dependencies
653     CHECKER[name] = PythonPackageDependency(
654         package=package, name=name, long_name=long_name,
655         minimum_version=minimum_version, **kwargs)
656 # cleanup namespace
657 del package, name, long_name, minimum_version, and_dependencies, kwargs
658
659
660 class MercurialPythonPackage (PythonPackageDependency):
661     def _get_version(self):
662         try:  # mercurial >= 1.2
663             package = _importlib.import_module('mercurial.util')
664         except ImportError as e:  # mercurial <= 1.1.2
665             package = self._get_package('mercurial.version')
666             return package.get_version()
667         else:
668             return package.version()
669
670
671 CHECKER['mercurial'] = MercurialPythonPackage(
672     package='mercurial.util', name='mercurial',
673     long_name='Mercurial Python package',
674     minimum_version=CHECKER['hg'].minimum_version)
675
676
677 class TornadoPythonPackage (PythonPackageDependency):
678     def _get_version_from_package(self, package):
679         return package.version
680
681     def _get_parsed_version(self):
682         package = self._get_package(self.package)
683         return package.version_info
684
685
686 CHECKER['tornado'] = TornadoPythonPackage(
687     package='tornado', name='tornado', long_name='Tornado', minimum_version=(2, 0))
688
689
690 class SQLitePythonPackage (PythonPackageDependency):
691     def _get_version_from_package(self, package):
692         return _sys.version
693
694     def _get_parsed_version(self):
695         return _sys.version_info
696
697
698 CHECKER['sqlite3-python'] = SQLitePythonPackage(
699     package='sqlite3', name='sqlite3-python',
700     long_name='SQLite Python package',
701     minimum_version=CHECKER['sqlite3'].minimum_version)
702
703
704 class UserTaskDependency (Dependency):
705     "Prompt the user to complete a task and check for success"
706     def __init__(self, prompt, **kwargs):
707         super(UserTaskDependency, self).__init__(**kwargs)
708         self.prompt = prompt
709
710     def _check(self):
711         if _sys.version_info >= (3, ):
712             result = input(self.prompt)
713         else:  # Python 2.x
714             result = raw_input(self.prompt)
715         return self._check_result(result)
716
717     def _check_result(self, result):
718         raise NotImplementedError()
719
720
721 class EditorTaskDependency (UserTaskDependency):
722     def __init__(self, **kwargs):
723         self.path = _os.path.expanduser(_os.path.join(
724                 '~', 'swc-installation-test.txt'))
725         self.contents = 'Hello, world!'
726         super(EditorTaskDependency, self).__init__(
727             prompt=(
728                 'Open your favorite text editor and create the file\n'
729                 '  {0}\n'
730                 'containing the line:\n'
731                 '  {1}\n'
732                 'Press enter here after you have done this.\n'
733                 'You may remove the file after you have finished testing.'
734                 ).format(self.path, self.contents),
735             **kwargs)
736
737     def _check_result(self, result):
738         message = None
739         try:
740             with open(self.path, 'r') as f:
741                 contents = f.read()
742         except IOError as e:
743             raise DependencyError(
744                 checker=self,
745                 message='could not open {0!r}: {1}'.format(self.path, e)
746                 )# from e
747         if contents.strip() != self.contents:
748             raise DependencyError(
749                 checker=self,
750                 message=(
751                     'file contents ({0!r}) did not match the expected {1!r}'
752                     ).format(contents, self.contents))
753
754
755 CHECKER['other-editor'] = EditorTaskDependency(
756     name='other-editor', long_name='')
757
758
759 class VirtualDependency (Dependency):
760     def _check(self):
761         return '{0} {1}'.format(
762             self.or_pass['dependency'].full_name(),
763             self.or_pass['version'])
764
765
766 for name,long_name,dependencies in [
767         ('virtual-shell', 'command line shell', (
768             'bash',
769             'dash',
770             'ash',
771             'zsh',
772             'ksh',
773             'csh',
774             'tcsh',
775             'sh',
776             )),
777         ('virtual-editor', 'text/code editor', (
778             'emacs',
779             'xemacs',
780             'vim',
781             'vi',
782             'nano',
783             'gedit',
784             'kate',
785             'notepad++',
786             'sublime-text',
787             'textmate',
788             'textwrangler',
789             'other-editor',  # last because it requires user interaction
790             )),
791         ('virtual-browser', 'web browser', (
792             'firefox',
793             'google-chrome',
794             'chromium',
795             )),
796         ('virtual-pypi-installer', 'PyPI installer', (
797             'easy_install',
798             'pip',
799             )),
800         ]:
801     CHECKER[name] = VirtualDependency(
802         name=name, long_name=long_name, or_dependencies=dependencies)
803 del name, long_name, dependencies  # cleanup namespace
804
805
806 def _print_info(key, value, indent=19):
807     print('{0}{1}: {2}'.format(key, ' '*(indent-len(key)), value))
808
809 def print_system_info():
810     print("If you do not understand why the above failures occurred,")
811     print("copy and send the *entire* output (all info above and summary")
812     print("below) to the instructor for help.")
813     print()
814     print('==================')
815     print('System information')
816     print('==================')
817     _print_info('os.name', _os.name)
818     _print_info('os.uname', _platform.uname())
819     _print_info('platform', _sys.platform)
820     _print_info('platform+', _platform.platform())
821     for pversion in (
822             'linux_distribution',
823             'mac_ver',
824             'win32_ver',
825             ):
826         value = getattr(_platform, pversion)()
827         if value[0]:
828             _print_info(pversion, value)
829     _print_info('prefix', _sys.prefix)
830     _print_info('exec_prefix', _sys.exec_prefix)
831     _print_info('executable', _sys.executable)
832     _print_info('version_info', _sys.version_info)
833     _print_info('version', _sys.version)
834     _print_info('environment', '')
835     for key,value in sorted(_os.environ.items()):
836         print('  {0}={1}'.format(key, value))
837     print('==================')
838
839 def print_suggestions(instructor_fallback=True):
840     print()
841     print('For suggestions on installing missing packages, see')
842     print('http://software-carpentry.org/setup/')
843     print('')
844     print('For instructings on installing a particular package,')
845     print('see the failure message for that package printed above.')
846     if instructor_fallback:
847         print('')
848         print('For help, email the *entire* output of this script to')
849         print('your instructor.')
850
851
852 if __name__ == '__main__':
853     import optparse as _optparse
854
855     parser = _optparse.OptionParser(usage='%prog [options] [check...]')
856     epilog = __doc__
857     parser.format_epilog = lambda formatter: '\n' + epilog
858     parser.add_option(
859         '-v', '--verbose', action='store_true',
860         help=('print additional information to help troubleshoot '
861               'installation issues'))
862     options,args = parser.parse_args()
863     try:
864         passed = check(args)
865     except InvalidCheck as e:
866         print("I don't know how to check for {0!r}".format(e.check))
867         print('I do know how to check for:')
868         for key,checker in sorted(CHECKER.items()):
869             if checker.long_name != checker.name:
870                 print('  {0} {1}({2})'.format(
871                         key, ' '*(20-len(key)), checker.long_name))
872             else:
873                 print('  {0}'.format(key))
874         _sys.exit(1)
875     if not passed:
876         if options.verbose:
877             print()
878             print_system_info()
879             print_suggestions(instructor_fallback=True)
880         _sys.exit(1)