swc-installation-test-2.py: Sort shells in order of preference
[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
15 from __future__ import print_function  # for Python 2.6 compatibility
16
17 import distutils.ccompiler as _distutils_ccompiler
18 try:  # Python 2.7 and 3.x
19     import importlib as _importlib
20 except ImportError:  # Python 2.6 and earlier
21     class _Importlib (object):
22         """Minimal workarounds for functions we need
23         """
24         @staticmethod
25         def import_module(name):
26             module = __import__(name)
27             for n in name.split('.')[1:]:
28                 module = getattr(module, n)
29             return module
30     _importlib = _Importlib()
31 import logging as _logging
32 import os as _os
33 import platform as _platform
34 import re as _re
35 import subprocess as _subprocess
36 import sys as _sys
37
38
39 __version__ = '0.1'
40
41 # Comment out any entries you don't need
42 CHECKS = [
43 # Shell
44     'virtual-shell',
45 # Editors
46     'virtual-editor',
47 # Browsers
48     'virtual-browser',
49 # Version control
50     'git',
51     'hg',              # Command line tool
52     'mercurial',       # Python package
53 # Build tools and packaging
54     'make',
55     'easy_install',
56     'setuptools',
57 # Testing
58     'nosetests',       # Command line tool
59     'nose',            # Python package
60 # SQL
61     'sqlite3',         # Command line tool
62     'sqlite3-python',  # Python package
63 # Python
64     'python',
65     'IPython',
66     'numpy',
67     'scipy',
68     'matplotlib',
69     'sympy',
70     'Cython',
71     'networkx',
72     'mayavi.mlab',
73     ]
74
75 CHECKER = {}
76
77
78 class DependencyError (Exception):
79     def _get_message(self):
80         return self._message
81     def _set_message(self, message):
82         self._message = message
83     message = property(_get_message, _set_message)
84
85     def __init__(self, checker, message):
86         super(DependencyError, self).__init__(message)
87         self.checker = checker
88         self.message = message
89
90     def __str__(self):
91         url = 'http://software-carpentry.org/setup/'  # TODO: per-package URL
92         return 'check for {0} failed:\n{1}\n{2}\n{3}'.format(
93             self.checker.full_name(), self.message,
94             'For instructions on installing an up-to-date version, see',
95             url)
96
97
98 def check(checks=None):
99     successes = []
100     failures = []
101     if not checks:
102         checks = CHECKS
103     for check in checks:
104         checker = CHECKER[check]
105         _sys.stdout.write('check {0}...\t'.format(checker.full_name()))
106         try:
107             version = checker.check()
108         except DependencyError as e:
109             failures.append(e)
110             _sys.stdout.write('fail\n')
111         else:
112             _sys.stdout.write('pass\n')
113             successes.append((checker, version))
114     if successes:
115         print('\nSuccesses:\n')
116         for checker,version in successes:
117             print('{0} {1}'.format(
118                     checker.full_name(),
119                     version or 'unknown'))
120     if failures:
121         print('\nFailures:')
122         printed = []
123         for failure in failures:
124             if failure not in printed:
125                 print()
126                 print(failure)
127                 printed.append(failure)
128         return False
129     return True
130
131
132 class Dependency (object):
133     def __init__(self, name, long_name=None, minimum_version=None,
134                  version_delimiter='.', and_dependencies=None,
135                  or_dependencies=None):
136         self.name = name
137         self.long_name = long_name or name
138         self.minimum_version = minimum_version
139         self.version_delimiter = version_delimiter
140         if not and_dependencies:
141             and_dependencies = []
142         self.and_dependencies = and_dependencies
143         if not or_dependencies:
144             or_dependencies = []
145         self.or_dependencies = or_dependencies
146         self._check_error = None
147
148     def __str__(self):
149         return '<{0} {1}>'.format(type(self).__name__, self.name)
150
151     def full_name(self):
152         if self.name == self.long_name:
153             return self.name
154         else:
155             return '{0} ({1})'.format(self.long_name, self.name)
156
157     def check(self):
158         if self._check_error:
159             raise self._check_error
160         try:
161             self._check_dependencies()
162             return self._check()
163         except DependencyError as e:
164             self._check_error = e  # cache for future calls
165             raise
166
167     def _check_dependencies(self):
168         for dependency in self.and_dependencies:
169             if not hasattr(dependency, 'check'):
170                 dependency = CHECKER[dependency]
171             dependency.check()
172         self.or_pass = or_error = None
173         for dependency in self.or_dependencies:
174             if not hasattr(dependency, 'check'):
175                 dependency = CHECKER[dependency]
176             try:
177                 version = dependency.check()
178             except DependencyError as e:
179                 or_error = e
180             else:
181                 self.or_pass = {
182                     'dependency': dependency,
183                     'version': version,
184                     }
185                 break  # no need to test other dependencies
186         if self.or_dependencies and not self.or_pass:
187             raise or_error
188
189     def _check(self):
190         version = self._get_version()
191         parsed_version = None
192         if hasattr(self, '_get_parsed_version'):
193             parsed_version = self._get_parsed_version()
194         if self.minimum_version:
195             self._check_version(version=version, parsed_version=parsed_version)
196         return version
197
198     def _get_version(self):
199         raise NotImplementedError(self)
200
201     def _check_version(self, version, parsed_version=None):
202         if not parsed_version:
203             parsed_version = self._parse_version(version=version)
204         if not parsed_version or parsed_version < self.minimum_version:
205             raise DependencyError(
206                 checker=self,
207                 message='outdated version of {0}: {1} (need >= {2})'.format(
208                     self.full_name(), version,
209                     self.version_delimiter.join(
210                         str(part) for part in self.minimum_version)))
211
212     def _parse_version(self, version):
213         if not version:
214             return None
215         parsed_version = []
216         for part in version.split(self.version_delimiter):
217             try:
218                 parsed_version.append(int(part))
219             except ValueError as e:
220                 raise NotImplementedError((version, part))# from e
221         return tuple(parsed_version)
222
223
224 class PythonDependency (Dependency):
225     def __init__(self, name='python', long_name='Python version',
226                  minimum_version=(2, 6), **kwargs):
227         super(PythonDependency, self).__init__(
228             name=name, long_name=long_name, minimum_version=minimum_version,
229             **kwargs)
230
231     def _get_version(self):
232         return _sys.version
233
234     def _get_parsed_version(self):
235         return _sys.version_info
236
237
238 CHECKER['python'] = PythonDependency()
239
240
241 class CommandDependency (Dependency):
242     exe_extension = _distutils_ccompiler.new_compiler().exe_extension
243
244     def __init__(self, command, version_option='--version',
245                  version_regexp=None, version_stream='stdout', **kwargs):
246         if 'name' not in kwargs:
247             kwargs['name'] = command
248         super(CommandDependency, self).__init__(**kwargs)
249         self.command = command
250         self.version_option = version_option
251         if not version_regexp:
252             regexp = r'([\d][\d{0}]*[\d])'.format(self.version_delimiter)
253             version_regexp = _re.compile(regexp)
254         self.version_regexp = version_regexp
255         self.version_stream = version_stream
256
257     def _get_version_stream(self, expect=(0,)):
258         command = self.command + (self.exe_extension or '')
259         try:
260             p = _subprocess.Popen(
261                 [command, self.version_option],
262                 stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
263                 close_fds=True, shell=False, universal_newlines=True)
264         except OSError as e:
265             raise DependencyError(
266                 checker=self,
267                 message="could not find '{0}' executable".format(command),
268                 )# from e
269         stdout,stderr = p.communicate()
270         status = p.wait()
271         if status not in expect:
272             lines = [
273                 "failed to execute '{0} {1}':".format(
274                     command, self.version_option),
275                 'status: {0}'.format(status),
276                 ]
277             for name,string in [('stdout', stdout), ('stderr', stderr)]:
278                 if string:
279                     lines.extend([name + ':', string])
280             raise DependencyError(checker=self, message='\n'.join(lines))
281         for name,string in [('stdout', stdout), ('stderr', stderr)]:
282             if name == self.version_stream:
283                 return string
284         raise NotImplementedError(self.version_stream)
285
286     def _get_version(self):
287         version_stream = self._get_version_stream()
288         match = self.version_regexp.search(version_stream)
289         if not match:
290             raise DependencyError(
291                 checker=self,
292                 message='no version string in output:\n{0}'.format(
293                     version_stream))
294         return match.group(1)
295
296
297 for command,long_name,minimum_version in [
298         ('sh', 'Bourne Shell', None),
299         ('ash', 'Almquist Shell', None),
300         ('bash', 'Bourne Again Shell', None),
301         ('csh', 'C Shell', None),
302         ('ksh', 'KornShell', None),
303         ('dash', 'Debian Almquist Shell', None),
304         ('tcsh', 'TENEX C Shell', None),
305         ('zsh', 'Z Shell', None),
306         ('git', 'Git', (1, 7, 0)),
307         ('hg', 'Mercurial', (2, 0, 0)),
308         ('make', None, None),
309         ('sqlite3', 'SQLite 3', None),
310         ('nosetests', 'Nose', (1, 0, 0)),
311         ('emacs', 'Emacs', None),
312         ('xemacs', 'XEmacs', None),
313         ('vim', 'Vim', None),
314         ('vi', None, None),
315         ('nano', 'Nano', None),
316         ('kate', 'Kate', None),
317         ('notepad++', 'Notepad++', None),
318         ('firefox', 'Firefox', None),
319         ('google-chrome', 'Google Chrome', None),
320         ('chromium', 'Chromium', None),
321         ]:
322     if not long_name:
323         long_name = command
324     CHECKER[command] = CommandDependency(
325         command=command, long_name=long_name, minimum_version=minimum_version)
326 del command, long_name, minimum_version  # cleanup namespace
327
328
329 class EasyInstallDependency (CommandDependency):
330     def _get_version(self):
331         try:
332             return super(EasyInstallDependency, self)._get_version()
333         except DependencyError as e:
334             version_stream = self.version_stream
335             try:
336                 self.version_stream = 'stderr'
337                 stream = self._get_version_stream(expect=(1,))
338                 if 'option --version not recognized' in stream:
339                     return 'unknown (possibly Setuptools?)'
340             finally:
341                 self.version_stream = version_stream
342
343
344 CHECKER['easy_install'] = EasyInstallDependency(
345     command='easy_install', long_name='Setuptools easy_install',
346     minimum_version=None)
347
348
349 class PythonPackageDependency (Dependency):
350     def __init__(self, package, **kwargs):
351         if 'name' not in kwargs:
352             kwargs['name'] = package
353         if 'and_dependencies' not in kwargs:
354             kwargs['and_dependencies'] = []
355         if 'python' not in kwargs['and_dependencies']:
356             kwargs['and_dependencies'].append('python')
357         super(PythonPackageDependency, self).__init__(**kwargs)
358         self.package = package
359
360     def _get_version(self):
361         package = self._get_package(self.package)
362         return self._get_version_from_package(package)
363
364     def _get_package(self, package):
365         try:
366             return _importlib.import_module(package)
367         except ImportError as e:
368             raise DependencyError(
369                 checker=self,
370                 message="could not import the '{0}' package for {1}".format(
371                     package, self.full_name()),
372                 )# from e
373
374     def _get_version_from_package(self, package):
375         try:
376             version = package.__version__
377         except AttributeError:
378             version = None
379         return version
380
381
382 for package,name,long_name,minimum_version in [
383         ('nose', None, 'Nose Python package',
384          CHECKER['nosetests'].minimum_version),
385         ('IPython', None, None, None),
386         ('numpy', None, 'NumPy', None),
387         ('scipy', None, 'SciPy', None),
388         ('matplotlib', None, 'Matplotlib', None),
389         ('sympy', None, 'SymPy', None),
390         ('Cython', None, None, None),
391         ('networkx', None, 'NetworkX', None),
392         ('mayavi.mlab', None, 'MayaVi', None),
393         ('setuptools', None, 'Setuptools', None),
394         ]:
395     if not name:
396         name = package
397     if not long_name:
398         long_name = name
399     CHECKER[name] = PythonPackageDependency(
400         package=package, name=name, long_name=long_name,
401         minimum_version=minimum_version)
402 del package, name, long_name, minimum_version  # cleanup namespace
403
404
405 class MercurialPythonPackage (PythonPackageDependency):
406     def _get_version(self):
407         try:  # mercurial >= 1.2
408             package = _importlib.import_module('mercurial.util')
409         except ImportError as e:  # mercurial <= 1.1.2
410             package = self._get_package('mercurial.version')
411             return package.get_version()
412         else:
413             return package.version()
414
415
416 CHECKER['mercurial'] = MercurialPythonPackage(
417     package='mercurial.util', name='mercurial',
418     long_name='Mercurial Python package',
419     minimum_version=CHECKER['hg'].minimum_version)
420
421
422 class SQLitePythonPackage (PythonPackageDependency):
423     def _get_version_from_package(self, package):
424         return _sys.version
425
426     def _get_parsed_version(self):
427         return _sys.version_info
428
429
430 CHECKER['sqlite3-python'] = SQLitePythonPackage(
431     package='sqlite3', name='sqlite3-python',
432     long_name='SQLite Python package',
433     minimum_version=CHECKER['sqlite3'].minimum_version)
434
435
436 class VirtualDependency (Dependency):
437     def _check(self):
438         return '{0} {1}'.format(
439             self.or_pass['dependency'].full_name(),
440             self.or_pass['version'])
441
442
443 for name,dependencies in [
444         ('virtual-shell', (
445             'bash',
446             'dash',
447             'ash',
448             'zsh',
449             'ksh',
450             'csh',
451             'tcsh',
452             'sh',
453             )),
454         ('virtual-editor', (
455             'emacs',
456             'xemacs',
457             'vim',
458             'vi',
459             'nano',
460             'kate',
461             'notepad++',
462             )),
463         ('virtual-browser', (
464             'firefox',
465             'google-chrome',
466             'chromium',
467             )),
468         ]:
469     CHECKER[name] = VirtualDependency(
470         name=name, long_name=name, or_dependencies=dependencies)
471 del name, dependencies  # cleanup namespace
472
473
474 def print_system_info():
475     print("If you do not understand why the above failures occurred,")
476     print("copy and send the *entire* output (all info above and summary")
477     print("below) to the instructor for help.")
478     print()
479     print('==================')
480     print('System information')
481     print('==================')
482     print('os.name      : {0}'.format(_os.name))
483     try:
484         print('os.uname     : {0}'.format(_os.uname()))
485     except:
486         pass
487     print('platform     : {0}'.format(_sys.platform))
488     print('platform+    : {0}'.format(_platform.platform()))
489     print('prefix       : {0}'.format(_sys.prefix))
490     print('exec_prefix  : {0}'.format(_sys.exec_prefix))
491     print('executable   : {0}'.format(_sys.executable))
492     print('version_info : {0}'.format(_sys.version_info))
493     print('version      : {0}'.format(_sys.version))
494     print('environment  :')
495     for key,value in sorted(_os.environ.items()):
496         print('  {0}={1}'.format(key, value))
497     print('==================')
498
499 def print_suggestions(instructor_fallback=True):
500     print()
501     print('For suggestions on installing missing packages, see')
502     print('http://software-carpentry.org/setup/')
503     print('')
504     print('For instructings on installing a particular package,')
505     print('see the failure message for that package printed above.')
506     if instructor_fallback:
507         print('')
508         print('For help, email the *entire* output of this script to')
509         print('your instructor.')
510
511
512 if __name__ == '__main__':
513     if not check(_sys.argv[1:]):
514         print()
515         print_system_info()
516         print_suggestions(instructor_fallback=True)