Fix decorator in VersionPlistCommandDependency
[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 import fnmatch as _fnmatch
29 try:  # Python 2.7 and 3.x
30     import importlib as _importlib
31 except ImportError:  # Python 2.6 and earlier
32     class _Importlib (object):
33         """Minimal workarounds for functions we need
34         """
35         @staticmethod
36         def import_module(name):
37             module = __import__(name)
38             for n in name.split('.')[1:]:
39                 module = getattr(module, n)
40             return module
41     _importlib = _Importlib()
42 import logging as _logging
43 import os as _os
44 import platform as _platform
45 import re as _re
46 import shlex as _shlex
47 import subprocess as _subprocess
48 import sys as _sys
49 try:  # Python 3.x
50     import urllib.parse as _urllib_parse
51 except ImportError:  # Python 2.x
52     import urllib as _urllib_parse  # for quote()
53 import xml.etree.ElementTree as _element_tree
54
55
56 if not hasattr(_shlex, 'quote'):  # Python versions older than 3.3
57     # Use the undocumented pipes.quote()
58     import pipes as _pipes
59     _shlex.quote = _pipes.quote
60
61
62 __version__ = '0.1'
63
64 # Comment out any entries you don't need
65 CHECKS = [
66 # Shell
67     'virtual-shell',
68 # Editors
69     'virtual-editor',
70 # Browsers
71     'virtual-browser',
72 # Version control
73     'git',
74     'hg',              # Command line tool
75     #'mercurial',       # Python package
76     'EasyMercurial',
77 # Build tools and packaging
78     'make',
79     'virtual-pypi-installer',
80     'setuptools',
81     #'xcode',
82 # Testing
83     'nosetests',       # Command line tool
84     'nose',            # Python package
85     'py.test',         # Command line tool
86     'pytest',          # Python package
87 # SQL
88     'sqlite3',         # Command line tool
89     'sqlite3-python',  # Python package
90 # Python
91     'python',
92     'ipython',         # Command line tool
93     'IPython',         # Python package
94     'argparse',        # Useful for utility scripts
95     'numpy',
96     'scipy',
97     'matplotlib',
98     'pandas',
99     'sympy',
100     'Cython',
101     'networkx',
102     'mayavi.mlab',
103     ]
104
105 CHECKER = {}
106
107 _ROOT_PATH = _os.sep
108 if _platform.system() == 'win32':
109     _ROOT_PATH = 'c:\\'
110
111
112 class InvalidCheck (KeyError):
113     def __init__(self, check):
114         super(InvalidCheck, self).__init__(check)
115         self.check = check
116
117     def __str__(self):
118         return self.check
119
120
121 class DependencyError (Exception):
122     _default_url = 'http://software-carpentry.org/setup/'
123     _setup_urls = {  # (system, version, package) glob pairs
124         ('*', '*', 'Cython'): 'http://docs.cython.org/src/quickstart/install.html',
125         ('Linux', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-linux',
126         ('Darwin', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-mac',
127         ('Windows', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-windows',
128         ('*', '*', 'EasyMercurial'): 'http://easyhg.org/download.html',
129         ('*', '*', 'argparse'): 'https://pypi.python.org/pypi/argparse#installation',
130         ('*', '*', 'ash'): 'http://www.in-ulm.de/~mascheck/various/ash/',
131         ('*', '*', 'bash'): 'http://www.gnu.org/software/bash/manual/html_node/Basic-Installation.html#Basic-Installation',
132         ('Linux', '*', 'chromium'): 'http://code.google.com/p/chromium/wiki/LinuxBuildInstructions',
133         ('Darwin', '*', 'chromium'): 'http://code.google.com/p/chromium/wiki/MacBuildInstructions',
134         ('Windows', '*', 'chromium'): 'http://www.chromium.org/developers/how-tos/build-instructions-windows',
135         ('*', '*', 'chromium'): 'http://www.chromium.org/developers/how-tos',
136         ('Windows', '*', 'emacs'): 'http://www.gnu.org/software/emacs/windows/Installing-Emacs.html',
137         ('*', '*', 'emacs'): 'http://www.gnu.org/software/emacs/#Obtaining',
138         ('*', '*', 'firefox'): 'http://www.mozilla.org/en-US/firefox/new/',
139         ('Linux', '*', 'gedit'): 'http://www.linuxfromscratch.org/blfs/view/svn/gnome/gedit.html',
140         ('*', '*', 'git'): 'http://git-scm.com/downloads',
141         ('*', '*', 'google-chrome'): 'https://www.google.com/intl/en/chrome/browser/',
142         ('*', '*', 'hg'): 'http://mercurial.selenic.com/',
143         ('*', '*', 'mercurial'): 'http://mercurial.selenic.com/',
144         ('*', '*', 'IPython'): 'http://ipython.org/install.html',
145         ('*', '*', 'ipython'): 'http://ipython.org/install.html',
146         ('*', '*', 'jinja'): 'http://jinja.pocoo.org/docs/intro/#installation',
147         ('*', '*', 'kate'): 'http://kate-editor.org/get-it/',
148         ('*', '*', 'make'): 'http://www.gnu.org/software/make/',
149         ('Darwin', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#building-on-osx',
150         ('Windows', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#installing-on-windows',
151         ('*', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#installing',
152         ('*', '*', 'mayavi.mlab'): 'http://docs.enthought.com/mayavi/mayavi/installation.html',
153         ('*', '*', 'nano'): 'http://www.nano-editor.org/dist/latest/faq.html#3',
154         ('*', '*', 'networkx'): 'http://networkx.github.com/documentation/latest/install.html#installing',
155         ('*', '*', 'nose'): 'https://nose.readthedocs.org/en/latest/#installation-and-quick-start',
156         ('*', '*', 'nosetests'): 'https://nose.readthedocs.org/en/latest/#installation-and-quick-start',
157         ('*', '*', 'notepad++'): 'http://notepad-plus-plus.org/download/v6.3.html',
158         ('*', '*', 'numpy'): 'http://docs.scipy.org/doc/numpy/user/install.html',
159         ('*', '*', 'pandas'): 'http://pandas.pydata.org/pandas-docs/stable/install.html',
160         ('*', '*', 'pip'): 'http://www.pip-installer.org/en/latest/installing.html',
161         ('*', '*', 'pytest'): 'http://pytest.org/latest/getting-started.html',
162         ('*', '*', 'python'): 'http://www.python.org/download/releases/2.7.3/#download',
163         ('*', '*', 'pyzmq'): 'https://github.com/zeromq/pyzmq/wiki/Building-and-Installing-PyZMQ',
164         ('*', '*', 'py.test'): 'http://pytest.org/latest/getting-started.html',
165         ('Linux', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Linux',
166         ('Darwin', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Mac_OS_X',
167         ('Windows', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Windows',
168         ('*', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy',
169         ('*', '*', 'setuptools'): 'https://pypi.python.org/pypi/setuptools#installation-instructions',
170         ('*', '*', 'sqlite3'): 'http://www.sqlite.org/download.html',
171         ('*', '*', 'sublime-text'): 'http://www.sublimetext.com/2',
172         ('*', '*', 'sympy'): 'http://docs.sympy.org/dev/install.html',
173         ('Darwin', '*', 'textmate'): 'http://macromates.com/',
174         ('Darwin', '*', 'textwrangler'): 'http://www.barebones.com/products/textwrangler/download.html',
175         ('*', '*', 'tornado'): 'http://www.tornadoweb.org/',
176         ('*', '*', 'vim'): 'http://www.vim.org/download.php',
177         ('Darwin', '*', 'xcode'): 'https://developer.apple.com/xcode/',
178         ('*', '*', 'xemacs'): 'http://www.us.xemacs.org/Install/',
179         ('*', '*', 'zsh'): 'http://www.zsh.org/',
180         }
181
182     def _get_message(self):
183         return self._message
184     def _set_message(self, message):
185         self._message = message
186     message = property(_get_message, _set_message)
187
188     def __init__(self, checker, message, causes=None):
189         super(DependencyError, self).__init__(message)
190         self.checker = checker
191         self.message = message
192         if causes is None:
193             causes = []
194         self.causes = causes
195
196     def get_url(self):
197         system = _platform.system()
198         version = None
199         for pversion in (
200             'linux_distribution',
201             'mac_ver',
202             'win32_ver',
203             ):
204             value = getattr(_platform, pversion)()
205             if value[0]:
206                 version = value[0]
207                 break
208         package = self.checker.name
209         for (s,v,p),url in self._setup_urls.items():
210             if (_fnmatch.fnmatch(system, s) and
211                     _fnmatch.fnmatch(version, v) and
212                     _fnmatch.fnmatch(package, p)):
213                 return url
214         return self._default_url
215
216     def __str__(self):
217         url = self.get_url()
218         lines = [
219             'check for {0} failed:'.format(self.checker.full_name()),
220             '  ' + self.message,
221             '  For instructions on installing an up-to-date version, see',
222             '  ' + url,
223             ]
224         if self.causes:
225             lines.append('  causes:')
226             for cause in self.causes:
227                 lines.extend('  ' + line for line in str(cause).splitlines())
228         return '\n'.join(lines)
229
230
231 def check(checks=None):
232     successes = []
233     failures = []
234     if not checks:
235         checks = CHECKS
236     for check in checks:
237         try:
238             checker = CHECKER[check]
239         except KeyError as e:
240             raise InvalidCheck(check)# from e
241         _sys.stdout.write('check {0}...\t'.format(checker.full_name()))
242         try:
243             version = checker.check()
244         except DependencyError as e:
245             failures.append(e)
246             _sys.stdout.write('fail\n')
247         else:
248             _sys.stdout.write('pass\n')
249             successes.append((checker, version))
250     if successes:
251         print('\nSuccesses:\n')
252         for checker,version in successes:
253             print('{0} {1}'.format(
254                     checker.full_name(),
255                     version or 'unknown'))
256     if failures:
257         print('\nFailures:')
258         printed = []
259         for failure in failures:
260             if failure not in printed:
261                 print()
262                 print(failure)
263                 printed.append(failure)
264         return False
265     return True
266
267
268 class Dependency (object):
269     def __init__(self, name, long_name=None, minimum_version=None,
270                  version_delimiter='.', and_dependencies=None,
271                  or_dependencies=None):
272         self.name = name
273         self.long_name = long_name or name
274         self.minimum_version = minimum_version
275         self.version_delimiter = version_delimiter
276         if not and_dependencies:
277             and_dependencies = []
278         self.and_dependencies = and_dependencies
279         if not or_dependencies:
280             or_dependencies = []
281         self.or_dependencies = or_dependencies
282         self._check_error = None
283
284     def __str__(self):
285         return '<{0} {1}>'.format(type(self).__name__, self.name)
286
287     def full_name(self):
288         if self.name == self.long_name:
289             return self.name
290         else:
291             return '{0} ({1})'.format(self.long_name, self.name)
292
293     def check(self):
294         if self._check_error:
295             raise self._check_error
296         try:
297             self._check_dependencies()
298             return self._check()
299         except DependencyError as e:
300             self._check_error = e  # cache for future calls
301             raise
302
303     def _check_dependencies(self):
304         for dependency in self.and_dependencies:
305             if not hasattr(dependency, 'check'):
306                 dependency = CHECKER[dependency]
307             try:
308                 dependency.check()
309             except DependencyError as e:
310                 raise DependencyError(
311                     checker=self,
312                     message=(
313                         'some dependencies for {0} were not satisfied'
314                         ).format(self.full_name()),
315                     causes=[e])
316         self.or_pass = None
317         or_errors = []
318         for dependency in self.or_dependencies:
319             if not hasattr(dependency, 'check'):
320                 dependency = CHECKER[dependency]
321             try:
322                 version = dependency.check()
323             except DependencyError as e:
324                 or_errors.append(e)
325             else:
326                 self.or_pass = {
327                     'dependency': dependency,
328                     'version': version,
329                     }
330                 break  # no need to test other dependencies
331         if self.or_dependencies and not self.or_pass:
332             raise DependencyError(
333                 checker=self,
334                 message=(
335                     '{0} requires at least one of the following dependencies'
336                     ).format(self.full_name()),
337                     causes=or_errors)
338
339     def _check(self):
340         version = self._get_version()
341         parsed_version = None
342         if hasattr(self, '_get_parsed_version'):
343             parsed_version = self._get_parsed_version()
344         if self.minimum_version:
345             self._check_version(version=version, parsed_version=parsed_version)
346         return version
347
348     def _get_version(self):
349         raise NotImplementedError(self)
350
351     def _minimum_version_string(self):
352         return self.version_delimiter.join(
353             str(part) for part in self.minimum_version)
354
355     def _check_version(self, version, parsed_version=None):
356         if not parsed_version:
357             parsed_version = self._parse_version(version=version)
358         if not parsed_version or parsed_version < self.minimum_version:
359             raise DependencyError(
360                 checker=self,
361                 message='outdated version of {0}: {1} (need >= {2})'.format(
362                     self.full_name(), version, self._minimum_version_string()))
363
364     def _parse_version(self, version):
365         if not version:
366             return None
367         parsed_version = []
368         for part in version.split(self.version_delimiter):
369             try:
370                 parsed_version.append(int(part))
371             except ValueError as e:
372                 raise DependencyError(
373                     checker=self,
374                     message=(
375                         'unparsable {0!r} in version {1} of {2}, (need >= {3})'
376                         ).format(
377                         part, version, self.full_name(),
378                         self._minimum_version_string()))# from e
379         return tuple(parsed_version)
380
381
382 class VirtualDependency (Dependency):
383     def _check(self):
384         return '{0} {1}'.format(
385             self.or_pass['dependency'].full_name(),
386             self.or_pass['version'])
387
388
389 class CommandDependency (Dependency):
390     exe_extension = _distutils_ccompiler.new_compiler().exe_extension
391
392     def __init__(self, command, paths=None, version_options=('--version',),
393                  stdin=None, version_regexp=None, version_stream='stdout',
394                  **kwargs):
395         if 'name' not in kwargs:
396             kwargs['name'] = command
397         super(CommandDependency, self).__init__(**kwargs)
398         self.command = command
399         self.paths = paths
400         self.version_options = version_options
401         self.stdin = None
402         if not version_regexp:
403             regexp = r'([\d][\d{0}]*[\d])'.format(self.version_delimiter)
404             version_regexp = _re.compile(regexp)
405         self.version_regexp = version_regexp
406         self.version_stream = version_stream
407
408     def _get_command_version_stream(self, command=None, stdin=None,
409                                     expect=(0,)):
410         if command is None:
411             command = self.command + (self.exe_extension or '')
412         if not stdin:
413             stdin = self.stdin
414         if stdin:
415             popen_stdin = _subprocess.PIPE
416         else:
417             popen_stdin = None
418         try:
419             p = _subprocess.Popen(
420                 [command] + list(self.version_options), stdin=popen_stdin,
421                 stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
422                 universal_newlines=True)
423         except OSError as e:
424             raise DependencyError(
425                 checker=self,
426                 message="could not find '{0}' executable".format(command),
427                 )# from e
428         stdout,stderr = p.communicate(stdin)
429         status = p.wait()
430         if status not in expect:
431             lines = [
432                 "failed to execute: {0} {1}".format(
433                     command,
434                     ' '.join(_shlex.quote(arg)
435                              for arg in self.version_options)),
436                 'status: {0}'.format(status),
437                 ]
438             for name,string in [('stdout', stdout), ('stderr', stderr)]:
439                 if string:
440                     lines.extend([name + ':', string])
441             raise DependencyError(checker=self, message='\n'.join(lines))
442         for name,string in [('stdout', stdout), ('stderr', stderr)]:
443             if name == self.version_stream:
444                 if not string:
445                     raise DependencyError(
446                         checker=self,
447                         message='empty version stream on {0} for {1}'.format(
448                             self.version_stream, command))
449                 return string
450         raise NotImplementedError(self.version_stream)
451
452     def _get_version_stream(self, **kwargs):
453         paths = [self.command + (self.exe_extension or '')]
454         if self.exe_extension:
455             paths.append(self.command)  # also look at the extension-less path
456         if self.paths:
457             paths.extend(self.paths)
458         or_errors = []
459         for path in paths:
460             try:
461                 return self._get_command_version_stream(command=path, **kwargs)
462             except DependencyError as e:
463                 or_errors.append(e)
464         raise DependencyError(
465             checker=self,
466             message='errors finding {0} version'.format(
467                 self.full_name()),
468             causes=or_errors)
469
470     def _get_version(self):
471         version_stream = self._get_version_stream()
472         match = self.version_regexp.search(version_stream)
473         if not match:
474             raise DependencyError(
475                 checker=self,
476                 message='no version string in output:\n{0}'.format(
477                     version_stream))
478         return match.group(1)
479
480
481 class VersionPlistCommandDependency (CommandDependency):
482     """A command that doesn't support --version or equivalent options
483
484     On OS X, a command's executable may be hard to find, or not exist
485     in the PATH.  Work around that by looking up the version
486     information in the package's version.plist file.
487     """
488     def __init__(self, key='CFBundleShortVersionString', **kwargs):
489         super(VersionPlistCommandDependency, self).__init__(**kwargs)
490         self.key = key
491
492     def _get_command_version_stream(self, *args, **kwargs):
493         raise NotImplementedError()
494
495     def _get_version_stream(self, *args, **kwargs):
496         raise NotImplementedError()
497
498     @staticmethod
499     def _get_parent(root, element):
500         """Returns the parent of this element or None for the root element
501         """
502         for node in root.iter():
503             if element in node:
504                 return node
505         raise ValueError((root, element))
506
507     @classmethod
508     def _get_next(cls, root, element):
509         """Returns the following sibling of this element or None
510         """
511         parent = cls._get_parent(root=root, element=element)
512         siblings = iter(parent)
513         for node in siblings:
514             if node == element:
515                 try:
516                     return next(siblings)
517                 except StopIteration:
518                     return None
519         return None
520
521     def _get_version_from_plist(self, path):
522         """Parse the plist and return the value string for self.key
523         """
524         tree = _element_tree.parse(source=path)
525         data = {}
526         for key in tree.findall('.//key'):
527             value = self._get_next(root=tree, element=key)
528             if value.tag != 'string':
529                 raise ValueError((tree, key, value))
530             data[key.text] = value.text
531         return data[self.key]
532
533     def _get_version(self):
534         for path in self.paths:
535             if _os.path.exists(path):
536                 return self._get_version_from_plist(path=path)
537         raise DependencyError(
538             checker=self,
539             message=(
540                 'nothing exists at any of the expected paths for {0}:\n    {1}'
541                 ).format(
542                 self.full_name(),
543                 '\n    '.join(p for p in self.paths)))
544
545
546 class UserTaskDependency (Dependency):
547     "Prompt the user to complete a task and check for success"
548     def __init__(self, prompt, **kwargs):
549         super(UserTaskDependency, self).__init__(**kwargs)
550         self.prompt = prompt
551
552     def _check(self):
553         if _sys.version_info >= (3, ):
554             result = input(self.prompt)
555         else:  # Python 2.x
556             result = raw_input(self.prompt)
557         return self._check_result(result)
558
559     def _check_result(self, result):
560         raise NotImplementedError()
561
562
563 class EditorTaskDependency (UserTaskDependency):
564     def __init__(self, **kwargs):
565         self.path = _os.path.expanduser(_os.path.join(
566                 '~', 'swc-installation-test.txt'))
567         self.contents = 'Hello, world!'
568         super(EditorTaskDependency, self).__init__(
569             prompt=(
570                 'Open your favorite text editor and create the file\n'
571                 '  {0}\n'
572                 'containing the line:\n'
573                 '  {1}\n'
574                 'Press enter here after you have done this.\n'
575                 'You may remove the file after you have finished testing.'
576                 ).format(self.path, self.contents),
577             **kwargs)
578
579     def _check_result(self, result):
580         message = None
581         try:
582             with open(self.path, 'r') as f:
583                 contents = f.read()
584         except IOError as e:
585             raise DependencyError(
586                 checker=self,
587                 message='could not open {0!r}: {1}'.format(self.path, e)
588                 )# from e
589         if contents.strip() != self.contents:
590             raise DependencyError(
591                 checker=self,
592                 message=(
593                     'file contents ({0!r}) did not match the expected {1!r}'
594                     ).format(contents, self.contents))
595
596
597 class MakeDependency (CommandDependency):
598     makefile = '\n'.join([
599             'all:',
600             '\t@echo "MAKE_VERSION=$(MAKE_VERSION)"',
601             '\t@echo "MAKE=$(MAKE)"',
602             '',
603             ])
604
605     def _get_version(self):
606         try:
607             return super(MakeDependency, self)._get_version()
608         except DependencyError as e:
609             version_options = self.version_options
610             self.version_options = ['-f', '-']
611             try:
612                 stream = self._get_version_stream(stdin=self.makefile)
613                 info = {}
614                 for line in stream.splitlines():
615                     try:
616                         key,value = line.split('=', 1)
617                     except ValueError as ve:
618                         raise e# from NotImplementedError(stream)
619                     info[key] = value
620                 if info.get('MAKE_VERSION', None):
621                     return info['MAKE_VERSION']
622                 elif info.get('MAKE', None):
623                     return None
624                 raise e
625             finally:
626                 self.version_options = version_options
627
628
629 class EasyInstallDependency (CommandDependency):
630     def _get_version(self):
631         try:
632             return super(EasyInstallDependency, self)._get_version()
633         except DependencyError as e:
634             version_stream = self.version_stream
635             try:
636                 self.version_stream = 'stderr'
637                 stream = self._get_version_stream(expect=(1,))
638                 if 'option --version not recognized' in stream:
639                     return 'unknown (possibly Setuptools?)'
640             finally:
641                 self.version_stream = version_stream
642
643
644 class PythonDependency (Dependency):
645     def __init__(self, name='python', long_name='Python version',
646                  minimum_version=(2, 6), **kwargs):
647         super(PythonDependency, self).__init__(
648             name=name, long_name=long_name, minimum_version=minimum_version,
649             **kwargs)
650
651     def _get_version(self):
652         return _sys.version
653
654     def _get_parsed_version(self):
655         return _sys.version_info
656
657
658 class PythonPackageDependency (Dependency):
659     def __init__(self, package, **kwargs):
660         if 'name' not in kwargs:
661             kwargs['name'] = package
662         if 'and_dependencies' not in kwargs:
663             kwargs['and_dependencies'] = []
664         if 'python' not in kwargs['and_dependencies']:
665             kwargs['and_dependencies'].append('python')
666         super(PythonPackageDependency, self).__init__(**kwargs)
667         self.package = package
668
669     def _get_version(self):
670         package = self._get_package(self.package)
671         return self._get_version_from_package(package)
672
673     def _get_package(self, package):
674         try:
675             return _importlib.import_module(package)
676         except ImportError as e:
677             raise DependencyError(
678                 checker=self,
679                 message="could not import the '{0}' package for {1}".format(
680                     package, self.full_name()),
681                 )# from e
682
683     def _get_version_from_package(self, package):
684         try:
685             version = package.__version__
686         except AttributeError:
687             version = None
688         return version
689
690
691 class MercurialPythonPackage (PythonPackageDependency):
692     def _get_version(self):
693         try:  # mercurial >= 1.2
694             package = _importlib.import_module('mercurial.util')
695         except ImportError as e:  # mercurial <= 1.1.2
696             package = self._get_package('mercurial.version')
697             return package.get_version()
698         else:
699             return package.version()
700
701
702 class TornadoPythonPackage (PythonPackageDependency):
703     def _get_version_from_package(self, package):
704         return package.version
705
706     def _get_parsed_version(self):
707         package = self._get_package(self.package)
708         return package.version_info
709
710
711 class SQLitePythonPackage (PythonPackageDependency):
712     def _get_version_from_package(self, package):
713         return _sys.version
714
715     def _get_parsed_version(self):
716         return _sys.version_info
717
718
719 def _program_files_paths(*args):
720     "Utility for generating MS Windows search paths"
721     pf = _os.environ.get('ProgramFiles', '/usr/bin')
722     pfx86 = _os.environ.get('ProgramFiles(x86)', pf)
723     paths = [_os.path.join(pf, *args)]
724     if pfx86 != pf:
725         paths.append(_os.path.join(pfx86, *args))
726     return paths
727
728
729 CHECKER['python'] = PythonDependency()
730
731
732 for command,long_name,minimum_version,paths in [
733         ('sh', 'Bourne Shell', None, None),
734         ('ash', 'Almquist Shell', None, None),
735         ('bash', 'Bourne Again Shell', None, None),
736         ('csh', 'C Shell', None, None),
737         ('ksh', 'KornShell', None, None),
738         ('dash', 'Debian Almquist Shell', None, None),
739         ('tcsh', 'TENEX C Shell', None, None),
740         ('zsh', 'Z Shell', None, None),
741         ('git', 'Git', (1, 7, 0), None),
742         ('hg', 'Mercurial', (2, 0, 0), None),
743         ('EasyMercurial', None, (1, 3), None),
744         ('pip', None, None, None),
745         ('sqlite3', 'SQLite 3', None, None),
746         ('nosetests', 'Nose', (1, 0, 0), None),
747         ('ipython', 'IPython script', (1, 0), None),
748         ('emacs', 'Emacs', None, None),
749         ('xemacs', 'XEmacs', None, None),
750         ('vim', 'Vim', None, None),
751         ('vi', None, None, None),
752         ('nano', 'Nano', None, None),
753         ('gedit', None, None, None),
754         ('kate', 'Kate', None, None),
755         ('notepad++', 'Notepad++', None,
756          _program_files_paths('Notepad++', 'notepad++.exe')),
757         ('firefox', 'Firefox', None,
758          _program_files_paths('Mozilla Firefox', 'firefox.exe')),
759         ('google-chrome', 'Google Chrome', None,
760          _program_files_paths('Google', 'Chrome', 'Application', 'chrome.exe')
761          ),
762         ('chromium', 'Chromium', None, None),
763         ]:
764     if not long_name:
765         long_name = command
766     CHECKER[command] = CommandDependency(
767         command=command, paths=paths, long_name=long_name,
768         minimum_version=minimum_version)
769 del command, long_name, minimum_version, paths  # cleanup namespace
770
771
772 CHECKER['make'] = MakeDependency(command='make', minimum_version=None)
773
774
775 CHECKER['easy_install'] = EasyInstallDependency(
776     command='easy_install', long_name='Setuptools easy_install',
777     minimum_version=None)
778
779
780 CHECKER['py.test'] = CommandDependency(
781     command='py.test', version_stream='stderr',
782     minimum_version=None)
783
784
785 for paths,name,long_name in [
786         ([_os.path.join(_ROOT_PATH, 'Applications', 'Sublime Text 2.app',
787                         'Contents', 'version.plist')],
788          'sublime-text', 'Sublime Text'),
789         ([_os.path.join(_ROOT_PATH, 'Applications', 'TextMate.app',
790                         'Contents', 'version.plist')],
791          'textmate', 'TextMate'),
792         ([_os.path.join(_ROOT_PATH, 'Applications', 'TextWrangler.app',
793                         'Contents', 'version.plist')],
794          'textwrangler', 'TextWrangler'),
795         ([_os.path.join(_ROOT_PATH, 'Applications', 'Safari.app',
796                         'Contents', 'version.plist')],
797          'safari', 'Safari'),
798         ([_os.path.join(_ROOT_PATH, 'Applications', 'Xcode.app',
799                         'Contents', 'version.plist'),  # OS X >=1.7
800           _os.path.join(_ROOT_PATH, 'Developer', 'Applications', 'Xcode.app',
801                         'Contents', 'version.plist'),  # OS X 1.6,
802           ],
803          'xcode', 'Xcode'),
804         ]:
805     if not long_name:
806         long_name = name
807     CHECKER[name] = VersionPlistCommandDependency(
808         command=None, paths=paths, name=name, long_name=long_name)
809 del paths, name, long_name  # cleanup namespace
810
811
812 for package,name,long_name,minimum_version,and_dependencies in [
813         ('nose', None, 'Nose Python package',
814          CHECKER['nosetests'].minimum_version, None),
815         ('pytest', None, 'pytest Python package',
816          CHECKER['py.test'].minimum_version, None),
817         ('jinja2', 'jinja', 'Jinja', (2, 6), None),
818         ('zmq', 'pyzmq', 'PyZMQ', (2, 1, 4), None),
819         ('IPython', None, 'IPython Python package',
820          CHECKER['ipython'].minimum_version, [
821              'jinja',
822              'tornado',
823              'pyzmq',
824              VirtualDependency(
825                  name='virtual-browser-ipython',
826                  long_name='IPython-compatible web browser',
827                  or_dependencies=[
828                      CommandDependency(
829                          command=CHECKER['firefox'].command,
830                          paths=CHECKER['firefox'].paths,
831                          name='{0}-for-ipython'.format(
832                              CHECKER['firefox'].name),
833                          long_name='{0} for IPython'.format(
834                              CHECKER['firefox'].long_name),
835                          minimum_version=(6, 0)),
836                      CommandDependency(
837                          command=CHECKER['google-chrome'].command,
838                          paths=CHECKER['google-chrome'].paths,
839                          name='{0}-for-ipython'.format(
840                              CHECKER['google-chrome'].name),
841                          long_name='{0} for IPython'.format(
842                              CHECKER['google-chrome'].long_name),
843                          minimum_version=(13, 0)),
844                      CommandDependency(
845                          command=CHECKER['chromium'].command,
846                          paths=CHECKER['chromium'].paths,
847                          name='{0}-for-ipython'.format(
848                              CHECKER['chromium'].name),
849                          long_name='{0} for IPython'.format(
850                              CHECKER['chromium'].long_name),
851                          minimum_version=(13, 0)),
852                      VersionPlistCommandDependency(
853                          command=CHECKER['safari'].command,
854                          paths=CHECKER['safari'].paths,
855                          key=CHECKER['safari'].key,
856                          name='{0}-for-ipython'.format(
857                              CHECKER['safari'].name),
858                          long_name='{0} for IPython'.format(
859                              CHECKER['safari'].long_name),
860                          minimum_version=(5, 0)),
861                  ]),
862          ]),
863         ('argparse', None, 'Argparse', None, None),
864         ('numpy', None, 'NumPy', None, None),
865         ('scipy', None, 'SciPy', None, None),
866         ('matplotlib', None, 'Matplotlib', None, None),
867         ('pandas', None, 'Pandas', (0, 8), None),
868         ('sympy', None, 'SymPy', None, None),
869         ('Cython', None, None, None, None),
870         ('networkx', None, 'NetworkX', None, None),
871         ('mayavi.mlab', None, 'MayaVi', None, None),
872         ('setuptools', None, 'Setuptools', None, None),
873         ]:
874     if not name:
875         name = package
876     if not long_name:
877         long_name = name
878     kwargs = {}
879     if and_dependencies:
880         kwargs['and_dependencies'] = and_dependencies
881     CHECKER[name] = PythonPackageDependency(
882         package=package, name=name, long_name=long_name,
883         minimum_version=minimum_version, **kwargs)
884 # cleanup namespace
885 del package, name, long_name, minimum_version, and_dependencies, kwargs
886
887
888 CHECKER['mercurial'] = MercurialPythonPackage(
889     package='mercurial.util', name='mercurial',
890     long_name='Mercurial Python package',
891     minimum_version=CHECKER['hg'].minimum_version)
892
893
894 CHECKER['tornado'] = TornadoPythonPackage(
895     package='tornado', name='tornado', long_name='Tornado', minimum_version=(2, 0))
896
897
898 CHECKER['sqlite3-python'] = SQLitePythonPackage(
899     package='sqlite3', name='sqlite3-python',
900     long_name='SQLite Python package',
901     minimum_version=CHECKER['sqlite3'].minimum_version)
902
903
904 CHECKER['other-editor'] = EditorTaskDependency(
905     name='other-editor', long_name='')
906
907
908 for name,long_name,dependencies in [
909         ('virtual-shell', 'command line shell', (
910             'bash',
911             'dash',
912             'ash',
913             'zsh',
914             'ksh',
915             'csh',
916             'tcsh',
917             'sh',
918             )),
919         ('virtual-editor', 'text/code editor', (
920             'emacs',
921             'xemacs',
922             'vim',
923             'vi',
924             'nano',
925             'gedit',
926             'kate',
927             'notepad++',
928             'sublime-text',
929             'textmate',
930             'textwrangler',
931             'other-editor',  # last because it requires user interaction
932             )),
933         ('virtual-browser', 'web browser', (
934             'firefox',
935             'google-chrome',
936             'chromium',
937             'safari',
938             )),
939         ('virtual-pypi-installer', 'PyPI installer', (
940             'pip',
941             'easy_install',
942             )),
943         ]:
944     CHECKER[name] = VirtualDependency(
945         name=name, long_name=long_name, or_dependencies=dependencies)
946 del name, long_name, dependencies  # cleanup namespace
947
948
949 def _print_info(key, value, indent=19):
950     print('{0}{1}: {2}'.format(key, ' '*(indent-len(key)), value))
951
952 def print_system_info():
953     print("If you do not understand why the above failures occurred,")
954     print("copy and send the *entire* output (all info above and summary")
955     print("below) to the instructor for help.")
956     print()
957     print('==================')
958     print('System information')
959     print('==================')
960     _print_info('os.name', _os.name)
961     _print_info('os.uname', _platform.uname())
962     _print_info('platform', _sys.platform)
963     _print_info('platform+', _platform.platform())
964     for pversion in (
965             'linux_distribution',
966             'mac_ver',
967             'win32_ver',
968             ):
969         value = getattr(_platform, pversion)()
970         if value[0]:
971             _print_info(pversion, value)
972     _print_info('prefix', _sys.prefix)
973     _print_info('exec_prefix', _sys.exec_prefix)
974     _print_info('executable', _sys.executable)
975     _print_info('version_info', _sys.version_info)
976     _print_info('version', _sys.version)
977     _print_info('environment', '')
978     for key,value in sorted(_os.environ.items()):
979         print('  {0}={1}'.format(key, value))
980     print('==================')
981
982 def print_suggestions(instructor_fallback=True):
983     print()
984     print('For suggestions on installing missing packages, see')
985     print('http://software-carpentry.org/setup/')
986     print('')
987     print('For instructings on installing a particular package,')
988     print('see the failure message for that package printed above.')
989     if instructor_fallback:
990         print('')
991         print('For help, email the *entire* output of this script to')
992         print('your instructor.')
993
994
995 if __name__ == '__main__':
996     import optparse as _optparse
997
998     parser = _optparse.OptionParser(usage='%prog [options] [check...]')
999     epilog = __doc__
1000     parser.format_epilog = lambda formatter: '\n' + epilog
1001     parser.add_option(
1002         '-v', '--verbose', action='store_true',
1003         help=('print additional information to help troubleshoot '
1004               'installation issues'))
1005     options,args = parser.parse_args()
1006     try:
1007         passed = check(args)
1008     except InvalidCheck as e:
1009         print("I don't know how to check for {0!r}".format(e.check))
1010         print('I do know how to check for:')
1011         for key,checker in sorted(CHECKER.items()):
1012             if checker.long_name != checker.name:
1013                 print('  {0} {1}({2})'.format(
1014                         key, ' '*(20-len(key)), checker.long_name))
1015             else:
1016                 print('  {0}'.format(key))
1017         _sys.exit(1)
1018     if not passed:
1019         if options.verbose:
1020             print()
1021             print_system_info()
1022             print_suggestions(instructor_fallback=True)
1023         _sys.exit(1)