b479c98bd0bc9e1eae09d242086b41e152631404
[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 InvalidCheck (KeyError):
96     def __init__(self, check):
97         super(InvalidCheck, self).__init__(check)
98         self.check = check
99
100     def __str__(self):
101         return self.check
102
103
104 class DependencyError (Exception):
105     def _get_message(self):
106         return self._message
107     def _set_message(self, message):
108         self._message = message
109     message = property(_get_message, _set_message)
110
111     def __init__(self, checker, message):
112         super(DependencyError, self).__init__(message)
113         self.checker = checker
114         self.message = message
115
116     def __str__(self):
117         url = 'http://software-carpentry.org/setup/'  # TODO: per-package URL
118         return 'check for {0} failed:\n{1}\n{2}\n{3}'.format(
119             self.checker.full_name(), self.message,
120             'For instructions on installing an up-to-date version, see',
121             url)
122
123
124 def check(checks=None):
125     successes = []
126     failures = []
127     if not checks:
128         checks = CHECKS
129     for check in checks:
130         try:
131             checker = CHECKER[check]
132         except KeyError as e:
133             raise InvalidCheck(check)# from e
134         _sys.stdout.write('check {0}...\t'.format(checker.full_name()))
135         try:
136             version = checker.check()
137         except DependencyError as e:
138             failures.append(e)
139             _sys.stdout.write('fail\n')
140         else:
141             _sys.stdout.write('pass\n')
142             successes.append((checker, version))
143     if successes:
144         print('\nSuccesses:\n')
145         for checker,version in successes:
146             print('{0} {1}'.format(
147                     checker.full_name(),
148                     version or 'unknown'))
149     if failures:
150         print('\nFailures:')
151         printed = []
152         for failure in failures:
153             if failure not in printed:
154                 print()
155                 print(failure)
156                 printed.append(failure)
157         return False
158     return True
159
160
161 class Dependency (object):
162     def __init__(self, name, long_name=None, minimum_version=None,
163                  version_delimiter='.', and_dependencies=None,
164                  or_dependencies=None):
165         self.name = name
166         self.long_name = long_name or name
167         self.minimum_version = minimum_version
168         self.version_delimiter = version_delimiter
169         if not and_dependencies:
170             and_dependencies = []
171         self.and_dependencies = and_dependencies
172         if not or_dependencies:
173             or_dependencies = []
174         self.or_dependencies = or_dependencies
175         self._check_error = None
176
177     def __str__(self):
178         return '<{0} {1}>'.format(type(self).__name__, self.name)
179
180     def full_name(self):
181         if self.name == self.long_name:
182             return self.name
183         else:
184             return '{0} ({1})'.format(self.long_name, self.name)
185
186     def check(self):
187         if self._check_error:
188             raise self._check_error
189         try:
190             self._check_dependencies()
191             return self._check()
192         except DependencyError as e:
193             self._check_error = e  # cache for future calls
194             raise
195
196     def _check_dependencies(self):
197         for dependency in self.and_dependencies:
198             if not hasattr(dependency, 'check'):
199                 dependency = CHECKER[dependency]
200             dependency.check()
201         self.or_pass = or_error = None
202         for dependency in self.or_dependencies:
203             if not hasattr(dependency, 'check'):
204                 dependency = CHECKER[dependency]
205             try:
206                 version = dependency.check()
207             except DependencyError as e:
208                 or_error = e
209             else:
210                 self.or_pass = {
211                     'dependency': dependency,
212                     'version': version,
213                     }
214                 break  # no need to test other dependencies
215         if self.or_dependencies and not self.or_pass:
216             raise or_error
217
218     def _check(self):
219         version = self._get_version()
220         parsed_version = None
221         if hasattr(self, '_get_parsed_version'):
222             parsed_version = self._get_parsed_version()
223         if self.minimum_version:
224             self._check_version(version=version, parsed_version=parsed_version)
225         return version
226
227     def _get_version(self):
228         raise NotImplementedError(self)
229
230     def _check_version(self, version, parsed_version=None):
231         if not parsed_version:
232             parsed_version = self._parse_version(version=version)
233         if not parsed_version or parsed_version < self.minimum_version:
234             raise DependencyError(
235                 checker=self,
236                 message='outdated version of {0}: {1} (need >= {2})'.format(
237                     self.full_name(), version,
238                     self.version_delimiter.join(
239                         str(part) for part in self.minimum_version)))
240
241     def _parse_version(self, version):
242         if not version:
243             return None
244         parsed_version = []
245         for part in version.split(self.version_delimiter):
246             try:
247                 parsed_version.append(int(part))
248             except ValueError as e:
249                 raise NotImplementedError((version, part))# from e
250         return tuple(parsed_version)
251
252
253 class PythonDependency (Dependency):
254     def __init__(self, name='python', long_name='Python version',
255                  minimum_version=(2, 6), **kwargs):
256         super(PythonDependency, self).__init__(
257             name=name, long_name=long_name, minimum_version=minimum_version,
258             **kwargs)
259
260     def _get_version(self):
261         return _sys.version
262
263     def _get_parsed_version(self):
264         return _sys.version_info
265
266
267 CHECKER['python'] = PythonDependency()
268
269
270 class CommandDependency (Dependency):
271     exe_extension = _distutils_ccompiler.new_compiler().exe_extension
272
273     def __init__(self, command, version_options=('--version',), stdin=None,
274                  version_regexp=None, version_stream='stdout', **kwargs):
275         if 'name' not in kwargs:
276             kwargs['name'] = command
277         super(CommandDependency, self).__init__(**kwargs)
278         self.command = command
279         self.version_options = version_options
280         self.stdin = None
281         if not version_regexp:
282             regexp = r'([\d][\d{0}]*[\d])'.format(self.version_delimiter)
283             version_regexp = _re.compile(regexp)
284         self.version_regexp = version_regexp
285         self.version_stream = version_stream
286
287     def _get_version_stream(self, stdin=None, expect=(0,)):
288         if not stdin:
289             stdin = self.stdin
290         if stdin:
291             popen_stdin = _subprocess.PIPE
292         else:
293             popen_stdin = None
294         command = self.command + (self.exe_extension or '')
295         try:
296             p = _subprocess.Popen(
297                 [command] + list(self.version_options), stdin=popen_stdin,
298                 stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
299                 close_fds=True, shell=False, universal_newlines=True)
300         except OSError as e:
301             raise DependencyError(
302                 checker=self,
303                 message="could not find '{0}' executable".format(command),
304                 )# from e
305         stdout,stderr = p.communicate(stdin)
306         status = p.wait()
307         if status not in expect:
308             lines = [
309                 "failed to execute: {0} {1}".format(
310                     command,
311                     ' '.join(_shlex.quote(arg)
312                              for arg in self.version_options)),
313                 'status: {0}'.format(status),
314                 ]
315             for name,string in [('stdout', stdout), ('stderr', stderr)]:
316                 if string:
317                     lines.extend([name + ':', string])
318             raise DependencyError(checker=self, message='\n'.join(lines))
319         for name,string in [('stdout', stdout), ('stderr', stderr)]:
320             if name == self.version_stream:
321                 return string
322         raise NotImplementedError(self.version_stream)
323
324     def _get_version(self):
325         version_stream = self._get_version_stream()
326         match = self.version_regexp.search(version_stream)
327         if not match:
328             raise DependencyError(
329                 checker=self,
330                 message='no version string in output:\n{0}'.format(
331                     version_stream))
332         return match.group(1)
333
334
335 for command,long_name,minimum_version in [
336         ('sh', 'Bourne Shell', None),
337         ('ash', 'Almquist Shell', None),
338         ('bash', 'Bourne Again Shell', None),
339         ('csh', 'C Shell', None),
340         ('ksh', 'KornShell', None),
341         ('dash', 'Debian Almquist Shell', None),
342         ('tcsh', 'TENEX C Shell', None),
343         ('zsh', 'Z Shell', None),
344         ('git', 'Git', (1, 7, 0)),
345         ('hg', 'Mercurial', (2, 0, 0)),
346         ('sqlite3', 'SQLite 3', None),
347         ('nosetests', 'Nose', (1, 0, 0)),
348         ('emacs', 'Emacs', None),
349         ('xemacs', 'XEmacs', None),
350         ('vim', 'Vim', None),
351         ('vi', None, None),
352         ('nano', 'Nano', None),
353         ('kate', 'Kate', None),
354         ('notepad++', 'Notepad++', None),
355         ('firefox', 'Firefox', None),
356         ('google-chrome', 'Google Chrome', None),
357         ('chromium', 'Chromium', None),
358         ]:
359     if not long_name:
360         long_name = command
361     CHECKER[command] = CommandDependency(
362         command=command, long_name=long_name, minimum_version=minimum_version)
363 del command, long_name, minimum_version  # cleanup namespace
364
365
366 class MakeDependency (CommandDependency):
367     makefile = '\n'.join([
368             'all:',
369             '\t@echo "MAKE_VERSION=$(MAKE_VERSION)"',
370             '\t@echo "MAKE=$(MAKE)"',
371             '',
372             ])
373
374     def _get_version(self):
375         try:
376             return super(MakeDependency, self)._get_version()
377         except DependencyError as e:
378             version_options = self.version_options
379             self.version_options = ['-f', '-']
380             try:
381                 stream = self._get_version_stream(stdin=self.makefile)
382                 info = {}
383                 for line in stream.splitlines():
384                     try:
385                         key,value = line.split('=', 1)
386                     except ValueError as ve:
387                         raise e# from NotImplementedError(stream)
388                     info[key] = value
389                 if info.get('MAKE_VERSION', None):
390                     return info['MAKE_VERSION']
391                 elif info.get('MAKE', None):
392                     return None
393                 raise e
394             finally:
395                 self.version_options = version_options
396
397
398 CHECKER['make'] = MakeDependency(command='make', minimum_version=None)
399
400
401 class EasyInstallDependency (CommandDependency):
402     def _get_version(self):
403         try:
404             return super(EasyInstallDependency, self)._get_version()
405         except DependencyError as e:
406             version_stream = self.version_stream
407             try:
408                 self.version_stream = 'stderr'
409                 stream = self._get_version_stream(expect=(1,))
410                 if 'option --version not recognized' in stream:
411                     return 'unknown (possibly Setuptools?)'
412             finally:
413                 self.version_stream = version_stream
414
415
416 CHECKER['easy_install'] = EasyInstallDependency(
417     command='easy_install', long_name='Setuptools easy_install',
418     minimum_version=None)
419
420
421 class PythonPackageDependency (Dependency):
422     def __init__(self, package, **kwargs):
423         if 'name' not in kwargs:
424             kwargs['name'] = package
425         if 'and_dependencies' not in kwargs:
426             kwargs['and_dependencies'] = []
427         if 'python' not in kwargs['and_dependencies']:
428             kwargs['and_dependencies'].append('python')
429         super(PythonPackageDependency, self).__init__(**kwargs)
430         self.package = package
431
432     def _get_version(self):
433         package = self._get_package(self.package)
434         return self._get_version_from_package(package)
435
436     def _get_package(self, package):
437         try:
438             return _importlib.import_module(package)
439         except ImportError as e:
440             raise DependencyError(
441                 checker=self,
442                 message="could not import the '{0}' package for {1}".format(
443                     package, self.full_name()),
444                 )# from e
445
446     def _get_version_from_package(self, package):
447         try:
448             version = package.__version__
449         except AttributeError:
450             version = None
451         return version
452
453
454 for package,name,long_name,minimum_version in [
455         ('nose', None, 'Nose Python package',
456          CHECKER['nosetests'].minimum_version),
457         ('IPython', None, None, None),
458         ('numpy', None, 'NumPy', None),
459         ('scipy', None, 'SciPy', None),
460         ('matplotlib', None, 'Matplotlib', None),
461         ('sympy', None, 'SymPy', None),
462         ('Cython', None, None, None),
463         ('networkx', None, 'NetworkX', None),
464         ('mayavi.mlab', None, 'MayaVi', None),
465         ('setuptools', None, 'Setuptools', None),
466         ]:
467     if not name:
468         name = package
469     if not long_name:
470         long_name = name
471     CHECKER[name] = PythonPackageDependency(
472         package=package, name=name, long_name=long_name,
473         minimum_version=minimum_version)
474 del package, name, long_name, minimum_version  # cleanup namespace
475
476
477 class MercurialPythonPackage (PythonPackageDependency):
478     def _get_version(self):
479         try:  # mercurial >= 1.2
480             package = _importlib.import_module('mercurial.util')
481         except ImportError as e:  # mercurial <= 1.1.2
482             package = self._get_package('mercurial.version')
483             return package.get_version()
484         else:
485             return package.version()
486
487
488 CHECKER['mercurial'] = MercurialPythonPackage(
489     package='mercurial.util', name='mercurial',
490     long_name='Mercurial Python package',
491     minimum_version=CHECKER['hg'].minimum_version)
492
493
494 class SQLitePythonPackage (PythonPackageDependency):
495     def _get_version_from_package(self, package):
496         return _sys.version
497
498     def _get_parsed_version(self):
499         return _sys.version_info
500
501
502 CHECKER['sqlite3-python'] = SQLitePythonPackage(
503     package='sqlite3', name='sqlite3-python',
504     long_name='SQLite Python package',
505     minimum_version=CHECKER['sqlite3'].minimum_version)
506
507
508 class VirtualDependency (Dependency):
509     def _check(self):
510         return '{0} {1}'.format(
511             self.or_pass['dependency'].full_name(),
512             self.or_pass['version'])
513
514
515 for name,dependencies in [
516         ('virtual-shell', (
517             'bash',
518             'dash',
519             'ash',
520             'zsh',
521             'ksh',
522             'csh',
523             'tcsh',
524             'sh',
525             )),
526         ('virtual-editor', (
527             'emacs',
528             'xemacs',
529             'vim',
530             'vi',
531             'nano',
532             'kate',
533             'notepad++',
534             )),
535         ('virtual-browser', (
536             'firefox',
537             'google-chrome',
538             'chromium',
539             )),
540         ]:
541     CHECKER[name] = VirtualDependency(
542         name=name, long_name=name, or_dependencies=dependencies)
543 del name, dependencies  # cleanup namespace
544
545
546 def _print_info(key, value, indent=19):
547     print('{0}{1}: {2}'.format(key, ' '*(indent-len(key)), value))
548
549 def print_system_info():
550     print("If you do not understand why the above failures occurred,")
551     print("copy and send the *entire* output (all info above and summary")
552     print("below) to the instructor for help.")
553     print()
554     print('==================')
555     print('System information')
556     print('==================')
557     _print_info('os.name', _os.name)
558     _print_info('os.uname', _platform.uname())
559     _print_info('platform', _sys.platform)
560     _print_info('platform+', _platform.platform())
561     for pversion in (
562             'linux_distribution',
563             'mac_ver',
564             'win32_ver',
565             ):
566         value = getattr(_platform, pversion)()
567         if value[0]:
568             _print_info(pversion, value)
569     _print_info('prefix', _sys.prefix)
570     _print_info('exec_prefix', _sys.exec_prefix)
571     _print_info('executable', _sys.executable)
572     _print_info('version_info', _sys.version_info)
573     _print_info('version', _sys.version)
574     _print_info('environment', '')
575     for key,value in sorted(_os.environ.items()):
576         print('  {0}={1}'.format(key, value))
577     print('==================')
578
579 def print_suggestions(instructor_fallback=True):
580     print()
581     print('For suggestions on installing missing packages, see')
582     print('http://software-carpentry.org/setup/')
583     print('')
584     print('For instructings on installing a particular package,')
585     print('see the failure message for that package printed above.')
586     if instructor_fallback:
587         print('')
588         print('For help, email the *entire* output of this script to')
589         print('your instructor.')
590
591
592 if __name__ == '__main__':
593     try:
594         passed = check(_sys.argv[1:])
595     except InvalidCheck as e:
596         print("I don't know how to check for {0!r}".format(e.check))
597         print('I do know how to check for:')
598         for key,checker in sorted(CHECKER.items()):
599             if checker.long_name != checker.name:
600                 print('  {0} {1}({2})'.format(
601                         key, ' '*(20-len(key)), checker.long_name))
602             else:
603                 print('  {0}'.format(key))
604         _sys.exit(1)
605     if not passed:
606         print()
607         print_system_info()
608         print_suggestions(instructor_fallback=True)