Repository: pybuilder/pybuilder
Issue: Unable to specify version for custom plugin.

I'm developing a custom plugin and can't get pybuilder to pull new versions when they're available. How do I specify min and/or locked version for a plugin pulled from a pypi repository?


Changed files:
src/main/python/pybuilder/reactor.py, src/main/python/pybuilder/pluginloader.py, src/unittest/python/pluginloader_tests.py

diff --git a/src/main/python/pybuilder/pluginloader.py b/src/main/python/pybuilder/pluginloader.py
index 4911b68..e8654ad 100644
--- a/src/main/python/pybuilder/pluginloader.py
+++ b/src/main/python/pybuilder/pluginloader.py
@@ -24,13 +24,12 @@
 import sys
 import tempfile
 
-from pip._vendor.packaging.version import Version
-
 from pybuilder import __version__ as pyb_version
 from pybuilder.errors import (MissingPluginException,
                               IncompatiblePluginException,
                               UnspecifiedPluginNameException,
                               )
+from pybuilder.pip_utils import Version, SpecifierSet
 from pybuilder.pip_utils import pip_install, version_satisfies_spec
 from pybuilder.utils import read_file
 
@@ -47,11 +46,17 @@ class PluginLoader(object):
     def __init__(self, logger):
         self.logger = logger
 
+    def can_load(self, project, name, version=None, plugin_module_name=None):
+        pass
+
     def load_plugin(self, project, name, version=None, plugin_module_name=None):
         pass
 
 
 class BuiltinPluginLoader(PluginLoader):
+    def can_load(self, project, name, version=None, plugin_module_name=None):
+        return ":" not in name
+
     def load_plugin(self, project, name, version=None, plugin_module_name=None):
         self.logger.debug("Trying to load builtin plugin '%s'", name)
         builtin_plugin_name = plugin_module_name or "pybuilder.plugins.%s_plugin" % name
@@ -61,27 +66,57 @@ class BuiltinPluginLoader(PluginLoader):
         return plugin_module
 
 
-class ThirdPartyPluginLoader(PluginLoader):
+class DownloadingPluginLoader(PluginLoader):
+    def can_load(self, project, name, version=None, plugin_module_name=None):
+        return name.startswith(PYPI_PLUGIN_PROTOCOL) or name.startswith(VCS_PLUGIN_PROTOCOL) or ":" not in name
+
     def load_plugin(self, project, name, version=None, plugin_module_name=None):
+        display_name = _plugin_display_name(name, version, plugin_module_name)
+
+        update_plugin = False
+        force_reinstall = False
+
         thirdparty_plugin = name
         # Maybe we already installed this plugin from PyPI before
         if thirdparty_plugin.startswith(PYPI_PLUGIN_PROTOCOL):
             thirdparty_plugin = thirdparty_plugin.replace(PYPI_PLUGIN_PROTOCOL, "")
+            if version:
+                version_specifier = SpecifierSet(version)
+                # We always check if even one specifier in the set is not exact
+                for spec in version_specifier._specs:
+                    if spec.operator not in ("==", "==="):
+                        update_plugin = True
+                        break
         elif thirdparty_plugin.startswith(VCS_PLUGIN_PROTOCOL):
             if not plugin_module_name:
                 raise UnspecifiedPluginNameException(name)
             thirdparty_plugin = plugin_module_name
-        self.logger.debug("Trying to load third party plugin '%s'", thirdparty_plugin)
-
-        plugin_module = _load_plugin(thirdparty_plugin, name)
-        self.logger.debug("Found third party plugin '%s'", thirdparty_plugin)
-        return plugin_module
+            force_reinstall = True
 
+        # This is done before we attempt to load a plugin regardless of whether it can be loaded or not
+        if update_plugin or force_reinstall:
+            self.logger.info("Downloading or updating plugin {0}".format(display_name))
+            try:
+                _install_external_plugin(name, version, self.logger, plugin_module_name,
+                                         project.get_property("install_dependencies_index_url"),
+                                         project.get_property("install_dependencies_extra_index_url"),
+                                         update_plugin,
+                                         force_reinstall)
+                self.logger.info("Installed or updated plugin {0}.".format(display_name))
+            except MissingPluginException as e:
+                self.logger.error("Could not install or upgrade plugin {0}: {1}.".format(display_name, e))
 
