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