swc-installation-test-2.py: Cache check errors
[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         self._check_error = None
122
123     def __str__(self):
124         return '<{0} {1}>'.format(type(self).__name__, self.name)
125
126     def full_name(self):
127         if self.name == self.long_name:
128             return self.name
129         else:
130             return '{0} ({1})'.format(self.long_name, self.name)
131
132     def check(self):
133         if self._check_error:
134             raise self._check_error
135         try:
136             self._check_dependencies()
137             self._check()
138         except DependencyError as e:
139             self._check_error = e  # cache for future calls
140             raise
141
142     def _check_dependencies(self):
143         for dependency in self.and_dependencies:
144             if not hasattr(dependency, 'check'):
145                 dependency = CHECKER[dependency]
146             dependency.check()
147         or_pass = not bool(self.or_dependencies)
148         or_error = None
149         for dependency in self.or_dependencies:
150             if not hasattr(dependency, 'check'):
151                 dependency = CHECKER[dependency]
152             try:
153                 dependency.check()
154             except DependencyError as e:
155                 or_error = e
156             else:
157                 or_pass = True
158         if not or_pass:
159             print(or_error)
160             raise or_error
161
162     def _check(self):
163         version = self._get_version()
164         parsed_version = None
165         if hasattr(self, '_get_parsed_version'):
166             parsed_version = self._get_parsed_version()
167         if self.minimum_version:
168             self._check_version(version=version, parsed_version=parsed_version)
169
170     def _get_version(self):
171         raise NotImplementedError(self)
172
173     def _check_version(self, version, parsed_version=None):
174         if not parsed_version:
175             parsed_version = self._parse_version(version=version)
176         if parsed_version < self.minimum_version:
177             raise DependencyError(
178                 checker=self,
179                 message='outdated version of {0}: {1} (need >= {2})'.format(
180                     self.full_name(), version,
181                     self.version_delimiter.join(
182                         str(part) for part in self.minimum_version)))
183
184     def _parse_version(self, version):
185         parsed_version = []
186         for part in version.split(self.version_delimiter):
187             try:
188                 parsed_version.append(int(part))
189             except ValueError as e:
190                 raise NotImplementedError((version, part)) from e
191         return tuple(parsed_version)
192
193
194 class PythonDependency (Dependency):
195     def __init__(self, name='python', long_name='Python version',
196                  minimum_version=(2, 6), **kwargs):
197         super(PythonDependency, self).__init__(
198             name=name, long_name=long_name, minimum_version=minimum_version,
199             **kwargs)
200
201     def _get_version(self):
202         return _sys.version
203
204     def _get_parsed_version(self):
205         return _sys.version_info
206
207
208 CHECKER['python'] = PythonDependency()
209
210
211 class CommandDependency (Dependency):
212     exe_extension = _distutils_ccompiler.new_compiler().exe_extension
213
214     def __init__(self, command, version_option='--version',
215                  version_regexp=None, version_stream='stdout', **kwargs):
216         if 'name' not in kwargs:
217             kwargs['name'] = command
218         super(CommandDependency, self).__init__(**kwargs)
219         self.command = command
220         self.version_option = version_option
221         if not version_regexp:
222             regexp = r'([\d][\d{0}]*[\d])'.format(self.version_delimiter)
223             version_regexp = _re.compile(regexp)
224         self.version_regexp = version_regexp
225         self.version_stream = version_stream
226
227     def _get_version_stream(self):
228         command = self.command + (self.exe_extension or '')
229         try:
230             p = _subprocess.Popen(
231                 [command, self.version_option],
232                 stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
233                 close_fds=True, shell=False, universal_newlines=True)
234         except OSError as e:
235             raise DependencyError(
236                 checker=self,
237                 message="could not find '{0}' executable".format(command),
238                 ) from e
239         stdout,stderr = p.communicate()
240         status = p.wait()
241         if status:
242             lines = [
243                 "failed to execute '{0} {1}':".format(
244                     command, self.version_option),
245                 'status: {0}'.format(status),
246                 ]
247             for name,string in [('stdout', stdout), ('stderr', stderr)]:
248                 if string:
249                     lines.extend([name + ':', string])
250             raise DependencyError(checker=self, message='\n'.join(lines))
251         for name,string in [('stdout', stdout), ('stderr', stderr)]:
252             if name == self.version_stream:
253                 return string
254         raise NotImplementedError(self.version_stream)
255
256     def _get_version(self):
257         version_stream = self._get_version_stream()
258         match = self.version_regexp.search(version_stream)
259         if not match:
260             raise DependencyError(
261                 checker=self,
262                 message='no version string in output:\n{0}'.format(
263                     version_stream))
264         return match.group(1)
265
266
267 for command,long_name,minimum_version in [
268         ('bash', 'Bourne Again Shell', (4, 0)),
269         ('easy_install', 'Setuptools easy_install', None),
270         ('git', 'Git', (1, 8, 0)),
271         ('hg', 'Mercurial', (2, 0, 0)),
272         ('make', None, None),
273         ('sqlite3', 'SQLite 3', None),
274         ('nosetests', 'Nose', (1, 0, 0)),
275         ('emacs', 'Emacs', None),
276         ('xemacs', 'XEmacs', None),
277         ('vim', 'Vim', None),
278         ('vi', None, None),
279         ('nano', 'Nano', None),
280         ('kate', 'Kate', None),
281         ('notepad++', 'Notepad++', None),
282         ('firefox', 'Firefox', None),
283         ('google-chrome', 'Google Chrome', None),
284         ('chromium', 'Chromium', None),
285         ]:
286     if not long_name:
287         long_name = command
288     CHECKER[command] = CommandDependency(
289         command=command, long_name=long_name, minimum_version=minimum_version)
290 del command, long_name, minimum_version  # cleanup namespace
291
292
293 class PythonPackageDependency (Dependency):
294     def __init__(self, package, **kwargs):
295         if 'name' not in kwargs:
296             kwargs['name'] = package
297         if 'and_dependencies' not in kwargs:
298             kwargs['and_dependencies'] = []
299         if 'python' not in kwargs['and_dependencies']:
300             kwargs['and_dependencies'].append('python')
301         super(PythonPackageDependency, self).__init__(**kwargs)
302         self.package = package
303
304     def _get_version(self):
305         try:
306             package = _importlib.import_module(self.package)
307         except ImportError as e:
308             raise DependencyError(
309                 checker=self,
310                 message="could not import the '{0}' package".format(
311                     self.package),
312                 ) from e
313         try:
314             version = package.__version__
315         except AttributeError:
316             version = 'unknown'
317         return version
318
319
320 for package,name,long_name,minimum_version in [
321         ('mercurial', None, 'Mercurial Python package',
322          CHECKER['hg'].minimum_version),
323         ('nose', None, 'Nose Python package',
324          CHECKER['nosetests'].minimum_version),
325         ('sqlite3', 'sqlite3-python', 'SQLite Python package',
326          CHECKER['sqlite3'].minimum_version),
327         ('IPython', None, None, None),
328         ('numpy', None, 'NumPy', None),
329         ('scipy', None, 'SciPy', None),
330         ('matplotlib', None, 'Matplotlib', None),
331         ('sympy', None, 'SymPy', None),
332         ('Cython', None, None, None),
333         ('networkx', None, 'NetworkX', None),
334         ('mayavi.mlab', None, 'MayaVi', None),
335         ('setuptools', None, 'Setuptools', None),
336         ]:
337     if not name:
338         name = package
339     if not long_name:
340         long_name = name
341     CHECKER[name] = PythonPackageDependency(
342         package=package, name=name, long_name=long_name,
343         minimum_version=minimum_version)
344 del package, name, long_name, minimum_version  # cleanup namespace
345
346
347 class VirtualDependency (Dependency):
348     def _check(self):
349         pass
350
351
352 for name,dependencies in [
353         ('virtual-editor', (
354             'emacs',
355             'xemacs',
356             'vim',
357             'vi',
358             'nano',
359             'kate',
360             'notepad++',
361             )),
362         ('virtual-browser', (
363             'firefox',
364             'google-chrome',
365             'chromium',
366             )),
367         ]:
368     CHECKER[name] = VirtualDependency(
369         name=name, long_name=name, or_dependencies=dependencies)
370 del name, dependencies  # cleanup namespace
371
372
373 def print_system_info():
374     print("If you do not understand why the above failures occurred,")
375     print("copy and send the *entire* output (all info above and summary")
376     print("below) to the instructor for help.")
377     print()
378     print('==================')
379     print('System information')
380     print('==================')
381     print('os.name      : {0}'.format(_os.name))
382     try:
383         print('os.uname     : {0}'.format(_os.uname()))
384     except:
385         pass
386     print('platform     : {0}'.format(_sys.platform))
387     print('platform+    : {0}'.format(_platform.platform()))
388     print('prefix       : {0}'.format(_sys.prefix))
389     print('exec_prefix  : {0}'.format(_sys.exec_prefix))
390     print('executable   : {0}'.format(_sys.executable))
391     print('version_info : {0}'.format(_sys.version_info))
392     print('version      : {0}'.format(_sys.version))
393     print('environment  :')
394     for key,value in sorted(_os.environ.items()):
395         print('  {0}={1}'.format(key, value))
396     print('==================')
397
398
399 if __name__ == '__main__':
400     if not check(_sys.argv[1:]):
401         print()
402         print_system_info()