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