4cd22e27da8466cc0ac0a83ef2a54c9bdb0742d1
[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         ('gedit', None, None),
366         ('kate', 'Kate', None),
367         ('notepad++', 'Notepad++', None),
368         ('firefox', 'Firefox', None),
369         ('google-chrome', 'Google Chrome', None),
370         ('chromium', 'Chromium', None),
371         ]:
372     if not long_name:
373         long_name = command
374     CHECKER[command] = CommandDependency(
375         command=command, long_name=long_name, minimum_version=minimum_version)
376 del command, long_name, minimum_version  # cleanup namespace
377
378
379 class MakeDependency (CommandDependency):
380     makefile = '\n'.join([
381             'all:',
382             '\t@echo "MAKE_VERSION=$(MAKE_VERSION)"',
383             '\t@echo "MAKE=$(MAKE)"',
384             '',
385             ])
386
387     def _get_version(self):
388         try:
389             return super(MakeDependency, self)._get_version()
390         except DependencyError as e:
391             version_options = self.version_options
392             self.version_options = ['-f', '-']
393             try:
394                 stream = self._get_version_stream(stdin=self.makefile)
395                 info = {}
396                 for line in stream.splitlines():
397                     try:
398                         key,value = line.split('=', 1)
399                     except ValueError as ve:
400                         raise e# from NotImplementedError(stream)
401                     info[key] = value
402                 if info.get('MAKE_VERSION', None):
403                     return info['MAKE_VERSION']
404                 elif info.get('MAKE', None):
405                     return None
406                 raise e
407             finally:
408                 self.version_options = version_options
409
410
411 CHECKER['make'] = MakeDependency(command='make', minimum_version=None)
412
413
414 class EasyInstallDependency (CommandDependency):
415     def _get_version(self):
416         try:
417             return super(EasyInstallDependency, self)._get_version()
418         except DependencyError as e:
419             version_stream = self.version_stream
420             try:
421                 self.version_stream = 'stderr'
422                 stream = self._get_version_stream(expect=(1,))
423                 if 'option --version not recognized' in stream:
424                     return 'unknown (possibly Setuptools?)'
425             finally:
426                 self.version_stream = version_stream
427
428
429 CHECKER['easy_install'] = EasyInstallDependency(
430     command='easy_install', long_name='Setuptools easy_install',
431     minimum_version=None)
432
433
434 class PythonPackageDependency (Dependency):
435     def __init__(self, package, **kwargs):
436         if 'name' not in kwargs:
437             kwargs['name'] = package
438         if 'and_dependencies' not in kwargs:
439             kwargs['and_dependencies'] = []
440         if 'python' not in kwargs['and_dependencies']:
441             kwargs['and_dependencies'].append('python')
442         super(PythonPackageDependency, self).__init__(**kwargs)
443         self.package = package
444
445     def _get_version(self):
446         package = self._get_package(self.package)
447         return self._get_version_from_package(package)
448
449     def _get_package(self, package):
450         try:
451             return _importlib.import_module(package)
452         except ImportError as e:
453             raise DependencyError(
454                 checker=self,
455                 message="could not import the '{0}' package for {1}".format(
456                     package, self.full_name()),
457                 )# from e
458
459     def _get_version_from_package(self, package):
460         try:
461             version = package.__version__
462         except AttributeError:
463             version = None
464         return version
465
466
467 for package,name,long_name,minimum_version,and_dependencies in [
468         ('nose', None, 'Nose Python package',
469          CHECKER['nosetests'].minimum_version, None),
470         ('IPython', None, 'IPython Python package',
471          CHECKER['ipython'].minimum_version, None),
472         ('numpy', None, 'NumPy', None, None),
473         ('scipy', None, 'SciPy', None, None),
474         ('matplotlib', None, 'Matplotlib', None, None),
475         ('pandas', None, 'Pandas', (0, 8), None),
476         ('sympy', None, 'SymPy', None, None),
477         ('Cython', None, None, None, None),
478         ('networkx', None, 'NetworkX', None, None),
479         ('mayavi.mlab', None, 'MayaVi', None, None),
480         ('setuptools', None, 'Setuptools', None, None),
481         ]:
482     if not name:
483         name = package
484     if not long_name:
485         long_name = name
486     kwargs = {}
487     if and_dependencies:
488         kwargs['and_dependencies'] = and_dependencies
489     CHECKER[name] = PythonPackageDependency(
490         package=package, name=name, long_name=long_name,
491         minimum_version=minimum_version, **kwargs)
492 # cleanup namespace
493 del package, name, long_name, minimum_version, and_dependencies, kwargs
494
495
496 class MercurialPythonPackage (PythonPackageDependency):
497     def _get_version(self):
498         try:  # mercurial >= 1.2
499             package = _importlib.import_module('mercurial.util')
500         except ImportError as e:  # mercurial <= 1.1.2
501             package = self._get_package('mercurial.version')
502             return package.get_version()
503         else:
504             return package.version()
505
506
507 CHECKER['mercurial'] = MercurialPythonPackage(
508     package='mercurial.util', name='mercurial',
509     long_name='Mercurial Python package',
510     minimum_version=CHECKER['hg'].minimum_version)
511
512
513 class SQLitePythonPackage (PythonPackageDependency):
514     def _get_version_from_package(self, package):
515         return _sys.version
516
517     def _get_parsed_version(self):
518         return _sys.version_info
519
520
521 CHECKER['sqlite3-python'] = SQLitePythonPackage(
522     package='sqlite3', name='sqlite3-python',
523     long_name='SQLite Python package',
524     minimum_version=CHECKER['sqlite3'].minimum_version)
525
526
527 class VirtualDependency (Dependency):
528     def _check(self):
529         return '{0} {1}'.format(
530             self.or_pass['dependency'].full_name(),
531             self.or_pass['version'])
532
533
534 for name,dependencies in [
535         ('virtual-shell', (
536             'bash',
537             'dash',
538             'ash',
539             'zsh',
540             'ksh',
541             'csh',
542             'tcsh',
543             'sh',
544             )),
545         ('virtual-editor', (
546             'emacs',
547             'xemacs',
548             'vim',
549             'vi',
550             'nano',
551             'gedit',
552             'kate',
553             'notepad++',
554             )),
555         ('virtual-browser', (
556             'firefox',
557             'google-chrome',
558             'chromium',
559             )),
560         ('virtual-pypi-installer', (
561             'easy_install',
562             'pip',
563             )),
564         ]:
565     CHECKER[name] = VirtualDependency(
566         name=name, long_name=name, or_dependencies=dependencies)
567 del name, dependencies  # cleanup namespace
568
569
570 def _print_info(key, value, indent=19):
571     print('{0}{1}: {2}'.format(key, ' '*(indent-len(key)), value))
572
573 def print_system_info():
574     print("If you do not understand why the above failures occurred,")
575     print("copy and send the *entire* output (all info above and summary")
576     print("below) to the instructor for help.")
577     print()
578     print('==================')
579     print('System information')
580     print('==================')
581     _print_info('os.name', _os.name)
582     _print_info('os.uname', _platform.uname())
583     _print_info('platform', _sys.platform)
584     _print_info('platform+', _platform.platform())
585     for pversion in (
586             'linux_distribution',
587             'mac_ver',
588             'win32_ver',
589             ):
590         value = getattr(_platform, pversion)()
591         if value[0]:
592             _print_info(pversion, value)
593     _print_info('prefix', _sys.prefix)
594     _print_info('exec_prefix', _sys.exec_prefix)
595     _print_info('executable', _sys.executable)
596     _print_info('version_info', _sys.version_info)
597     _print_info('version', _sys.version)
598     _print_info('environment', '')
599     for key,value in sorted(_os.environ.items()):
600         print('  {0}={1}'.format(key, value))
601     print('==================')
602
603 def print_suggestions(instructor_fallback=True):
604     print()
605     print('For suggestions on installing missing packages, see')
606     print('http://software-carpentry.org/setup/')
607     print('')
608     print('For instructings on installing a particular package,')
609     print('see the failure message for that package printed above.')
610     if instructor_fallback:
611         print('')
612         print('For help, email the *entire* output of this script to')
613         print('your instructor.')
614
615
616 if __name__ == '__main__':
617     try:
618         passed = check(_sys.argv[1:])
619     except InvalidCheck as e:
620         print("I don't know how to check for {0!r}".format(e.check))
621         print('I do know how to check for:')
622         for key,checker in sorted(CHECKER.items()):
623             if checker.long_name != checker.name:
624                 print('  {0} {1}({2})'.format(
625                         key, ' '*(20-len(key)), checker.long_name))
626             else:
627                 print('  {0}'.format(key))
628         _sys.exit(1)
629     if not passed:
630         print()
631         print_system_info()
632         print_suggestions(instructor_fallback=True)
633         _sys.exit(1)