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