-class DownloadingPluginLoader(ThirdPartyPluginLoader):
-    def load_plugin(self, project, name, version=None, plugin_module_name=None):
-        display_name = "%s%s" % (name, " version %s" % version if version else "")
-        self.logger.info("Downloading missing plugin {0}".format(display_name))
+        # Now let's try to load the plugin
+        try:
+            return self._load_installed_plugin(thirdparty_plugin, name)
+        except MissingPluginException:
+            if update_plugin or force_reinstall:
+                # If we already tried installing - fail fast
+                raise
+            self.logger.warn("Missing plugin {0}".format(display_name))
+
+        # We have failed to update or to load a plugin without a previous installation
+        self.logger.info("Downloading plugin {0}".format(display_name))
         try:
             _install_external_plugin(name, version, self.logger, plugin_module_name,
                                      project.get_property("install_dependencies_index_url"),
@@ -89,32 +124,50 @@ class DownloadingPluginLoader(ThirdPartyPluginLoader):
             self.logger.info("Installed plugin {0}.".format(display_name))
         except MissingPluginException as e:
             self.logger.error("Could not install plugin {0}: {1}.".format(display_name, e))
-            return None
-        return ThirdPartyPluginLoader.load_plugin(self, project, name, version, plugin_module_name)
+            raise
+
+        # After we have failed to update or load
+        return self._load_installed_plugin(thirdparty_plugin, name)
+
+    def _load_installed_plugin(self, thirdparty_plugin, name):
+        self.logger.debug("Trying to load third party plugin '%s'", thirdparty_plugin)
+        plugin_module = _load_plugin(thirdparty_plugin, name)
+        self.logger.debug("Found third party plugin '%s'", thirdparty_plugin)
+        return plugin_module
 
 
 class DispatchingPluginLoader(PluginLoader):
-    def __init__(self, logger, *loader):
+    def __init__(self, logger, *loaders):
         super(DispatchingPluginLoader, self).__init__(logger)
-        self.loader = loader
+        self._loaders = loaders
+
+    def can_load(self, project, name, version=None, plugin_module_name=None):
+        for loader in self._loaders:
+            if loader.can_load(project, name, version, plugin_module_name):
+                return True
+        return False
 
     def load_plugin(self, project, name, version=None, plugin_module_name=None):
         last_problem = None
-        for loader in self.loader:
-            try:
-                return loader.load_plugin(project, name, version, plugin_module_name)
-            except MissingPluginException as e:
-                last_problem = e
-        raise last_problem
-
-
-def _install_external_plugin(name, version, logger, plugin_module_name, index_url=None, extra_index_url=None):
+        for loader in self._loaders:
+            if loader.can_load(project, name, version, plugin_module_name):
+                try:
+                    return loader.load_plugin(project, name, version, plugin_module_name)
+                except MissingPluginException as e:
+                    last_problem = e
+        if last_problem:
+            raise last_problem
+        else:
+            raise MissingPluginException(_plugin_display_name(name, version, plugin_module_name),
+                                         "no plugin loader was able to load the plugin specified")
+
+
+def _install_external_plugin(name, version, logger, plugin_module_name, index_url=None, extra_index_url=None,
+                             upgrade=False, force_reinstall=False):
     if not name.startswith(PYPI_PLUGIN_PROTOCOL) and not name.startswith(VCS_PLUGIN_PROTOCOL):
         message = "Only plugins starting with '{0}' are currently supported"
         raise MissingPluginException(name, message.format((PYPI_PLUGIN_PROTOCOL, VCS_PLUGIN_PROTOCOL)))
 
-    force_reinstall = False
-    upgrade = False
     if name.startswith(PYPI_PLUGIN_PROTOCOL):
         pip_package = name.replace(PYPI_PLUGIN_PROTOCOL, "")
         if version:
@@ -141,6 +194,11 @@ def _install_external_plugin(name, version, logger, plugin_module_name, index_ur
             raise MissingPluginException(name, message)
 
 
+def _plugin_display_name(name, version, plugin_module_name):
+    return "%s%s%s" % (name, " version %s" % version if version else "",
+                       ", module name '%s'" % plugin_module_name if plugin_module_name else "")
+
+
 def _load_plugin(plugin_module_name, plugin_name):
     try:
         __import__(plugin_module_name)
diff --git a/src/main/python/pybuilder/reactor.py b/src/main/python/pybuilder/reactor.py
index cd4b7c5..28d6e84 100644
--- a/src/main/python/pybuilder/reactor.py
+++ b/src/main/python/pybuilder/reactor.py
@@ -23,7 +23,6 @@
 """
 
 import imp
-
 import os.path
 
 from pybuilder.core import (TASK_ATTRIBUTE, DEPENDS_ATTRIBUTE, DEPENDENTS_ATTRIBUTE,
@@ -35,7 +34,6 @@ from pybuilder.errors import PyBuilderException, ProjectValidationFailedExceptio
 from pybuilder.execution import Action, Initializer, Task, TaskDependency
 from pybuilder.pluginloader import (BuiltinPluginLoader,
                                     DispatchingPluginLoader,
-                                    ThirdPartyPluginLoader,
                                     DownloadingPluginLoader)
 from pybuilder.utils import as_list, get_dist_version_string, basestring
 
@@ -57,12 +55,9 @@ class Reactor(object):
         self.logger = logger
         self.execution_manager = execution_manager
         if not plugin_loader:
-            builtin_plugin_loader = BuiltinPluginLoader(self.logger)
-            installed_thirdparty_plugin_loader = ThirdPartyPluginLoader(self.logger)
-            downloading_thirdparty_plugin_loader = DownloadingPluginLoader(self.logger)
-            self.plugin_loader = DispatchingPluginLoader(
-                self.logger, builtin_plugin_loader, installed_thirdparty_plugin_loader,
-                downloading_thirdparty_plugin_loader)
+            self.plugin_loader = DispatchingPluginLoader(self.logger,
+                                                         BuiltinPluginLoader(self.logger),
+                                                         DownloadingPluginLoader(self.logger))
         else:
             self.plugin_loader = plugin_loader
         self._plugins = []
diff --git a/src/unittest/python/pluginloader_tests.py b/src/unittest/python/pluginloader_tests.py
index f00295c..104cf72 100644
--- a/src/unittest/python/pluginloader_tests.py
+++ b/src/unittest/python/pluginloader_tests.py
@@ -16,7 +16,6 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-import sys
 import unittest
 
 try:
@@ -28,14 +27,11 @@ except ImportError as e:
 
     builtin_module = builtins
 
-from mockito import when, verify, unstub, never  # TODO @mriehl get rid of mockito here
 from mock import patch, Mock, ANY
-from test_utils import mock  # TODO @mriehl WTF is this sorcery?!
 from pybuilder.pip_utils import PIP_EXEC_STANZA
 from pybuilder.errors import MissingPluginException, IncompatiblePluginException, UnspecifiedPluginNameException
 from pybuilder.pluginloader import (BuiltinPluginLoader,
                                     DispatchingPluginLoader,
-                                    ThirdPartyPluginLoader,
                                     DownloadingPluginLoader,
                                     _install_external_plugin,
                                     _check_plugin_version)
@@ -51,123 +47,79 @@ class PluginVersionCheckTest(unittest.TestCase):
         pluginloader.PYB_VERSION = self.old_pyb_version
 
     def test_version_exact_match(self):
-        plugin_module = mock()
+        plugin_module = Mock()
         pluginloader.PYB_VERSION = Version("1.2.3")
         plugin_module.pyb_version = "===1.2.3"
         _check_plugin_version(plugin_module, "test plugin")
 
     def test_version_compatible_match(self):
-        plugin_module = mock()
+        plugin_module = Mock()
         pluginloader.PYB_VERSION = Version("1.2.3")
         plugin_module.pyb_version = "~=1.2"
         _check_plugin_version(plugin_module, "test plugin")
 
     def test_version_multiple_specifier_match(self):
-        plugin_module = mock()
+        plugin_module = Mock()
         pluginloader.PYB_VERSION = Version("1.2.3")
         plugin_module.pyb_version = ">=1.2.0,<=1.2.4"
         _check_plugin_version(plugin_module, "test plugin")
 
     def test_version_no_match(self):
-        plugin_module = mock()
+        plugin_module = Mock()
         pluginloader.PYB_VERSION = Version("1.2.3")
         plugin_module.pyb_version = ">=1.2.5"
         self.assertRaises(IncompatiblePluginException, _check_plugin_version, plugin_module, "test plugin")
 
 
-class ThirdPartyPluginLoaderTest(unittest.TestCase):
-    def setUp(self):
-        self.project = mock()
-        self.loader = ThirdPartyPluginLoader(mock())
-
-    def tearDown(self):
-        unstub()
-
-    def test_should_raise_exception_when_requiring_plugin_and_plugin_is_not_found(self):
-        when(builtin_module).__import__(
-            "spam").thenRaise(ImportError())
-
-        self.assertRaises(
-            MissingPluginException, self.loader.load_plugin, self.project, "spam")
-
-        verify(builtin_module).__import__("spam")
-
-    def test_should_import_plugin_when_requiring_plugin_and_plugin_is_found_as_third_party(self):
-        old_module = sys.modules.get("spam")
-        try:
-            plugin_module = mock()
-            plugin_module.pyb_version = None
-            sys.modules["spam"] = plugin_module
-            when(builtin_module).__import__(
-                "spam").thenReturn(plugin_module)
-
-            self.loader.load_plugin(self.project, "spam")
-
-            verify(builtin_module).__import__("spam")
-        finally:
-            del sys.modules["spam"]
-            if old_module:
-                sys.modules["spam"] = old_module
-
-    def test_should_remove_pypi_protocol_when_importing(self):
-        old_module = sys.modules.get("spam")
-        try:
-            plugin_module = mock()
-            plugin_module.pyb_version = None
-            sys.modules["spam"] = plugin_module
-            when(builtin_module).__import__(
-                "pypi:spam").thenReturn(plugin_module)
-
-            self.loader.load_plugin(self.project, "spam")
-
-            verify(builtin_module).__import__("spam")
-        finally:
-            del sys.modules["spam"]
-            if old_module:
-                sys.modules["spam"] = old_module
-
-
 class DownloadingPluginLoaderTest(unittest.TestCase):
-    @patch("pybuilder.pluginloader.ThirdPartyPluginLoader")
+    @patch("pybuilder.pluginloader._load_plugin")
     @patch("pybuilder.pluginloader._install_external_plugin")
-    def test_should_download_module_from_pypi(self, install, _):
+    def test_should_download_module_from_pypi(self, install, load):
         logger = Mock()
         project = Mock()
         project.get_property.side_effect = lambda x: "index_url" if x == "install_dependencies_index_url" \\
             else "extra_index_url" if x == "install_dependencies_extra_index_url" else None
+        load.side_effect = (MissingPluginException("external_plugin"), Mock())
         DownloadingPluginLoader(logger).load_plugin(project, "pypi:external_plugin")
 
         install.assert_called_with("pypi:external_plugin", None, logger, None, "index_url", "extra_index_url")
 
-    @patch("pybuilder.pluginloader.ThirdPartyPluginLoader.load_plugin")
+    @patch("pybuilder.pluginloader._load_plugin")
     @patch("pybuilder.pluginloader._install_external_plugin")
     def test_should_load_module_after_downloading_with_pypi_when_download_succeeds(self, _, load):
         project = Mock()
         downloader = DownloadingPluginLoader(Mock())
         plugin = downloader.load_plugin(project, "pypi:external_plugin")
 
-        load.assert_called_with(downloader, project, "pypi:external_plugin", None, None)
+        load.assert_called_with("external_plugin", "pypi:external_plugin")
         self.assertEquals(plugin, load.return_value)
 
-    @patch("pybuilder.pluginloader.ThirdPartyPluginLoader.load_plugin")
+    @patch("pybuilder.pluginloader._load_plugin")
     @patch("pybuilder.pluginloader._install_external_plugin")
-    def test_should_not_load_module_after_downloading_when_pypi_download_fails(self, install, load):
-        install.side_effect = MissingPluginException("PyPI BOOM")
+    def test_should_not_load_module_twice_after_downloading_when_pypi_download_fails(self, install, load):
+        install.side_effect = MissingPluginException("PyPI Install Boom")
+        load.side_effect = MissingPluginException("PyPI Load Boom")
         downloader = DownloadingPluginLoader(Mock())
-        plugin = downloader.load_plugin(Mock(), "pypi:external_plugin")
+        self.assertRaises(MissingPluginException, downloader.load_plugin, Mock(), "pypi:external_plugin")
 
-        self.assertFalse(load.called)
-        self.assertEquals(plugin, None)
+        self.assertEquals(load.call_count, 1)
 
-    @patch("pybuilder.pluginloader.ThirdPartyPluginLoader.load_plugin")
+    @patch("pybuilder.pluginloader._load_plugin")
     @patch("pybuilder.pluginloader._install_external_plugin")
-    def test_should_not_load_module_after_downloading_when_vcs_download_fails(self, install, load):
-        install.side_effect = MissingPluginException("VCS BOOM")
+    def test_should_not_load_module_twice_after_downloading_when_vcs_download_fails(self, install, load):
+        install.side_effect = MissingPluginException("VCS Install BOOM")
+        load.side_effect = MissingPluginException("VCS Load Boom")
         downloader = DownloadingPluginLoader(Mock())
-        plugin = downloader.load_plugin(Mock(), "vcs:external_plugin URL")
+        self.assertRaises(MissingPluginException, downloader.load_plugin, Mock(), "vcs:external_plugin URL",
+                          plugin_module_name="vcs_module_name")
+        self.assertEquals(load.call_count, 1)
 
-        self.assertFalse(load.called)
-        self.assertEquals(plugin, None)
+    @patch("pybuilder.pluginloader._load_plugin")
+    @patch("pybuilder.pluginloader._install_external_plugin")
+    def test_should_fail_with_vcs_and_no_module_name(self, install, load):
+        install.side_effect = MissingPluginException("VCS BOOM")
+        downloader = DownloadingPluginLoader(Mock())
+        self.assertRaises(UnspecifiedPluginNameException, downloader.load_plugin, Mock(), "vcs:external_plugin URL")
 
     @patch("pybuilder.pluginloader._load_plugin")
     @patch("pybuilder.pluginloader._install_external_plugin")
@@ -187,6 +139,79 @@ class DownloadingPluginLoaderTest(unittest.TestCase):
         load.assert_called_with("external_plugin_module", "vcs:external_plugin URL")
         self.assertEquals(plugin, load.return_value)
 
+    @patch("pybuilder.pluginloader._load_plugin")
+    @patch("pybuilder.pluginloader._install_external_plugin")
+    def test_should_raise_exception_when_requiring_plugin_and_plugin_is_not_found(self, _, load):
+        project = Mock()
+        downloader = DownloadingPluginLoader(Mock())
+        load.side_effect = MissingPluginException("Load boom")
+
+        self.assertRaises(MissingPluginException, downloader.load_plugin, project, "spam")
+
+        load.assert_called_with("spam", "spam")
+
+    @patch("pybuilder.pluginloader._load_plugin")
+    @patch("pybuilder.pluginloader._install_external_plugin")
+    def test_should_import_plugin_when_requiring_plugin_and_plugin_is_found_as_third_party(self, install, load):
+        project = Mock()
+        downloader = DownloadingPluginLoader(Mock())
+        load.return_value = Mock()
+
+        self.assertEquals(load.return_value, downloader.load_plugin(project, "spam"))
+
+        install.assert_not_called()
+        self.assertEquals(install.call_count, 0)
+
+    @patch("pybuilder.pluginloader._load_plugin")
+    @patch("pybuilder.pluginloader._install_external_plugin")
+    def test_should_force_reinstall_vcs_plugin_before_first_loading_attempt(self, install, load):
+        project = Mock()
+        downloader = DownloadingPluginLoader(Mock())
+        load.return_value = Mock()
+
+        self.assertEquals(load.return_value, downloader.load_plugin(project, "vcs:spam", plugin_module_name="spam"))
+
+        install.assert_called_with("vcs:spam", None, downloader.logger, "spam", ANY, ANY, False, True)
+        self.assertEquals(install.call_count, 1)
+
+    @patch("pybuilder.pluginloader._load_plugin")
+    @patch("pybuilder.pluginloader._install_external_plugin")
+    def test_should_update_pypi_plugin_with_non_exact_version_before_first_loading_attempt(self, install, load):
+        project = Mock()
+        downloader = DownloadingPluginLoader(Mock())
+        load.return_value = Mock()
+
+        self.assertEquals(load.return_value, downloader.load_plugin(project, "pypi:spam", ">1.2"))
+
+        install.assert_called_with("pypi:spam", ">1.2", downloader.logger, None, ANY, ANY, True, False)
+        self.assertEquals(install.call_count, 1)
+
+    @patch("pybuilder.pluginloader._load_plugin")
+    @patch("pybuilder.pluginloader._install_external_plugin")
+    def test_should_update_pypi_plugin_with_compound_non_exact_version_before_first_loading_attempt(self, install,
+                                                                                                    load):
+        project = Mock()
+        downloader = DownloadingPluginLoader(Mock())
+        load.return_value = Mock()
+
+        self.assertEquals(load.return_value, downloader.load_plugin(project, "pypi:spam", ">1.2,==1.4"))
+
+        install.assert_called_with("pypi:spam", ">1.2,==1.4", downloader.logger, None, ANY, ANY, True, False)
+        self.assertEquals(install.call_count, 1)
+
+    @patch("pybuilder.pluginloader._load_plugin")
+    @patch("pybuilder.pluginloader._install_external_plugin")
+    def test_should_not_update_pypi_plugin_with_exact_version_before_first_loading_attempt(self, install, load):
+        project = Mock()
+        downloader = DownloadingPluginLoader(Mock())
+        plugin = Mock()
+        load.side_effect = (MissingPluginException("no spam installed"), plugin)
+
+        self.assertEquals(plugin, downloader.load_plugin(project, "pypi:spam", "===1.4"))
+
+        install.assert_called_with("pypi:spam", "===1.4", downloader.logger, None, ANY, ANY)
+        self.assertEquals(install.call_count, 1)
+
 
 class InstallExternalPluginTests(unittest.TestCase):
     def test_should_raise_error_when_protocol_is_invalid(self):
@@ -261,80 +286,61 @@ class InstallExternalPluginTests(unittest.TestCase):
 
 class BuiltinPluginLoaderTest(unittest.TestCase):
     def setUp(self):
-        self.project = mock()
-        self.loader = BuiltinPluginLoader(mock())
-
-    def tearDown(self):
-        unstub()
+        self.project = Mock()
+        self.loader = BuiltinPluginLoader(Mock())
 
-    def test_should_raise_exception_when_requiring_plugin_and_plugin_is_not_found(self):
-        when(builtin_module).__import__(
-            "pybuilder.plugins.spam_plugin").thenRaise(ImportError())
+    @patch("pybuilder.pluginloader._load_plugin")
+    def test_should_raise_exception_when_requiring_plugin_and_plugin_is_not_found(self, load):
+        load.side_effect = MissingPluginException("pybuilder.plugins.spam_plugin")
 
-        self.assertRaises(
-            MissingPluginException, self.loader.load_plugin, self.project, "spam")
+        self.assertRaises(MissingPluginException, self.loader.load_plugin, self.project, "spam")
 
-        verify(builtin_module).__import__("pybuilder.plugins.spam_plugin")
+        load.assert_called_with("pybuilder.plugins.spam_plugin", "spam")
 
-    def test_should_import_plugin_when_requiring_plugin_and_plugin_is_found_as_builtin(self):
-        old_module = sys.modules.get("pybuilder.plugins.spam_plugin")
-        try:
-            plugin_module = mock()
-            plugin_module.pyb_version = None
-            sys.modules["pybuilder.plugins.spam_plugin"] = plugin_module
-            when(builtin_module).__import__(
-                "pybuilder.plugins.spam_plugin").thenReturn(plugin_module)
+    @patch("pybuilder.pluginloader._load_plugin")
+    def test_should_import_plugin_when_requiring_plugin_and_plugin_is_found_as_builtin(self, load):
+        load.return_value = Mock()
 
-            self.loader.load_plugin(self.project, "spam")
+        plugin_module = self.loader.load_plugin(self.project, "spam")
 
-            verify(builtin_module).__import__("pybuilder.plugins.spam_plugin")
-        finally:
-            del sys.modules["pybuilder.plugins.spam_plugin"]
-            if old_module:
-                sys.modules["pybuilder.plugins.spam_plugin"] = old_module
+        load.assert_called_with("pybuilder.plugins.spam_plugin", "spam")
+        self.assertEquals(load.return_value, plugin_module)
 
 
 class DispatchingPluginLoaderTest(unittest.TestCase):
     def setUp(self):
-        self.project = mock()
-        self.first_delegatee = mock()
-        self.second_delegatee = mock()
+        self.project = Mock()
+        self.first_delegatee = Mock()
+        self.second_delegatee = Mock()
 
         self.loader = DispatchingPluginLoader(
-            mock, self.first_delegatee, self.second_delegatee)
+            Mock(), self.first_delegatee, self.second_delegatee)
 
     def test_should_raise_exception_when_all_delegatees_raise_exception(self):
-        when(self.first_delegatee).load_plugin(
-            self.project, "spam", None, None).thenRaise(MissingPluginException("spam"))
-        when(self.second_delegatee).load_plugin(
-            self.project, "spam", None, None).thenRaise(MissingPluginException("spam"))
+        self.first_delegatee.load_plugin.side_effect = MissingPluginException("spam")
+        self.second_delegatee.load_plugin.side_effect = MissingPluginException("spam")
 
         self.assertRaises(
             MissingPluginException, self.loader.load_plugin, self.project, "spam")
 
-        verify(self.first_delegatee).load_plugin(self.project, "spam", None, None)
-        verify(self.second_delegatee).load_plugin(self.project, "spam", None, None)
+        self.first_delegatee.load_plugin.assert_called_with(self.project, "spam", None, None)
+        self.second_delegatee.load_plugin.assert_called_with(self.project, "spam", None, None)
 
     def test_should_return_module_returned_by_second_loader_when_first_delegatee_raises_exception(self):
         result = "result"
-        when(self.first_delegatee).load_plugin(
-            self.project, "spam", None, None).thenRaise(MissingPluginException("spam"))
-        when(self.second_delegatee).load_plugin(
-            self.project, "spam", None, None).thenReturn(result)
+        self.first_delegatee.load_plugin.side_effect = MissingPluginException("spam")
+        self.second_delegatee.load_plugin.return_value = result
 
-        self.assertEquals(
-            result, self.loader.load_plugin(self.project, "spam"))
+        self.assertEquals(result, self.loader.load_plugin(self.project, "spam"))
 
-        verify(self.first_delegatee).load_plugin(self.project, "spam", None, None)
-        verify(self.second_delegatee).load_plugin(self.project, "spam", None, None)
+        self.first_delegatee.load_plugin.assert_called_with(self.project, "spam", None, None)
+        self.second_delegatee.load_plugin.assert_called_with(self.project, "spam", None, None)
 
     def test_ensure_second_delegatee_will_not_try_when_first_delegatee_loads_plugin(self):
         result = "result"
-        when(self.first_delegatee).load_plugin(
-            self.project, "spam", None, None).thenReturn(result)
+        self.first_delegatee.load_plugin.return_value = result
 
-        self.assertEquals(
-            result, self.loader.load_plugin(self.project, "spam"))
+        self.assertEquals(result, self.loader.load_plugin(self.project, "spam"))
 
-        verify(self.first_delegatee).load_plugin(self.project, "spam", None, None)
-        verify(self.second_delegatee, never).load_plugin(self.project, "spam", None, None)
+        self.first_delegatee.load_plugin.assert_called_with(self.project, "spam", None, None)
+        self.second_delegatee.load_plugin.assert_not_called()