from __future__ import print_function # for Python 2.6 compatibility
import distutils.ccompiler as _distutils_ccompiler
+import fnmatch as _fnmatch
try: # Python 2.7 and 3.x
import importlib as _importlib
except ImportError: # Python 2.6 and earlier
import shlex as _shlex
import subprocess as _subprocess
import sys as _sys
+try: # Python 3.x
+ import urllib.parse as _urllib_parse
+except ImportError: # Python 2.x
+ import urllib as _urllib_parse # for quote()
if not hasattr(_shlex, 'quote'): # Python versions older than 3.3
'git',
'hg', # Command line tool
#'mercurial', # Python package
+ 'EasyMercurial',
# Build tools and packaging
'make',
'virtual-pypi-installer',
'setuptools',
+ #'xcode',
# Testing
'nosetests', # Command line tool
'nose', # Python package
'python',
'ipython', # Command line tool
'IPython', # Python package
+ 'argparse', # Useful for utility scripts
'numpy',
'scipy',
'matplotlib',
class DependencyError (Exception):
+ _default_url = 'http://software-carpentry.org/setup/'
+ _setup_urls = { # (system, version, package) glob pairs
+ ('*', '*', 'Cython'): 'http://docs.cython.org/src/quickstart/install.html',
+ ('Linux', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-linux',
+ ('Darwin', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-mac',
+ ('Windows', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-windows',
+ ('*', '*', 'EasyMercurial'): 'http://easyhg.org/download.html',
+ ('*', '*', 'argparse'): 'https://pypi.python.org/pypi/argparse#installation',
+ ('*', '*', 'ash'): 'http://www.in-ulm.de/~mascheck/various/ash/',
+ ('*', '*', 'bash'): 'http://www.gnu.org/software/bash/manual/html_node/Basic-Installation.html#Basic-Installation',
+ ('Linux', '*', 'chromium'): 'http://code.google.com/p/chromium/wiki/LinuxBuildInstructions',
+ ('Darwin', '*', 'chromium'): 'http://code.google.com/p/chromium/wiki/MacBuildInstructions',
+ ('Windows', '*', 'chromium'): 'http://www.chromium.org/developers/how-tos/build-instructions-windows',
+ ('*', '*', 'chromium'): 'http://www.chromium.org/developers/how-tos',
+ ('Windows', '*', 'emacs'): 'http://www.gnu.org/software/emacs/windows/Installing-Emacs.html',
+ ('*', '*', 'emacs'): 'http://www.gnu.org/software/emacs/#Obtaining',
+ ('*', '*', 'firefox'): 'http://www.mozilla.org/en-US/firefox/new/',
+ ('Linux', '*', 'gedit'): 'http://www.linuxfromscratch.org/blfs/view/svn/gnome/gedit.html',
+ ('*', '*', 'git'): 'http://git-scm.com/downloads',
+ ('*', '*', 'google-chrome'): 'https://www.google.com/intl/en/chrome/browser/',
+ ('*', '*', 'hg'): 'http://mercurial.selenic.com/',
+ ('*', '*', 'mercurial'): 'http://mercurial.selenic.com/',
+ ('*', '*', 'IPython'): 'http://ipython.org/install.html',
+ ('*', '*', 'ipython'): 'http://ipython.org/install.html',
+ ('*', '*', 'jinja'): 'http://jinja.pocoo.org/docs/intro/#installation',
+ ('*', '*', 'kate'): 'http://kate-editor.org/get-it/',
+ ('*', '*', 'make'): 'http://www.gnu.org/software/make/',
+ ('Darwin', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#building-on-osx',
+ ('Windows', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#installing-on-windows',
+ ('*', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#installing',
+ ('*', '*', 'mayavi.mlab'): 'http://docs.enthought.com/mayavi/mayavi/installation.html',
+ ('*', '*', 'nano'): 'http://www.nano-editor.org/dist/latest/faq.html#3',
+ ('*', '*', 'networkx'): 'http://networkx.github.com/documentation/latest/install.html#installing',
+ ('*', '*', 'nose'): 'https://nose.readthedocs.org/en/latest/#installation-and-quick-start',
+ ('*', '*', 'nosetests'): 'https://nose.readthedocs.org/en/latest/#installation-and-quick-start',
+ ('*', '*', 'notepad++'): 'http://notepad-plus-plus.org/download/v6.3.html',
+ ('*', '*', 'numpy'): 'http://docs.scipy.org/doc/numpy/user/install.html',
+ ('*', '*', 'pandas'): 'http://pandas.pydata.org/pandas-docs/stable/install.html',
+ ('*', '*', 'pip'): 'http://www.pip-installer.org/en/latest/installing.html',
+ ('*', '*', 'python'): 'http://www.python.org/download/releases/2.7.3/#download',
+ ('*', '*', 'pyzmq'): 'https://github.com/zeromq/pyzmq/wiki/Building-and-Installing-PyZMQ',
+ ('Linux', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Linux',
+ ('Darwin', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Mac_OS_X',
+ ('Windows', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Windows',
+ ('*', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy',
+ ('*', '*', 'setuptools'): 'https://pypi.python.org/pypi/setuptools#installation-instructions',
+ ('*', '*', 'sqlite3'): 'http://www.sqlite.org/download.html',
+ ('*', '*', 'sublime-text'): 'http://www.sublimetext.com/2',
+ ('*', '*', 'sympy'): 'http://docs.sympy.org/dev/install.html',
+ ('Darwin', '*', 'textmate'): 'http://macromates.com/',
+ ('Darwin', '*', 'textwrangler'): 'http://www.barebones.com/products/textwrangler/download.html',
+ ('*', '*', 'tornado'): 'http://www.tornadoweb.org/',
+ ('*', '*', 'vim'): 'http://www.vim.org/download.php',
+ ('Darwin', '*', 'xcode'): 'https://developer.apple.com/xcode/',
+ ('*', '*', 'xemacs'): 'http://www.us.xemacs.org/Install/',
+ ('*', '*', 'zsh'): 'http://www.zsh.org/',
+ }
+
def _get_message(self):
return self._message
def _set_message(self, message):
self._message = message
message = property(_get_message, _set_message)
- def __init__(self, checker, message, cause=None):
+ def __init__(self, checker, message, causes=None):
super(DependencyError, self).__init__(message)
self.checker = checker
self.message = message
- self.cause = cause
+ if causes is None:
+ causes = []
+ self.causes = causes
+
+ def get_url(self):
+ system = _platform.system()
+ version = None
+ for pversion in (
+ 'linux_distribution',
+ 'mac_ver',
+ 'win32_ver',
+ ):
+ value = getattr(_platform, pversion)()
+ if value[0]:
+ version = value[0]
+ break
+ package = self.checker.name
+ for (s,v,p),url in self._setup_urls.items():
+ if (_fnmatch.fnmatch(system, s) and
+ _fnmatch.fnmatch(version, v) and
+ _fnmatch.fnmatch(package, p)):
+ return url
+ return self._default_url
def __str__(self):
- url = 'http://software-carpentry.org/setup/' # TODO: per-package URL
+ url = self.get_url()
lines = [
'check for {0} failed:'.format(self.checker.full_name()),
' ' + self.message,
' For instructions on installing an up-to-date version, see',
' ' + url,
]
- if self.cause:
- lines.append(' cause:')
- lines.extend(' ' + line for line in str(self.cause).splitlines())
+ if self.causes:
+ lines.append(' causes:')
+ for cause in self.causes:
+ lines.extend(' ' + line for line in str(cause).splitlines())
return '\n'.join(lines)
except DependencyError as e:
raise DependencyError(
checker=self,
- message='and-dependency not satisfied for {0}'.format(
- self.full_name()),
- cause=e)
- self.or_pass = or_error = None
+ message=(
+ 'some dependencies for {0} were not satisfied'
+ ).format(self.full_name()),
+ causes=[e])
+ self.or_pass = None
+ or_errors = []
for dependency in self.or_dependencies:
if not hasattr(dependency, 'check'):
dependency = CHECKER[dependency]
try:
version = dependency.check()
except DependencyError as e:
- or_error = e
+ or_errors.append(e)
else:
self.or_pass = {
'dependency': dependency,
if self.or_dependencies and not self.or_pass:
raise DependencyError(
checker=self,
- message='or-dependency not satisfied for {0}'.format(
- self.full_name()),
- cause=or_error)
+ message=(
+ '{0} requires at least one of the following dependencies'
+ ).format(self.full_name()),
+ causes=or_errors)
def _check(self):
version = self._get_version()
('zsh', 'Z Shell', None),
('git', 'Git', (1, 7, 0)),
('hg', 'Mercurial', (2, 0, 0)),
+ ('EasyMercurial', None, (1, 3)),
('pip', None, None),
('sqlite3', 'SQLite 3', None),
('nosetests', 'Nose', (1, 0, 0)),
minimum_version=None)
+class PathCommandDependency (CommandDependency):
+ """A command that doesn't support --version or equivalent options
+
+ On some operating systems (e.g. OS X), a command's executable may
+ be hard to find, or not exist in the PATH. Work around that by
+ just checking for the existence of a characteristic file or
+ directory. Since the characteristic path may depend on OS,
+ installed version, etc., take a list of paths, and succeed if any
+ of them exists.
+ """
+ def __init__(self, paths, **kwargs):
+ super(PathCommandDependency, self).__init__(self, **kwargs)
+ self.paths = paths
+
+ def _get_version_stream(self, *args, **kwargs):
+ raise NotImplementedError()
+
+ def _get_version(self):
+ for path in self.paths:
+ if _os.path.exists(path):
+ return None
+ raise DependencyError(
+ checker=self,
+ message=(
+ 'nothing exists at any of the expected paths for {0}:\n {1}'
+ ).format(
+ self.full_name(),
+ '\n '.join(p for p in self.paths)))
+
+
+for paths,name,long_name in [
+ ([_os.path.join(_os.sep, 'Applications', 'Sublime Text 2.app')],
+ 'sublime-text', 'Sublime Text'),
+ ([_os.path.join(_os.sep, 'Applications', 'TextMate.app')],
+ 'textmate', 'TextMate'),
+ ([_os.path.join(_os.sep, 'Applications', 'TextWrangler.app')],
+ 'textwrangler', 'TextWrangler'),
+ ([_os.path.join(_os.sep, 'Applications', 'Xcode.app'), # OS X >=1.7
+ _os.path.join(_os.sep, 'Developer', 'Applications', 'Xcode.app'
+ ) # OS X 1.6,
+ ],
+ 'xcode', 'Xcode'),
+ ]:
+ if not long_name:
+ long_name = name
+ CHECKER[name] = PathCommandDependency(
+ paths=paths, name=name, long_name=long_name)
+del paths, name, long_name # cleanup namespace
+
+
class PythonPackageDependency (Dependency):
def __init__(self, package, **kwargs):
if 'name' not in kwargs:
for package,name,long_name,minimum_version,and_dependencies in [
('nose', None, 'Nose Python package',
CHECKER['nosetests'].minimum_version, None),
+ ('jinja2', 'jinja', 'Jinja', (2, 6), None),
+ ('zmq', 'pyzmq', 'PyZMQ', (2, 1, 4), None),
('IPython', None, 'IPython Python package',
- CHECKER['ipython'].minimum_version, None),
+ CHECKER['ipython'].minimum_version, ['jinja', 'tornado', 'pyzmq']),
+ ('argparse', None, 'Argparse', None, None),
('numpy', None, 'NumPy', None, None),
('scipy', None, 'SciPy', None, None),
('matplotlib', None, 'Matplotlib', None, None),
minimum_version=CHECKER['hg'].minimum_version)
+class TornadoPythonPackage (PythonPackageDependency):
+ def _get_version_from_package(self, package):
+ return package.version
+
+ def _get_parsed_version(self):
+ package = self._get_package(self.package)
+ return package.version_info
+
+
+CHECKER['tornado'] = TornadoPythonPackage(
+ package='tornado', name='tornado', long_name='Tornado', minimum_version=(2, 0))
+
+
class SQLitePythonPackage (PythonPackageDependency):
def _get_version_from_package(self, package):
return _sys.version
minimum_version=CHECKER['sqlite3'].minimum_version)
+class UserTaskDependency (Dependency):
+ "Prompt the user to complete a task and check for success"
+ def __init__(self, prompt, **kwargs):
+ super(UserTaskDependency, self).__init__(**kwargs)
+ self.prompt = prompt
+
+ def _check(self):
+ if _sys.version_info >= (3, ):
+ result = input(self.prompt)
+ else: # Python 2.x
+ result = raw_input(self.prompt)
+ return self._check_result(result)
+
+ def _check_result(self, result):
+ raise NotImplementedError()
+
+
+class EditorTaskDependency (UserTaskDependency):
+ def __init__(self, **kwargs):
+ self.path = _os.path.expanduser(_os.path.join(
+ '~', 'swc-installation-test.txt'))
+ self.contents = 'Hello, world!'
+ super(EditorTaskDependency, self).__init__(
+ prompt=(
+ 'Open your favorite text editor and create the file\n'
+ ' {0}\n'
+ 'containing the line:\n'
+ ' {1}\n'
+ 'Press enter here after you have done this.\n'
+ 'You may remove the file after you have finished testing.'
+ ).format(self.path, self.contents),
+ **kwargs)
+
+ def _check_result(self, result):
+ message = None
+ try:
+ with open(self.path, 'r') as f:
+ contents = f.read()
+ except IOError as e:
+ raise DependencyError(
+ checker=self,
+ message='could not open {0!r}: {1}'.format(self.path, e)
+ )# from e
+ if contents.strip() != self.contents:
+ raise DependencyError(
+ checker=self,
+ message=(
+ 'file contents ({0!r}) did not match the expected {1!r}'
+ ).format(contents, self.contents))
+
+
+CHECKER['other-editor'] = EditorTaskDependency(
+ name='other-editor', long_name='')
+
+
class VirtualDependency (Dependency):
def _check(self):
return '{0} {1}'.format(
self.or_pass['version'])
-for name,dependencies in [
- ('virtual-shell', (
+for name,long_name,dependencies in [
+ ('virtual-shell', 'command line shell', (
'bash',
'dash',
'ash',
'tcsh',
'sh',
)),
- ('virtual-editor', (
+ ('virtual-editor', 'text/code editor', (
'emacs',
'xemacs',
'vim',
'gedit',
'kate',
'notepad++',
+ 'sublime-text',
+ 'textmate',
+ 'textwrangler',
+ 'other-editor', # last because it requires user interaction
)),
- ('virtual-browser', (
+ ('virtual-browser', 'web browser', (
'firefox',
'google-chrome',
'chromium',
)),
- ('virtual-pypi-installer', (
+ ('virtual-pypi-installer', 'PyPI installer', (
'easy_install',
'pip',
)),
]:
CHECKER[name] = VirtualDependency(
- name=name, long_name=name, or_dependencies=dependencies)
-del name, dependencies # cleanup namespace
+ name=name, long_name=long_name, or_dependencies=dependencies)
+del name, long_name, dependencies # cleanup namespace
def _print_info(key, value, indent=19):
if __name__ == '__main__':
+ import optparse as _optparse
+
+ parser = _optparse.OptionParser(usage='%prog [options] [check...]')
+ epilog = __doc__
+ parser.format_epilog = lambda formatter: '\n' + epilog
+ parser.add_option(
+ '-v', '--verbose', action='store_true',
+ help=('print additional information to help troubleshoot '
+ 'installation issues'))
+ options,args = parser.parse_args()
try:
- passed = check(_sys.argv[1:])
+ passed = check(args)
except InvalidCheck as e:
print("I don't know how to check for {0!r}".format(e.check))
print('I do know how to check for:')
print(' {0}'.format(key))
_sys.exit(1)
if not passed:
- print()
- print_system_info()
- print_suggestions(instructor_fallback=True)
+ if options.verbose:
+ print()
+ print_system_info()
+ print_suggestions(instructor_fallback=True)
_sys.exit(1)