Merge branch 'master' into bc-namespaced
[swc-setup-installation-test.git] / setup / swc-installation-test-2.py
index 3c1fc090beacd90291eacb14691a1b4302b73c90..bd7960d58c96444e915c02d7582a20040d6a1323 100755 (executable)
@@ -22,6 +22,25 @@ version of a particular dependency, and you just want to re-test that
 dependency.
 """
 
+# Some details about the implementation:
+
+# The dependencies are divided into a hierarchy of classes rooted on
+# Dependency class. You can refer to the code to see which package
+# comes under which type of dependency.
+
+# The CHECKER dictionary stores information about all the dependencies
+# and CHECKS stores list of the dependencies which are to be checked in
+# the current workshop.
+
+# In the "__name__ == '__main__'" block, we launch all the checks with
+# check() function, which prints information about the tests as they run
+# and details about the failures after the tests complete. In case of
+# failure, the functions print_system_info() and print_suggestions()
+# are called after this, where the former prints information about the
+# user's system for debugging purposes while the latter prints some
+# suggestions to follow.
+
+
 from __future__ import print_function  # for Python 2.6 compatibility
 
 import distutils.ccompiler as _distutils_ccompiler
@@ -50,6 +69,7 @@ try:  # Python 3.x
     import urllib.parse as _urllib_parse
 except ImportError:  # Python 2.x
     import urllib as _urllib_parse  # for quote()
+import xml.etree.ElementTree as _element_tree
 
 
 if not hasattr(_shlex, 'quote'):  # Python versions older than 3.3
@@ -81,6 +101,8 @@ CHECKS = [
 # Testing
     'nosetests',       # Command line tool
     'nose',            # Python package
+    'py.test',         # Command line tool
+    'pytest',          # Python package
 # SQL
     'sqlite3',         # Command line tool
     'sqlite3-python',  # Python package
@@ -93,10 +115,10 @@ CHECKS = [
     'scipy',
     'matplotlib',
     'pandas',
-    'sympy',
-    'Cython',
-    'networkx',
-    'mayavi.mlab',
+    #'sympy',
+    #'Cython',
+    #'networkx',
+    #'mayavi.mlab',
     ]
 
 CHECKER = {}
@@ -155,8 +177,10 @@ class DependencyError (Exception):
         ('*', '*', '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',
+        ('*', '*', 'pytest'): 'http://pytest.org/latest/getting-started.html',
         ('*', '*', 'python'): 'http://www.python.org/download/releases/2.7.3/#download',
         ('*', '*', 'pyzmq'): 'https://github.com/zeromq/pyzmq/wiki/Building-and-Installing-PyZMQ',
+        ('*', '*', 'py.test'): 'http://pytest.org/latest/getting-started.html',
         ('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',
@@ -374,21 +398,11 @@ class Dependency (object):
         return tuple(parsed_version)
 
 
-class PythonDependency (Dependency):
-    def __init__(self, name='python', long_name='Python version',
-                 minimum_version=(2, 6), **kwargs):
-        super(PythonDependency, self).__init__(
-            name=name, long_name=long_name, minimum_version=minimum_version,
-            **kwargs)
-
-    def _get_version(self):
-        return _sys.version
-
-    def _get_parsed_version(self):
-        return _sys.version_info
-
-
-CHECKER['python'] = PythonDependency()
+class VirtualDependency (Dependency):
+    def _check(self):
+        return '{0} {1}'.format(
+            self.or_pass['dependency'].full_name(),
+            self.or_pass['version'])
 
 
 class CommandDependency (Dependency):
@@ -455,25 +469,22 @@ class CommandDependency (Dependency):
         raise NotImplementedError(self.version_stream)
 
     def _get_version_stream(self, **kwargs):
-        try:
-            return self._get_command_version_stream(**kwargs)
-        except DependencyError as e:
-            if self.paths:
-                or_errors = [e]
-                for path in self.paths:
-                    try:
-                        return self._get_command_version_stream(
-                            command=path, **kwargs)
-                    except DependencyError as e:
-                        print('a')
-                        or_errors.append(e)
-                raise DependencyError(
-                    checker=self,
-                    message='errors finding {0} version'.format(
-                        self.full_name()),
-                    causes=or_errors)
-            else:
-                raise
+        paths = [self.command + (self.exe_extension or '')]
+        if self.exe_extension:
+            paths.append(self.command)  # also look at the extension-less path
+        if self.paths:
+            paths.extend(self.paths)
+        or_errors = []
+        for path in paths:
+            try:
+                return self._get_command_version_stream(command=path, **kwargs)
+            except DependencyError as e:
+                or_errors.append(e)
+        raise DependencyError(
+            checker=self,
+            message='errors finding {0} version'.format(
+                self.full_name()),
+            causes=or_errors)
 
     def _get_version(self):
         version_stream = self._get_version_stream()
@@ -486,50 +497,120 @@ class CommandDependency (Dependency):
         return match.group(1)
 
 
-for command,long_name,minimum_version,paths in [
-        ('sh', 'Bourne Shell', None, None),
-        ('ash', 'Almquist Shell', None, None),
-        ('bash', 'Bourne Again Shell', None, None),
-        ('csh', 'C Shell', None, None),
-        ('ksh', 'KornShell', None, None),
-        ('dash', 'Debian Almquist Shell', None, None),
-        ('tcsh', 'TENEX C Shell', None, None),
-        ('zsh', 'Z Shell', None, None),
-        ('git', 'Git', (1, 7, 0), None),
-        ('hg', 'Mercurial', (2, 0, 0), None),
-        ('EasyMercurial', None, (1, 3), None),
-        ('pip', None, None, None),
-        ('sqlite3', 'SQLite 3', None, None),
-        ('nosetests', 'Nose', (1, 0, 0), None),
-        ('ipython', 'IPython script', (0, 13), None),
-        ('emacs', 'Emacs', None, None),
-        ('xemacs', 'XEmacs', None, None),
-        ('vim', 'Vim', None, None),
-        ('vi', None, None, None),
-        ('nano', 'Nano', None, None),
-        ('gedit', None, None, None),
-        ('kate', 'Kate', None, None),
-        ('notepad++', 'Notepad++', None, [
-            _os.path.join(
-                _ROOT_PATH, 'Program Files', 'Notepad++', 'notepad++.exe'),
-            ]),
-        ('firefox', 'Firefox', None, [
-            _os.path.join(
-                _ROOT_PATH, 'Program Files', 'Mozilla Firefox', 'firefox.exe'),
-            ]),
-        ('google-chrome', 'Google Chrome', None, [
-            _os.path.join(
-                _ROOT_PATH, 'Program Files', 'Google', 'Chrome', 'Application',
-                'chrome.exe'),
-            ]),
-        ('chromium', 'Chromium', None, None),
-        ]:
-    if not long_name:
-        long_name = command
-    CHECKER[command] = CommandDependency(
-        command=command, paths=paths, long_name=long_name,
-        minimum_version=minimum_version)
-del command, long_name, minimum_version, paths  # cleanup namespace
+class VersionPlistCommandDependency (CommandDependency):
+    """A command that doesn't support --version or equivalent options
+
+    On OS X, a command's executable may be hard to find, or not exist
+    in the PATH.  Work around that by looking up the version
+    information in the package's version.plist file.
+    """
+    def __init__(self, key='CFBundleShortVersionString', **kwargs):
+        super(VersionPlistCommandDependency, self).__init__(**kwargs)
+        self.key = key
+
+    def _get_command_version_stream(self, *args, **kwargs):
+        raise NotImplementedError()
+
+    def _get_version_stream(self, *args, **kwargs):
+        raise NotImplementedError()
+
+    @staticmethod
+    def _get_parent(root, element):
+        """Returns the parent of this element or None for the root element
+        """
+        for node in root.iter():
+            if element in node:
+                return node
+        raise ValueError((root, element))
+
+    @classmethod
+    def _get_next(cls, root, element):
+        """Returns the following sibling of this element or None
+        """
+        parent = cls._get_parent(root=root, element=element)
+        siblings = iter(parent)
+        for node in siblings:
+            if node == element:
+                try:
+                    return next(siblings)
+                except StopIteration:
+                    return None
+        return None
+
+    def _get_version_from_plist(self, path):
+        """Parse the plist and return the value string for self.key
+        """
+        tree = _element_tree.parse(source=path)
+        data = {}
+        for key in tree.findall('.//key'):
+            value = self._get_next(root=tree, element=key)
+            if value.tag != 'string':
+                raise ValueError((tree, key, value))
+            data[key.text] = value.text
+        return data[self.key]
+
+    def _get_version(self):
+        for path in self.paths:
+            if _os.path.exists(path):
+                return self._get_version_from_plist(path=path)
+        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)))
+
+
+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))
 
 
 class MakeDependency (CommandDependency):
@@ -564,9 +645,6 @@ class MakeDependency (CommandDependency):
                 self.version_options = version_options
 
 
-CHECKER['make'] = MakeDependency(command='make', minimum_version=None)
-
-
 class EasyInstallDependency (CommandDependency):
     def _get_version(self):
         try:
@@ -582,60 +660,18 @@ class EasyInstallDependency (CommandDependency):
                 self.version_stream = version_stream
 
 
-CHECKER['easy_install'] = EasyInstallDependency(
-    command='easy_install', long_name='Setuptools easy_install',
-    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 _get_command_version_stream(self, *args, **kwargs):
-        raise NotImplementedError()
-
-    def _get_version_stream(self, *args, **kwargs):
-        raise NotImplementedError()
+class PythonDependency (Dependency):
+    def __init__(self, name='python', long_name='Python version',
+                 minimum_version=(2, 6), **kwargs):
+        super(PythonDependency, self).__init__(
+            name=name, long_name=long_name, minimum_version=minimum_version,
+            **kwargs)
 
     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)))
-
+        return _sys.version
 
-for paths,name,long_name in [
-        ([_os.path.join(_ROOT_PATH, 'Applications', 'Sublime Text 2.app')],
-         'sublime-text', 'Sublime Text'),
-        ([_os.path.join(_ROOT_PATH, 'Applications', 'TextMate.app')],
-         'textmate', 'TextMate'),
-        ([_os.path.join(_ROOT_PATH, 'Applications', 'TextWrangler.app')],
-         'textwrangler', 'TextWrangler'),
-        ([_os.path.join(_ROOT_PATH, 'Applications', 'Safari.app')],
-         'safari', 'Safari'),
-        ([_os.path.join(_ROOT_PATH, 'Applications', 'Xcode.app'),  # OS X >=1.7
-          _os.path.join(_ROOT_PATH, 'Developer', 'Applications', 'Xcode.app'
-                        )  # OS X 1.6,
-          ],
-         'xcode', 'Xcode'),
-        ]:
-    if not long_name:
-        long_name = name
-    CHECKER[name] = PathCommandDependency(
-        command=None, paths=paths, name=name, long_name=long_name)
-del paths, name, long_name  # cleanup namespace
+    def _get_parsed_version(self):
+        return _sys.version_info
 
 
 class PythonPackageDependency (Dependency):
@@ -671,13 +707,178 @@ class PythonPackageDependency (Dependency):
         return version
 
 
+class MercurialPythonPackage (PythonPackageDependency):
+    def _get_version(self):
+        try:  # mercurial >= 1.2
+            package = _importlib.import_module('mercurial.util')
+        except ImportError as e:  # mercurial <= 1.1.2
+            package = self._get_package('mercurial.version')
+            return package.get_version()
+        else:
+            return package.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
+
+
+class SQLitePythonPackage (PythonPackageDependency):
+    def _get_version_from_package(self, package):
+        return _sys.version
+
+    def _get_parsed_version(self):
+        return _sys.version_info
+
+
+def _program_files_paths(*args):
+    "Utility for generating MS Windows search paths"
+    pf = _os.environ.get('ProgramFiles', '/usr/bin')
+    pfx86 = _os.environ.get('ProgramFiles(x86)', pf)
+    paths = [_os.path.join(pf, *args)]
+    if pfx86 != pf:
+        paths.append(_os.path.join(pfx86, *args))
+    return paths
+
+
+CHECKER['python'] = PythonDependency()
+
+
+for command,long_name,minimum_version,paths in [
+        ('sh', 'Bourne Shell', None, None),
+        ('ash', 'Almquist Shell', None, None),
+        ('bash', 'Bourne Again Shell', None, None),
+        ('csh', 'C Shell', None, None),
+        ('ksh', 'KornShell', None, None),
+        ('dash', 'Debian Almquist Shell', None, None),
+        ('tcsh', 'TENEX C Shell', None, None),
+        ('zsh', 'Z Shell', None, None),
+        ('git', 'Git', (1, 7, 0), None),
+        ('hg', 'Mercurial', (2, 0, 0), None),
+        ('EasyMercurial', None, (1, 3), None),
+        ('pip', None, None, None),
+        ('sqlite3', 'SQLite 3', None, None),
+        ('nosetests', 'Nose', (1, 0, 0), None),
+        ('ipython', 'IPython script', (1, 0), None),
+        ('emacs', 'Emacs', None, None),
+        ('xemacs', 'XEmacs', None, None),
+        ('vim', 'Vim', None, None),
+        ('vi', None, None, None),
+        ('nano', 'Nano', None, None),
+        ('gedit', None, None, None),
+        ('kate', 'Kate', None, None),
+        ('notepad++', 'Notepad++', None,
+         _program_files_paths('Notepad++', 'notepad++.exe')),
+        ('firefox', 'Firefox', None,
+         _program_files_paths('Mozilla Firefox', 'firefox.exe')),
+        ('google-chrome', 'Google Chrome', None,
+         _program_files_paths('Google', 'Chrome', 'Application', 'chrome.exe')
+         ),
+        ('chromium', 'Chromium', None, None),
+        ]:
+    if not long_name:
+        long_name = command
+    CHECKER[command] = CommandDependency(
+        command=command, paths=paths, long_name=long_name,
+        minimum_version=minimum_version)
+del command, long_name, minimum_version, paths  # cleanup namespace
+
+
+CHECKER['make'] = MakeDependency(command='make', minimum_version=None)
+
+
+CHECKER['easy_install'] = EasyInstallDependency(
+    command='easy_install', long_name='Setuptools easy_install',
+    minimum_version=None)
+
+
+CHECKER['py.test'] = CommandDependency(
+    command='py.test', version_stream='stderr',
+    minimum_version=None)
+
+
+for paths,name,long_name in [
+        ([_os.path.join(_ROOT_PATH, 'Applications', 'Sublime Text 2.app',
+                        'Contents', 'version.plist')],
+         'sublime-text', 'Sublime Text'),
+        ([_os.path.join(_ROOT_PATH, 'Applications', 'TextMate.app',
+                        'Contents', 'version.plist')],
+         'textmate', 'TextMate'),
+        ([_os.path.join(_ROOT_PATH, 'Applications', 'TextWrangler.app',
+                        'Contents', 'version.plist')],
+         'textwrangler', 'TextWrangler'),
+        ([_os.path.join(_ROOT_PATH, 'Applications', 'Safari.app',
+                        'Contents', 'version.plist')],
+         'safari', 'Safari'),
+        ([_os.path.join(_ROOT_PATH, 'Applications', 'Xcode.app',
+                        'Contents', 'version.plist'),  # OS X >=1.7
+          _os.path.join(_ROOT_PATH, 'Developer', 'Applications', 'Xcode.app',
+                        'Contents', 'version.plist'),  # OS X 1.6,
+          ],
+         'xcode', 'Xcode'),
+        ]:
+    if not long_name:
+        long_name = name
+    CHECKER[name] = VersionPlistCommandDependency(
+        command=None, paths=paths, name=name, long_name=long_name)
+del paths, name, long_name  # cleanup namespace
+
+
 for package,name,long_name,minimum_version,and_dependencies in [
         ('nose', None, 'Nose Python package',
          CHECKER['nosetests'].minimum_version, None),
+        ('pytest', None, 'pytest Python package',
+         CHECKER['py.test'].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, ['jinja', 'tornado', 'pyzmq']),
+         CHECKER['ipython'].minimum_version, [
+             'jinja',
+             'tornado',
+             'pyzmq',
+             VirtualDependency(
+                 name='virtual-browser-ipython',
+                 long_name='IPython-compatible web browser',
+                 or_dependencies=[
+                     CommandDependency(
+                         command=CHECKER['firefox'].command,
+                         paths=CHECKER['firefox'].paths,
+                         name='{0}-for-ipython'.format(
+                             CHECKER['firefox'].name),
+                         long_name='{0} for IPython'.format(
+                             CHECKER['firefox'].long_name),
+                         minimum_version=(6, 0)),
+                     CommandDependency(
+                         command=CHECKER['google-chrome'].command,
+                         paths=CHECKER['google-chrome'].paths,
+                         name='{0}-for-ipython'.format(
+                             CHECKER['google-chrome'].name),
+                         long_name='{0} for IPython'.format(
+                             CHECKER['google-chrome'].long_name),
+                         minimum_version=(13, 0)),
+                     CommandDependency(
+                         command=CHECKER['chromium'].command,
+                         paths=CHECKER['chromium'].paths,
+                         name='{0}-for-ipython'.format(
+                             CHECKER['chromium'].name),
+                         long_name='{0} for IPython'.format(
+                             CHECKER['chromium'].long_name),
+                         minimum_version=(13, 0)),
+                     VersionPlistCommandDependency(
+                         command=CHECKER['safari'].command,
+                         paths=CHECKER['safari'].paths,
+                         key=CHECKER['safari'].key,
+                         name='{0}-for-ipython'.format(
+                             CHECKER['safari'].name),
+                         long_name='{0} for IPython'.format(
+                             CHECKER['safari'].long_name),
+                         minimum_version=(5, 0)),
+                 ]),
+         ]),
         ('argparse', None, 'Argparse', None, None),
         ('numpy', None, 'NumPy', None, None),
         ('scipy', None, 'SciPy', None, None),
@@ -703,112 +904,26 @@ for package,name,long_name,minimum_version,and_dependencies in [
 del package, name, long_name, minimum_version, and_dependencies, kwargs
 
 
-class MercurialPythonPackage (PythonPackageDependency):
-    def _get_version(self):
-        try:  # mercurial >= 1.2
-            package = _importlib.import_module('mercurial.util')
-        except ImportError as e:  # mercurial <= 1.1.2
-            package = self._get_package('mercurial.version')
-            return package.get_version()
-        else:
-            return package.version()
-
-
 CHECKER['mercurial'] = MercurialPythonPackage(
     package='mercurial.util', name='mercurial',
     long_name='Mercurial Python package',
     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
-
-    def _get_parsed_version(self):
-        return _sys.version_info
-
-
 CHECKER['sqlite3-python'] = SQLitePythonPackage(
     package='sqlite3', name='sqlite3-python',
     long_name='SQLite Python package',
     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['dependency'].full_name(),
-            self.or_pass['version'])
-
-
 for name,long_name,dependencies in [
         ('virtual-shell', 'command line shell', (
             'bash',
@@ -841,8 +956,8 @@ for name,long_name,dependencies in [
             'safari',
             )),
         ('virtual-pypi-installer', 'PyPI installer', (
-            'easy_install',
             'pip',
+            'easy_install',
             )),
         ]:
     CHECKER[name] = VirtualDependency(