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