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