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