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