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