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