swc-installation-test-2.py: Add exe_extension for MS Windows compat.
[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 import distutils.ccompiler as _distutils_ccompiler
26 import importlib as _importlib
27 import logging as _logging
28 import os as _os
29 import platform as _platform
30 import re as _re
31 import subprocess as _subprocess
32 import sys as _sys
33
34
35 __version__ = '0.1'
36
37 # Comment out any entries you don't need
38 CHECKS = [
39 # Shell
40     'bash',
41 # Editors
42     'virtual-editor',
43 # Browsers
44     'virtual-browser',
45 # Version control
46     'git',
47     'hg',              # Command line tool
48     'mercurial',       # Python package
49 # Build tools and packaging
50     'make',
51     'easy_install',
52     'setuptools',
53 # Testing
54     'nosetests',       # Command line tool
55     'nose',            # Python package
56 # SQL
57     'sqlite3',         # Command line tool
58     'sqlite3-python',  # Python package
59 # Python
60     'python',
61     'IPython',
62     'numpy',
63     'scipy',
64     'matplotlib',
65     'sympy',
66     'Cython',
67     'networkx',
68     'mayavi.mlab',
69     ]
70
71 CHECKER = {}
72
73
74 class DependencyError (Exception):
75     def __init__(self, checker, message):
76         self.checker = checker
77         self.message = message
78
79     def __str__(self):
80         return 'check for {0} failed:\n{1}'.format(
81             self.checker.full_name(), self.message)
82
83
84 def check(checks=None):
85     failures = []
86     if not checks:
87         checks = CHECKS
88     for check in checks:
89         checker = CHECKER[check]
90         _sys.stdout.write('check {0}...\t'.format(checker.full_name()))
91         try:
92             checker.check()
93         except DependencyError as e:
94             failures.append(e)
95             _sys.stdout.write('fail\n')
96         else:
97             _sys.stdout.write('pass\n')
98     if failures:
99         print('\nFailures:')
100         for failure in failures:
101             print()
102             print(failure)
103         return False
104     return True
105
106
107 class Dependency (object):
108     def __init__(self, name, long_name=None, minimum_version=None,
109                  version_delimiter='.', and_dependencies=None,
110                  or_dependencies=None):
111         self.name = name
112         self.long_name = long_name or name
113         self.minimum_version = minimum_version
114         self.version_delimiter = version_delimiter
115         if not and_dependencies:
116             and_dependencies = []
117         self.and_dependencies = and_dependencies
118         if not or_dependencies:
119             or_dependencies = []
120         self.or_dependencies = or_dependencies
121
122     def __str__(self):
123         return '<{0} {1}>'.format(type(self).__name__, self.name)
124
125     def full_name(self):
126         if self.name == self.long_name:
127             return self.name
128         else:
129             return '{0} ({1})'.format(self.long_name, self.name)
130
131     def check(self):
132         self._check_dependencies()
133         self._check()
134
135     def _check_dependencies(self):
136         for dependency in self.and_dependencies:
137             if not hasattr(dependency, 'check'):
138                 dependency = CHECKER[dependency]
139             dependency.check()
140         or_pass = not bool(self.or_dependencies)
141         or_error = None
142         for dependency in self.or_dependencies:
143             if not hasattr(dependency, 'check'):
144                 dependency = CHECKER[dependency]
145             try:
146                 dependency.check()
147             except DependencyError as e:
148                 or_error = e
149             else:
150                 or_pass = True
151         if not or_pass:
152             print(or_error)
153             raise or_error
154
155     def _check(self):
156         version = self._get_version()
157         parsed_version = None
158         if hasattr(self, '_get_parsed_version'):
159             parsed_version = self._get_parsed_version()
160         if self.minimum_version:
161             self._check_version(version=version, parsed_version=parsed_version)
162
163     def _get_version(self):
164         raise NotImplementedError(self)
165
166     def _check_version(self, version, parsed_version=None):
167         if not parsed_version:
168             parsed_version = self._parse_version(version=version)
169         if parsed_version < self.minimum_version:
170             raise DependencyError(
171                 checker=self,
172                 message='outdated version of {0}: {1} (need >= {2})'.format(
173                     self.full_name(), version,
174                     self.version_delimiter.join(
175                         str(part) for part in self.minimum_version)))
176
177     def _parse_version(self, version):
178         parsed_version = []
179         for part in version.split(self.version_delimiter):
180             try:
181                 parsed_version.append(int(part))
182             except ValueError as e:
183                 raise NotImplementedError((version, part)) from e
184         return tuple(parsed_version)
185
186
187 class PythonDependency (Dependency):
188     def __init__(self, name='python', long_name='Python version',
189                  minimum_version=(2, 6), **kwargs):
190         super(PythonDependency, self).__init__(
191             name=name, long_name=long_name, minimum_version=minimum_version,
192             **kwargs)
193
194     def _get_version(self):
195         return _sys.version
196
197     def _get_parsed_version(self):
198         return _sys.version_info
199
200
201 CHECKER['python'] = PythonDependency()
202
203
204 class CommandDependency (Dependency):
205     exe_extension = _distutils_ccompiler.new_compiler().exe_extension
206
207     def __init__(self, command, version_option='--version',
208                  version_regexp=None, version_stream='stdout', **kwargs):
209         if 'name' not in kwargs:
210             kwargs['name'] = command
211         super(CommandDependency, self).__init__(**kwargs)
212         self.command = command
213         self.version_option = version_option
214         if not version_regexp:
215             regexp = r'([\d][\d{0}]*[\d])'.format(self.version_delimiter)
216             version_regexp = _re.compile(regexp)
217         self.version_regexp = version_regexp
218         self.version_stream = version_stream
219
220     def _get_version_stream(self):
221         command = self.command + (self.exe_extension or '')
222         try:
223             p = _subprocess.Popen(
224                 [command, self.version_option],
225                 stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
226                 close_fds=True, shell=False, universal_newlines=True)
227         except OSError as e:
228             raise DependencyError(
229                 checker=self,
230                 message="could not find '{0}' executable".format(command),
231                 ) from e
232         stdout,stderr = p.communicate()
233         status = p.wait()
234         if status:
235             lines = [
236                 "failed to execute '{0} {1}':".format(
237                     command, self.version_option),
238                 'status: {0}'.format(status),
239                 ]
240             for name,string in [('stdout', stdout), ('stderr', stderr)]:
241                 if string:
242                     lines.extend([name + ':', string])
243             raise DependencyError(checker=self, message='\n'.join(lines))
244         for name,string in [('stdout', stdout), ('stderr', stderr)]:
245             if name == self.version_stream:
246                 return string
247         raise NotImplementedError(self.version_stream)
248
249     def _get_version(self):
250         version_stream = self._get_version_stream()
251         match = self.version_regexp.search(version_stream)
252         if not match:
253             raise DependencyError(
254                 checker=self,
255                 message='no version string in output:\n{0}'.format(
256                     version_stream))
257         return match.group(1)
258
259
260 for command,long_name,minimum_version in [
261         ('bash', 'Bourne Again Shell', (4, 0)),
262         ('easy_install', 'Setuptools easy_install', None),
263         ('git', 'Git', (1, 8, 0)),
264         ('hg', 'Mercurial', (2, 0, 0)),
265         ('make', None, None),
266         ('sqlite3', 'SQLite 3', None),
267         ('nosetests', 'Nose', (1, 0, 0)),
268         ('emacs', 'Emacs', None),
269         ('xemacs', 'XEmacs', None),
270         ('vim', 'Vim', None),
271         ('vi', None, None),
272         ('nano', 'Nano', None),
273         ('kate', 'Kate', None),
274         ('notepad++', 'Notepad++', None),
275         ('firefox', 'Firefox', None),
276         ('google-chrome', 'Google Chrome', None),
277         ('chromium', 'Chromium', None),
278         ]:
279     if not long_name:
280         long_name = command
281     CHECKER[command] = CommandDependency(
282         command=command, long_name=long_name, minimum_version=minimum_version)
283 del command, long_name, minimum_version  # cleanup namespace
284
285
286 class PythonPackageDependency (Dependency):
287     def __init__(self, package, **kwargs):
288         if 'name' not in kwargs:
289             kwargs['name'] = package
290         super(PythonPackageDependency, self).__init__(**kwargs)
291         self.package = package
292
293     def _get_version(self):
294         try:
295             package = _importlib.import_module(self.package)
296         except ImportError as e:
297             raise DependencyError(
298                 checker=self,
299                 message="could not import the '{0}' package".format(
300                     self.package),
301                 ) from e
302         try:
303             version = package.__version__
304         except AttributeError:
305             version = 'unknown'
306         return version
307
308
309 for package,name,long_name,minimum_version in [
310         ('mercurial', None, 'Mercurial Python package',
311          CHECKER['hg'].minimum_version),
312         ('nose', None, 'Nose Python package',
313          CHECKER['nosetests'].minimum_version),
314         ('sqlite3', 'sqlite3-python', 'SQLite Python package',
315          CHECKER['sqlite3'].minimum_version),
316         ('IPython', None, None, None),
317         ('numpy', None, 'NumPy', None),
318         ('scipy', None, 'SciPy', None),
319         ('matplotlib', None, 'Matplotlib', None),
320         ('sympy', None, 'SymPy', None),
321         ('Cython', None, None, None),
322         ('networkx', None, 'NetworkX', None),
323         ('mayavi.mlab', None, 'MayaVi', None),
324         ('setuptools', None, 'Setuptools', None),
325         ]:
326     if not name:
327         name = package
328     if not long_name:
329         long_name = name
330     CHECKER[name] = PythonPackageDependency(
331         package=package, name=name, long_name=long_name,
332         minimum_version=minimum_version)
333 del package, name, long_name, minimum_version  # cleanup namespace
334
335
336 class VirtualDependency (Dependency):
337     def _check(self):
338         pass
339
340
341 for name,dependencies in [
342         ('virtual-editor', (
343             'emacs',
344             'xemacs',
345             'vim',
346             'vi',
347             'nano',
348             'kate',
349             'notepad++',
350             )),
351         ('virtual-browser', (
352             'firefox',
353             'google-chrome',
354             'chromium',
355             )),
356         ]:
357     CHECKER[name] = VirtualDependency(
358         name=name, long_name=name, or_dependencies=dependencies)
359 del name, dependencies  # cleanup namespace
360
361
362 def print_system_info():
363     print("If you do not understand why the above failures occurred,")
364     print("copy and send the *entire* output (all info above and summary")
365     print("below) to the instructor for help.")
366     print()
367     print('==================')
368     print('System information')
369     print('==================')
370     print('os.name      : {0}'.format(_os.name))
371     try:
372         print('os.uname     : {0}'.format(_os.uname()))
373     except:
374         pass
375     print('platform     : {0}'.format(_sys.platform))
376     print('platform+    : {0}'.format(_platform.platform()))
377     print('prefix       : {0}'.format(_sys.prefix))
378     print('exec_prefix  : {0}'.format(_sys.exec_prefix))
379     print('executable   : {0}'.format(_sys.executable))
380     print('version_info : {0}'.format(_sys.version_info))
381     print('version      : {0}'.format(_sys.version))
382     print('environment  :')
383     for key,value in sorted(_os.environ.items()):
384         print('  {0}={1}'.format(key, value))
385     print('==================')
386
387
388 if __name__ == '__main__':
389     if not check(_sys.argv[1:]):
390         print()
391         print_system_info()