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