swc-installation-test-2.py: Add CommandDependency.stdin
[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',), stdin=None,
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         self.stdin = None
259         if not version_regexp:
260             regexp = r'([\d][\d{0}]*[\d])'.format(self.version_delimiter)
261             version_regexp = _re.compile(regexp)
262         self.version_regexp = version_regexp
263         self.version_stream = version_stream
264
265     def _get_version_stream(self, stdin=None, expect=(0,)):
266         if not stdin:
267             stdin = self.stdin
268         if stdin:
269             popen_stdin = _subprocess.PIPE
270         else:
271             popen_stdin = None
272         command = self.command + (self.exe_extension or '')
273         try:
274             p = _subprocess.Popen(
275                 [command] + list(self.version_options), stdin=popen_stdin,
276                 stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
277                 close_fds=True, shell=False, universal_newlines=True)
278         except OSError as e:
279             raise DependencyError(
280                 checker=self,
281                 message="could not find '{0}' executable".format(command),
282                 )# from e
283         stdout,stderr = p.communicate(stdin)
284         status = p.wait()
285         if status not in expect:
286             lines = [
287                 "failed to execute: {0} {1}".format(
288                     command,
289                     ' '.join(_shlex.quote(arg)
290                              for arg in self.version_options)),
291                 'status: {0}'.format(status),
292                 ]
293             for name,string in [('stdout', stdout), ('stderr', stderr)]:
294                 if string:
295                     lines.extend([name + ':', string])
296             raise DependencyError(checker=self, message='\n'.join(lines))
297         for name,string in [('stdout', stdout), ('stderr', stderr)]:
298             if name == self.version_stream:
299                 return string
300         raise NotImplementedError(self.version_stream)
301
302     def _get_version(self):
303         version_stream = self._get_version_stream()
304         match = self.version_regexp.search(version_stream)
305         if not match:
306             raise DependencyError(
307                 checker=self,
308                 message='no version string in output:\n{0}'.format(
309                     version_stream))
310         return match.group(1)
311
312
313 for command,long_name,minimum_version in [
314         ('sh', 'Bourne Shell', None),
315         ('ash', 'Almquist Shell', None),
316         ('bash', 'Bourne Again Shell', None),
317         ('csh', 'C Shell', None),
318         ('ksh', 'KornShell', None),
319         ('dash', 'Debian Almquist Shell', None),
320         ('tcsh', 'TENEX C Shell', None),
321         ('zsh', 'Z Shell', None),
322         ('git', 'Git', (1, 7, 0)),
323         ('hg', 'Mercurial', (2, 0, 0)),
324         ('make', None, None),
325         ('sqlite3', 'SQLite 3', None),
326         ('nosetests', 'Nose', (1, 0, 0)),
327         ('emacs', 'Emacs', None),
328         ('xemacs', 'XEmacs', None),
329         ('vim', 'Vim', None),
330         ('vi', None, None),
331         ('nano', 'Nano', None),
332         ('kate', 'Kate', None),
333         ('notepad++', 'Notepad++', None),
334         ('firefox', 'Firefox', None),
335         ('google-chrome', 'Google Chrome', None),
336         ('chromium', 'Chromium', None),
337         ]:
338     if not long_name:
339         long_name = command
340     CHECKER[command] = CommandDependency(
341         command=command, long_name=long_name, minimum_version=minimum_version)
342 del command, long_name, minimum_version  # cleanup namespace
343
344
345 class EasyInstallDependency (CommandDependency):
346     def _get_version(self):
347         try:
348             return super(EasyInstallDependency, self)._get_version()
349         except DependencyError as e:
350             version_stream = self.version_stream
351             try:
352                 self.version_stream = 'stderr'
353                 stream = self._get_version_stream(expect=(1,))
354                 if 'option --version not recognized' in stream:
355                     return 'unknown (possibly Setuptools?)'
356             finally:
357                 self.version_stream = version_stream
358
359
360 CHECKER['easy_install'] = EasyInstallDependency(
361     command='easy_install', long_name='Setuptools easy_install',
362     minimum_version=None)
363
364
365 class PythonPackageDependency (Dependency):
366     def __init__(self, package, **kwargs):
367         if 'name' not in kwargs:
368             kwargs['name'] = package
369         if 'and_dependencies' not in kwargs:
370             kwargs['and_dependencies'] = []
371         if 'python' not in kwargs['and_dependencies']:
372             kwargs['and_dependencies'].append('python')
373         super(PythonPackageDependency, self).__init__(**kwargs)
374         self.package = package
375
376     def _get_version(self):
377         package = self._get_package(self.package)
378         return self._get_version_from_package(package)
379
380     def _get_package(self, package):
381         try:
382             return _importlib.import_module(package)
383         except ImportError as e:
384             raise DependencyError(
385                 checker=self,
386                 message="could not import the '{0}' package for {1}".format(
387                     package, self.full_name()),
388                 )# from e
389
390     def _get_version_from_package(self, package):
391         try:
392             version = package.__version__
393         except AttributeError:
394             version = None
395         return version
396
397
398 for package,name,long_name,minimum_version in [
399         ('nose', None, 'Nose Python package',
400          CHECKER['nosetests'].minimum_version),
401         ('IPython', None, None, None),
402         ('numpy', None, 'NumPy', None),
403         ('scipy', None, 'SciPy', None),
404         ('matplotlib', None, 'Matplotlib', None),
405         ('sympy', None, 'SymPy', None),
406         ('Cython', None, None, None),
407         ('networkx', None, 'NetworkX', None),
408         ('mayavi.mlab', None, 'MayaVi', None),
409         ('setuptools', None, 'Setuptools', None),
410         ]:
411     if not name:
412         name = package
413     if not long_name:
414         long_name = name
415     CHECKER[name] = PythonPackageDependency(
416         package=package, name=name, long_name=long_name,
417         minimum_version=minimum_version)
418 del package, name, long_name, minimum_version  # cleanup namespace
419
420
421 class MercurialPythonPackage (PythonPackageDependency):
422     def _get_version(self):
423         try:  # mercurial >= 1.2
424             package = _importlib.import_module('mercurial.util')
425         except ImportError as e:  # mercurial <= 1.1.2
426             package = self._get_package('mercurial.version')
427             return package.get_version()
428         else:
429             return package.version()
430
431
432 CHECKER['mercurial'] = MercurialPythonPackage(
433     package='mercurial.util', name='mercurial',
434     long_name='Mercurial Python package',
435     minimum_version=CHECKER['hg'].minimum_version)
436
437
438 class SQLitePythonPackage (PythonPackageDependency):
439     def _get_version_from_package(self, package):
440         return _sys.version
441
442     def _get_parsed_version(self):
443         return _sys.version_info
444
445
446 CHECKER['sqlite3-python'] = SQLitePythonPackage(
447     package='sqlite3', name='sqlite3-python',
448     long_name='SQLite Python package',
449     minimum_version=CHECKER['sqlite3'].minimum_version)
450
451
452 class VirtualDependency (Dependency):
453     def _check(self):
454         return '{0} {1}'.format(
455             self.or_pass['dependency'].full_name(),
456             self.or_pass['version'])
457
458
459 for name,dependencies in [
460         ('virtual-shell', (
461             'bash',
462             'dash',
463             'ash',
464             'zsh',
465             'ksh',
466             'csh',
467             'tcsh',
468             'sh',
469             )),
470         ('virtual-editor', (
471             'emacs',
472             'xemacs',
473             'vim',
474             'vi',
475             'nano',
476             'kate',
477             'notepad++',
478             )),
479         ('virtual-browser', (
480             'firefox',
481             'google-chrome',
482             'chromium',
483             )),
484         ]:
485     CHECKER[name] = VirtualDependency(
486         name=name, long_name=name, or_dependencies=dependencies)
487 del name, dependencies  # cleanup namespace
488
489
490 def _print_info(key, value, indent=19):
491     print('{0}{1}: {2}'.format(key, ' '*(indent-len(key)), value))
492
493 def print_system_info():
494     print("If you do not understand why the above failures occurred,")
495     print("copy and send the *entire* output (all info above and summary")
496     print("below) to the instructor for help.")
497     print()
498     print('==================')
499     print('System information')
500     print('==================')
501     _print_info('os.name', _os.name)
502     _print_info('os.uname', _platform.uname())
503     _print_info('platform', _sys.platform)
504     _print_info('platform+', _platform.platform())
505     for pversion in (
506             'linux_distribution',
507             'mac_ver',
508             'win32_ver',
509             ):
510         value = getattr(_platform, pversion)()
511         if value[0]:
512             _print_info(pversion, value)
513     _print_info('prefix', _sys.prefix)
514     _print_info('exec_prefix', _sys.exec_prefix)
515     _print_info('executable', _sys.executable)
516     _print_info('version_info', _sys.version_info)
517     _print_info('version', _sys.version)
518     _print_info('environment', '')
519     for key,value in sorted(_os.environ.items()):
520         print('  {0}={1}'.format(key, value))
521     print('==================')
522
523 def print_suggestions(instructor_fallback=True):
524     print()
525     print('For suggestions on installing missing packages, see')
526     print('http://software-carpentry.org/setup/')
527     print('')
528     print('For instructings on installing a particular package,')
529     print('see the failure message for that package printed above.')
530     if instructor_fallback:
531         print('')
532         print('For help, email the *entire* output of this script to')
533         print('your instructor.')
534
535
536 if __name__ == '__main__':
537     if not check(_sys.argv[1:]):
538         print()
539         print_system_info()
540         print_suggestions(instructor_fallback=True)