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