3 """Test script to check for required functionality.
5 Execute this code at the command line by typing:
7 python swc-installation-test-2.py
9 Run the script and follow the instructions it prints at the end.
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'.
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:
18 python swc-installation-test-2.py git virtual-editor
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
25 from __future__ import print_function # for Python 2.6 compatibility
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
35 def import_module(name):
36 module = __import__(name)
37 for n in name.split('.')[1:]:
38 module = getattr(module, n)
40 _importlib = _Importlib()
41 import logging as _logging
43 import platform as _platform
45 import shlex as _shlex
46 import subprocess as _subprocess
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
58 # Comment out any entries you don't need
68 'hg', # Command line tool
69 #'mercurial', # Python package
71 # Build tools and packaging
73 'virtual-pypi-installer',
77 'nosetests', # Command line tool
78 'nose', # Python package
80 'sqlite3', # Command line tool
81 'sqlite3-python', # Python package
84 'ipython', # Command line tool
85 'IPython', # Python package
99 class InvalidCheck (KeyError):
100 def __init__(self, check):
101 super(InvalidCheck, self).__init__(check)
108 class DependencyError (Exception):
109 def _get_message(self):
111 def _set_message(self, message):
112 self._message = message
113 message = property(_get_message, _set_message)
115 def __init__(self, checker, message, causes=None):
116 super(DependencyError, self).__init__(message)
117 self.checker = checker
118 self.message = message
124 url = 'http://software-carpentry.org/setup/' # TODO: per-package URL
126 'check for {0} failed:'.format(self.checker.full_name()),
128 ' For instructions on installing an up-to-date version, see',
132 lines.append(' causes:')
133 for cause in self.causes:
134 lines.extend(' ' + line for line in str(cause).splitlines())
135 return '\n'.join(lines)
138 def check(checks=None):
145 checker = CHECKER[check]
146 except KeyError as e:
147 raise InvalidCheck(check)# from e
148 _sys.stdout.write('check {0}...\t'.format(checker.full_name()))
150 version = checker.check()
151 except DependencyError as e:
153 _sys.stdout.write('fail\n')
155 _sys.stdout.write('pass\n')
156 successes.append((checker, version))
158 print('\nSuccesses:\n')
159 for checker,version in successes:
160 print('{0} {1}'.format(
162 version or 'unknown'))
166 for failure in failures:
167 if failure not in printed:
170 printed.append(failure)
175 class Dependency (object):
176 def __init__(self, name, long_name=None, minimum_version=None,
177 version_delimiter='.', and_dependencies=None,
178 or_dependencies=None):
180 self.long_name = long_name or name
181 self.minimum_version = minimum_version
182 self.version_delimiter = version_delimiter
183 if not and_dependencies:
184 and_dependencies = []
185 self.and_dependencies = and_dependencies
186 if not or_dependencies:
188 self.or_dependencies = or_dependencies
189 self._check_error = None
192 return '<{0} {1}>'.format(type(self).__name__, self.name)
195 if self.name == self.long_name:
198 return '{0} ({1})'.format(self.long_name, self.name)
201 if self._check_error:
202 raise self._check_error
204 self._check_dependencies()
206 except DependencyError as e:
207 self._check_error = e # cache for future calls
210 def _check_dependencies(self):
211 for dependency in self.and_dependencies:
212 if not hasattr(dependency, 'check'):
213 dependency = CHECKER[dependency]
216 except DependencyError as e:
217 raise DependencyError(
220 'some dependencies for {0} were not satisfied'
221 ).format(self.full_name()),
225 for dependency in self.or_dependencies:
226 if not hasattr(dependency, 'check'):
227 dependency = CHECKER[dependency]
229 version = dependency.check()
230 except DependencyError as e:
234 'dependency': dependency,
237 break # no need to test other dependencies
238 if self.or_dependencies and not self.or_pass:
239 raise DependencyError(
242 '{0} requires at least one of the following dependencies'
243 ).format(self.full_name()),
247 version = self._get_version()
248 parsed_version = None
249 if hasattr(self, '_get_parsed_version'):
250 parsed_version = self._get_parsed_version()
251 if self.minimum_version:
252 self._check_version(version=version, parsed_version=parsed_version)
255 def _get_version(self):
256 raise NotImplementedError(self)
258 def _minimum_version_string(self):
259 return self.version_delimiter.join(
260 str(part) for part in self.minimum_version)
262 def _check_version(self, version, parsed_version=None):
263 if not parsed_version:
264 parsed_version = self._parse_version(version=version)
265 if not parsed_version or parsed_version < self.minimum_version:
266 raise DependencyError(
268 message='outdated version of {0}: {1} (need >= {2})'.format(
269 self.full_name(), version, self._minimum_version_string()))
271 def _parse_version(self, version):
275 for part in version.split(self.version_delimiter):
277 parsed_version.append(int(part))
278 except ValueError as e:
279 raise DependencyError(
282 'unparsable {0!r} in version {1} of {2}, (need >= {3})'
284 part, version, self.full_name(),
285 self._minimum_version_string()))# from e
286 return tuple(parsed_version)
289 class PythonDependency (Dependency):
290 def __init__(self, name='python', long_name='Python version',
291 minimum_version=(2, 6), **kwargs):
292 super(PythonDependency, self).__init__(
293 name=name, long_name=long_name, minimum_version=minimum_version,
296 def _get_version(self):
299 def _get_parsed_version(self):
300 return _sys.version_info
303 CHECKER['python'] = PythonDependency()
306 class CommandDependency (Dependency):
307 exe_extension = _distutils_ccompiler.new_compiler().exe_extension
309 def __init__(self, command, version_options=('--version',), stdin=None,
310 version_regexp=None, version_stream='stdout', **kwargs):
311 if 'name' not in kwargs:
312 kwargs['name'] = command
313 super(CommandDependency, self).__init__(**kwargs)
314 self.command = command
315 self.version_options = version_options
317 if not version_regexp:
318 regexp = r'([\d][\d{0}]*[\d])'.format(self.version_delimiter)
319 version_regexp = _re.compile(regexp)
320 self.version_regexp = version_regexp
321 self.version_stream = version_stream
323 def _get_version_stream(self, stdin=None, expect=(0,)):
327 popen_stdin = _subprocess.PIPE
330 command = self.command + (self.exe_extension or '')
332 p = _subprocess.Popen(
333 [command] + list(self.version_options), stdin=popen_stdin,
334 stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
335 close_fds=True, shell=False, universal_newlines=True)
337 raise DependencyError(
339 message="could not find '{0}' executable".format(command),
341 stdout,stderr = p.communicate(stdin)
343 if status not in expect:
345 "failed to execute: {0} {1}".format(
347 ' '.join(_shlex.quote(arg)
348 for arg in self.version_options)),
349 'status: {0}'.format(status),
351 for name,string in [('stdout', stdout), ('stderr', stderr)]:
353 lines.extend([name + ':', string])
354 raise DependencyError(checker=self, message='\n'.join(lines))
355 for name,string in [('stdout', stdout), ('stderr', stderr)]:
356 if name == self.version_stream:
358 raise NotImplementedError(self.version_stream)
360 def _get_version(self):
361 version_stream = self._get_version_stream()
362 match = self.version_regexp.search(version_stream)
364 raise DependencyError(
366 message='no version string in output:\n{0}'.format(
368 return match.group(1)
371 for command,long_name,minimum_version in [
372 ('sh', 'Bourne Shell', None),
373 ('ash', 'Almquist Shell', None),
374 ('bash', 'Bourne Again Shell', None),
375 ('csh', 'C Shell', None),
376 ('ksh', 'KornShell', None),
377 ('dash', 'Debian Almquist Shell', None),
378 ('tcsh', 'TENEX C Shell', None),
379 ('zsh', 'Z Shell', None),
380 ('git', 'Git', (1, 7, 0)),
381 ('hg', 'Mercurial', (2, 0, 0)),
382 ('EasyMercurial', None, (1, 3)),
384 ('sqlite3', 'SQLite 3', None),
385 ('nosetests', 'Nose', (1, 0, 0)),
386 ('ipython', 'IPython script', (0, 13)),
387 ('emacs', 'Emacs', None),
388 ('xemacs', 'XEmacs', None),
389 ('vim', 'Vim', None),
391 ('nano', 'Nano', None),
392 ('gedit', None, None),
393 ('kate', 'Kate', None),
394 ('notepad++', 'Notepad++', None),
395 ('firefox', 'Firefox', None),
396 ('google-chrome', 'Google Chrome', None),
397 ('chromium', 'Chromium', None),
401 CHECKER[command] = CommandDependency(
402 command=command, long_name=long_name, minimum_version=minimum_version)
403 del command, long_name, minimum_version # cleanup namespace
406 class MakeDependency (CommandDependency):
407 makefile = '\n'.join([
409 '\t@echo "MAKE_VERSION=$(MAKE_VERSION)"',
410 '\t@echo "MAKE=$(MAKE)"',
414 def _get_version(self):
416 return super(MakeDependency, self)._get_version()
417 except DependencyError as e:
418 version_options = self.version_options
419 self.version_options = ['-f', '-']
421 stream = self._get_version_stream(stdin=self.makefile)
423 for line in stream.splitlines():
425 key,value = line.split('=', 1)
426 except ValueError as ve:
427 raise e# from NotImplementedError(stream)
429 if info.get('MAKE_VERSION', None):
430 return info['MAKE_VERSION']
431 elif info.get('MAKE', None):
435 self.version_options = version_options
438 CHECKER['make'] = MakeDependency(command='make', minimum_version=None)
441 class EasyInstallDependency (CommandDependency):
442 def _get_version(self):
444 return super(EasyInstallDependency, self)._get_version()
445 except DependencyError as e:
446 version_stream = self.version_stream
448 self.version_stream = 'stderr'
449 stream = self._get_version_stream(expect=(1,))
450 if 'option --version not recognized' in stream:
451 return 'unknown (possibly Setuptools?)'
453 self.version_stream = version_stream
456 CHECKER['easy_install'] = EasyInstallDependency(
457 command='easy_install', long_name='Setuptools easy_install',
458 minimum_version=None)
461 class PathCommandDependency (CommandDependency):
462 """A command that doesn't support --version or equivalent options
464 On some operating systems (e.g. OS X), a command's executable may
465 be hard to find, or not exist in the PATH. Work around that by
466 just checking for the existence of a characteristic file or
467 directory. Since the characteristic path may depend on OS,
468 installed version, etc., take a list of paths, and succeed if any
471 def __init__(self, paths, **kwargs):
472 super(PathCommandDependency, self).__init__(self, **kwargs)
475 def _get_version_stream(self, *args, **kwargs):
476 raise NotImplementedError()
478 def _get_version(self):
479 for path in self.paths:
480 if _os.path.exists(path):
482 raise DependencyError(
485 'nothing exists at any of the expected paths for {0}:\n {1}'
488 '\n '.join(p for p in self.paths)))
491 for paths,name,long_name in [
492 ([_os.path.join(_os.sep, 'Applications', 'Sublime Text 2.app')],
493 'sublime-text', 'Sublime Text'),
494 ([_os.path.join(_os.sep, 'Applications', 'TextMate.app')],
495 'textmate', 'TextMate'),
496 ([_os.path.join(_os.sep, 'Applications', 'TextWrangler.app')],
497 'textwrangler', 'TextWrangler'),
498 ([_os.path.join(_os.sep, 'Applications', 'Xcode.app'), # OS X >=1.7
499 _os.path.join(_os.sep, 'Developer', 'Applications', 'Xcode.app'
506 CHECKER[name] = PathCommandDependency(
507 paths=paths, name=name, long_name=long_name)
508 del paths, name, long_name # cleanup namespace
511 class PythonPackageDependency (Dependency):
512 def __init__(self, package, **kwargs):
513 if 'name' not in kwargs:
514 kwargs['name'] = package
515 if 'and_dependencies' not in kwargs:
516 kwargs['and_dependencies'] = []
517 if 'python' not in kwargs['and_dependencies']:
518 kwargs['and_dependencies'].append('python')
519 super(PythonPackageDependency, self).__init__(**kwargs)
520 self.package = package
522 def _get_version(self):
523 package = self._get_package(self.package)
524 return self._get_version_from_package(package)
526 def _get_package(self, package):
528 return _importlib.import_module(package)
529 except ImportError as e:
530 raise DependencyError(
532 message="could not import the '{0}' package for {1}".format(
533 package, self.full_name()),
536 def _get_version_from_package(self, package):
538 version = package.__version__
539 except AttributeError:
544 for package,name,long_name,minimum_version,and_dependencies in [
545 ('nose', None, 'Nose Python package',
546 CHECKER['nosetests'].minimum_version, None),
547 ('jinja2', 'jinja', 'Jinja', (2, 6), None),
548 ('zmq', 'pyzmq', 'PyZMQ', (2, 1, 4), None),
549 ('IPython', None, 'IPython Python package',
550 CHECKER['ipython'].minimum_version, ['jinja', 'tornado', 'pyzmq']),
551 ('numpy', None, 'NumPy', None, None),
552 ('scipy', None, 'SciPy', None, None),
553 ('matplotlib', None, 'Matplotlib', None, None),
554 ('pandas', None, 'Pandas', (0, 8), None),
555 ('sympy', None, 'SymPy', None, None),
556 ('Cython', None, None, None, None),
557 ('networkx', None, 'NetworkX', None, None),
558 ('mayavi.mlab', None, 'MayaVi', None, None),
559 ('setuptools', None, 'Setuptools', None, None),
567 kwargs['and_dependencies'] = and_dependencies
568 CHECKER[name] = PythonPackageDependency(
569 package=package, name=name, long_name=long_name,
570 minimum_version=minimum_version, **kwargs)
572 del package, name, long_name, minimum_version, and_dependencies, kwargs
575 class MercurialPythonPackage (PythonPackageDependency):
576 def _get_version(self):
577 try: # mercurial >= 1.2
578 package = _importlib.import_module('mercurial.util')
579 except ImportError as e: # mercurial <= 1.1.2
580 package = self._get_package('mercurial.version')
581 return package.get_version()
583 return package.version()
586 CHECKER['mercurial'] = MercurialPythonPackage(
587 package='mercurial.util', name='mercurial',
588 long_name='Mercurial Python package',
589 minimum_version=CHECKER['hg'].minimum_version)
592 class TornadoPythonPackage (PythonPackageDependency):
593 def _get_version_from_package(self, package):
594 return package.version
596 def _get_parsed_version(self):
597 package = self._get_package(self.package)
598 return package.version_info
601 CHECKER['tornado'] = TornadoPythonPackage(
602 package='tornado', name='tornado', long_name='Tornado', minimum_version=(2, 0))
605 class SQLitePythonPackage (PythonPackageDependency):
606 def _get_version_from_package(self, package):
609 def _get_parsed_version(self):
610 return _sys.version_info
613 CHECKER['sqlite3-python'] = SQLitePythonPackage(
614 package='sqlite3', name='sqlite3-python',
615 long_name='SQLite Python package',
616 minimum_version=CHECKER['sqlite3'].minimum_version)
619 class UserTaskDependency (Dependency):
620 "Prompt the user to complete a task and check for success"
621 def __init__(self, prompt, **kwargs):
622 super(UserTaskDependency, self).__init__(**kwargs)
626 if _sys.version_info >= (3, ):
627 result = input(self.prompt)
629 result = raw_input(self.prompt)
630 return self._check_result(result)
632 def _check_result(self, result):
633 raise NotImplementedError()
636 class EditorTaskDependency (UserTaskDependency):
637 def __init__(self, **kwargs):
638 self.path = _os.path.expanduser(_os.path.join(
639 '~', 'swc-installation-test.txt'))
640 self.contents = 'Hello, world!'
641 super(EditorTaskDependency, self).__init__(
643 'Open your favorite text editor and create the file\n'
645 'containing the line:\n'
647 'Press enter here after you have done this.\n'
648 'You may remove the file after you have finished testing.'
649 ).format(self.path, self.contents),
652 def _check_result(self, result):
655 with open(self.path, 'r') as f:
658 raise DependencyError(
660 message='could not open {0!r}: {1}'.format(self.path, e)
662 if contents.strip() != self.contents:
663 raise DependencyError(
666 'file contents ({0!r}) did not match the expected {1!r}'
667 ).format(contents, self.contents))
670 CHECKER['other-editor'] = EditorTaskDependency(
671 name='other-editor', long_name='')
674 class VirtualDependency (Dependency):
676 return '{0} {1}'.format(
677 self.or_pass['dependency'].full_name(),
678 self.or_pass['version'])
681 for name,long_name,dependencies in [
682 ('virtual-shell', 'command line shell', (
692 ('virtual-editor', 'text/code editor', (
704 'other-editor', # last because it requires user interaction
706 ('virtual-browser', 'web browser', (
711 ('virtual-pypi-installer', 'PyPI installer', (
716 CHECKER[name] = VirtualDependency(
717 name=name, long_name=long_name, or_dependencies=dependencies)
718 del name, long_name, dependencies # cleanup namespace
721 def _print_info(key, value, indent=19):
722 print('{0}{1}: {2}'.format(key, ' '*(indent-len(key)), value))
724 def print_system_info():
725 print("If you do not understand why the above failures occurred,")
726 print("copy and send the *entire* output (all info above and summary")
727 print("below) to the instructor for help.")
729 print('==================')
730 print('System information')
731 print('==================')
732 _print_info('os.name', _os.name)
733 _print_info('os.uname', _platform.uname())
734 _print_info('platform', _sys.platform)
735 _print_info('platform+', _platform.platform())
737 'linux_distribution',
741 value = getattr(_platform, pversion)()
743 _print_info(pversion, value)
744 _print_info('prefix', _sys.prefix)
745 _print_info('exec_prefix', _sys.exec_prefix)
746 _print_info('executable', _sys.executable)
747 _print_info('version_info', _sys.version_info)
748 _print_info('version', _sys.version)
749 _print_info('environment', '')
750 for key,value in sorted(_os.environ.items()):
751 print(' {0}={1}'.format(key, value))
752 print('==================')
754 def print_suggestions(instructor_fallback=True):
756 print('For suggestions on installing missing packages, see')
757 print('http://software-carpentry.org/setup/')
759 print('For instructings on installing a particular package,')
760 print('see the failure message for that package printed above.')
761 if instructor_fallback:
763 print('For help, email the *entire* output of this script to')
764 print('your instructor.')
767 if __name__ == '__main__':
769 passed = check(_sys.argv[1:])
770 except InvalidCheck as e:
771 print("I don't know how to check for {0!r}".format(e.check))
772 print('I do know how to check for:')
773 for key,checker in sorted(CHECKER.items()):
774 if checker.long_name != checker.name:
775 print(' {0} {1}({2})'.format(
776 key, ' '*(20-len(key)), checker.long_name))
778 print(' {0}'.format(key))
783 print_suggestions(instructor_fallback=True)