summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2012-02-22 17:23:04 +0000
committerLars Wirzenius <liw@liw.fi>2012-02-22 17:23:04 +0000
commit4560577fce7e64fdd393d9e54dac237f35e8a25f (patch)
tree11e364bc87e98cc236d24f843d72f8a49c22f5da
parentd9f7efcde37f8208b8679b5aae0f1d38875ccc73 (diff)
downloadcliapp-4560577fce7e64fdd393d9e54dac237f35e8a25f.tar.gz
Add a plugin system
This code was originally written separately, which is why there's no history of it in the cliapp version control repository. The code is derived from code I wrote for obnam.
-rw-r--r--cliapp/__init__.py7
-rw-r--r--cliapp/hook.py76
-rw-r--r--cliapp/hook_tests.py79
-rw-r--r--cliapp/hookmgr.py49
-rw-r--r--cliapp/hookmgr_tests.py59
-rw-r--r--cliapp/plugin.py125
-rw-r--r--cliapp/plugin_tests.py55
-rw-r--r--cliapp/pluginmgr.py176
-rw-r--r--cliapp/pluginmgr_tests.py122
-rw-r--r--cliapp/settings.py2
-rw-r--r--cliapp/settings_tests.py2
-rw-r--r--test-plugins/aaa_hello_plugin.py8
-rw-r--r--test-plugins/hello_plugin.py15
-rw-r--r--test-plugins/oldhello_plugin.py9
-rw-r--r--test-plugins/wrongversion_plugin.py12
-rw-r--r--without-tests4
16 files changed, 798 insertions, 2 deletions
diff --git a/cliapp/__init__.py b/cliapp/__init__.py
index 00b74d3..5f6eb69 100644
--- a/cliapp/__init__.py
+++ b/cliapp/__init__.py
@@ -21,4 +21,11 @@ __version__ = '0.27'
from settings import Settings
from app import Application, AppException
+# The plugin system
+from hook import Hook, FilterHook
+from hookmgr import HookManager
+from plugin import Plugin
+from pluginmgr import PluginManager
+
+
__all__ = locals()
diff --git a/cliapp/hook.py b/cliapp/hook.py
new file mode 100644
index 0000000..21b1614
--- /dev/null
+++ b/cliapp/hook.py
@@ -0,0 +1,76 @@
+# Copyright (C) 2009-2012 Lars Wirzenius
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+'''Hooks with callbacks.
+
+In order to de-couple parts of the application, especially when plugins
+are used, hooks can be used. A hook is a location in the application
+code where plugins may want to do something. Each hook has a name and
+a list of callbacks. The application defines the name and the location
+where the hook will be invoked, and the plugins (or other parts of the
+application) will register callbacks.
+
+'''
+
+
+class Hook(object):
+
+ '''A hook.'''
+
+ def __init__(self):
+ self.callbacks = []
+
+ def add_callback(self, callback):
+ '''Add a callback to this hook.
+
+ Return an identifier that can be used to remove this callback.
+
+ '''
+
+ if callback not in self.callbacks:
+ self.callbacks.append(callback)
+ return callback
+
+ def call_callbacks(self, *args, **kwargs):
+ '''Call all callbacks with the given arguments.'''
+ for callback in self.callbacks:
+ callback(*args, **kwargs)
+
+ def remove_callback(self, callback_id):
+ '''Remove a specific callback.'''
+ if callback_id in self.callbacks:
+ self.callbacks.remove(callback_id)
+
+
+class FilterHook(Hook):
+
+ '''A hook which filters data through callbacks.
+
+ Every hook of this type accepts a piece of data as its first argument
+ Each callback gets the return value of the previous one as its
+ argument. The caller gets the value of the final callback.
+
+ Other arguments (with or without keywords) are passed as-is to
+ each callback.
+
+ '''
+
+ def call_callbacks(self, data, *args, **kwargs):
+ for callback in self.callbacks:
+ data = callback(data, *args, **kwargs)
+ return data
+
diff --git a/cliapp/hook_tests.py b/cliapp/hook_tests.py
new file mode 100644
index 0000000..c7e3a4d
--- /dev/null
+++ b/cliapp/hook_tests.py
@@ -0,0 +1,79 @@
+# Copyright (C) 2009-2012 Lars Wirzenius
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import unittest
+
+from cliapp import Hook, FilterHook
+
+
+class HookTests(unittest.TestCase):
+
+ def setUp(self):
+ self.hook = Hook()
+
+ def callback(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+ def test_has_no_callbacks_by_default(self):
+ self.assertEqual(self.hook.callbacks, [])
+
+ def test_adds_callback(self):
+ self.hook.add_callback(self.callback)
+ self.assertEqual(self.hook.callbacks, [self.callback])
+
+ def test_adds_callback_only_once(self):
+ self.hook.add_callback(self.callback)
+ self.hook.add_callback(self.callback)
+ self.assertEqual(self.hook.callbacks, [self.callback])
+
+ def test_calls_callback(self):
+ self.hook.add_callback(self.callback)
+ self.hook.call_callbacks('bar', kwarg='foobar')
+ self.assertEqual(self.args, ('bar',))
+ self.assertEqual(self.kwargs, { 'kwarg': 'foobar' })
+
+ def test_removes_callback(self):
+ cb_id = self.hook.add_callback(self.callback)
+ self.hook.remove_callback(cb_id)
+ self.assertEqual(self.hook.callbacks, [])
+
+
+class FilterHookTests(unittest.TestCase):
+
+ def setUp(self):
+ self.hook = FilterHook()
+
+ def callback(self, data, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+ return data + ['callback']
+
+ def test_returns_argument_if_no_callbacks(self):
+ self.assertEqual(self.hook.call_callbacks(['foo']), ['foo'])
+
+ def test_calls_callback_and_returns_modified_data(self):
+ self.hook.add_callback(self.callback)
+ data = self.hook.call_callbacks([])
+ self.assertEqual(data, ['callback'])
+
+ def test_calls_callback_with_extra_args(self):
+ self.hook.add_callback(self.callback)
+ self.hook.call_callbacks(['data'], 'extra', kwextra='kwextra')
+ self.assertEqual(self.args, ('extra',))
+ self.assertEqual(self.kwargs, { 'kwextra': 'kwextra' })
+
diff --git a/cliapp/hookmgr.py b/cliapp/hookmgr.py
new file mode 100644
index 0000000..15a0214
--- /dev/null
+++ b/cliapp/hookmgr.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2009-2012 Lars Wirzenius
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+from cliapp import Hook, FilterHook
+
+
+class HookManager(object):
+
+ '''Manage the set of hooks the application defines.'''
+
+ def __init__(self):
+ self.hooks = {}
+
+ def new(self, name, hook):
+ '''Add a new hook to the manager.
+
+ If a hook with that name already exists, nothing happens.
+
+ '''
+
+ if name not in self.hooks:
+ self.hooks[name] = hook
+
+ def add_callback(self, name, callback):
+ '''Add a callback to a named hook.'''
+ return self.hooks[name].add_callback(callback)
+
+ def remove_callback(self, name, callback_id):
+ '''Remove a specific callback from a named hook.'''
+ self.hooks[name].remove_callback(callback_id)
+
+ def call(self, name, *args, **kwargs):
+ '''Call callbacks for a named hook, using given arguments.'''
+ return self.hooks[name].call_callbacks(*args, **kwargs)
+
diff --git a/cliapp/hookmgr_tests.py b/cliapp/hookmgr_tests.py
new file mode 100644
index 0000000..9957d00
--- /dev/null
+++ b/cliapp/hookmgr_tests.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2009-2012 Lars Wirzenius
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import unittest
+
+from cliapp import HookManager, FilterHook
+
+
+class HookManagerTests(unittest.TestCase):
+
+ def setUp(self):
+ self.hooks = HookManager()
+ self.hooks.new('foo', FilterHook())
+
+ def callback(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+ def test_has_no_tests_initially(self):
+ hooks = HookManager()
+ self.assertEqual(hooks.hooks, {})
+
+ def test_adds_new_hook(self):
+ self.assert_(self.hooks.hooks.has_key('foo'))
+
+ def test_adds_callback(self):
+ self.hooks.add_callback('foo', self.callback)
+ self.assertEqual(self.hooks.hooks['foo'].callbacks, [self.callback])
+
+ def test_removes_callback(self):
+ cb_id = self.hooks.add_callback('foo', self.callback)
+ self.hooks.remove_callback('foo', cb_id)
+ self.assertEqual(self.hooks.hooks['foo'].callbacks, [])
+
+ def test_calls_callbacks(self):
+ self.hooks.add_callback('foo', self.callback)
+ self.hooks.call('foo', 'bar', kwarg='foobar')
+ self.assertEqual(self.args, ('bar',))
+ self.assertEqual(self.kwargs, { 'kwarg': 'foobar' })
+
+ def test_call_returns_value_of_callbacks(self):
+ self.hooks.new('bar', FilterHook())
+ self.hooks.add_callback('bar', lambda data: data + 1)
+ self.assertEqual(self.hooks.call('bar', 1), 2)
+
diff --git a/cliapp/plugin.py b/cliapp/plugin.py
new file mode 100644
index 0000000..c305841
--- /dev/null
+++ b/cliapp/plugin.py
@@ -0,0 +1,125 @@
+# Copyright (C) 2009-2012 Lars Wirzenius
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+'''A generic plugin manager.
+
+The plugin manager finds files with plugins and loads them. It looks
+for plugins in a number of locations specified by the caller. To add
+a plugin to be loaded, it is enough to put it in one of the locations,
+and name it *_plugin.py. (The naming convention is to allow having
+other modules as well, such as unit tests, in the same locations.)
+
+'''
+
+
+import imp
+import inspect
+import os
+
+
+class Plugin(object):
+
+ '''Base class for plugins.
+
+ A plugin MUST NOT have any side effects when it is instantiated.
+ This is necessary so that it can be safely loaded by unit tests,
+ and so that a user interface can allow the user to disable it,
+ even if it is installed, with no ill effects. Any side effects
+ that would normally happen should occur in the enable() method,
+ and be undone by the disable() method. These methods must be
+ callable any number of times.
+
+ The subclass MAY define the following attributes:
+
+ * name
+ * description
+ * version
+ * required_application_version
+
+ name is the user-visible identifier for the plugin. It defaults
+ to the plugin's classname.
+
+ description is the user-visible description of the plugin. It may
+ be arbitrarily long, and can use pango markup language. Defaults
+ to the empty string.
+
+ version is the plugin version. Defaults to '0.0.0'. It MUST be a
+ sequence of integers separated by periods. If several plugins with
+ the same name are found, the newest version is used. Versions are
+ compared integer by integer, starting with the first one, and a
+ missing integer treated as a zero. If two plugins have the same
+ version, either might be used.
+
+ required_application_version gives the version of the minimal
+ application version the plugin is written for. The first integer
+ must match exactly: if the application is version 2.3.4, the
+ plugin's required_application_version must be at least 2 and
+ at most 2.3.4 to be loaded. Defaults to 0.
+
+ '''
+
+ @property
+ def name(self):
+ return self.__class__.__name__
+
+ @property
+ def description(self):
+ return ''
+
+ @property
+ def version(self):
+ return '0.0.0'
+
+ @property
+ def required_application_version(self):
+ return '0.0.0'
+
+ def setup(self):
+ '''Setup plugin.
+
+ This is called at plugin load time. It should not yet enable the
+ plugin (the ``enable`` method does that), but it might do things
+ like add itself into a hook that adds command line arguments
+ to the application.
+
+ '''
+
+ def enable_wrapper(self):
+ '''Enable plugin.
+
+ The plugin manager will call this method, which then calls the
+ enable method. Plugins should implement the enable method.
+ The wrapper method is there to allow an application to provide
+ an extended base class that does some application specific
+ magic when plugins are enabled or disabled.
+
+ '''
+
+ self.enable()
+
+ def disable_wrapper(self):
+ '''Corresponds to enable_wrapper, but for disabling a plugin.'''
+ self.disable()
+
+ def enable(self):
+ '''Enable the plugin.'''
+ raise NotImplemented()
+
+ def disable(self):
+ '''Disable the plugin.'''
+ raise NotImplemented()
+
diff --git a/cliapp/plugin_tests.py b/cliapp/plugin_tests.py
new file mode 100644
index 0000000..99799d8
--- /dev/null
+++ b/cliapp/plugin_tests.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2009-2012 Lars Wirzenius
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import unittest
+
+from cliapp import Plugin
+
+
+class PluginTests(unittest.TestCase):
+
+ def setUp(self):
+ self.plugin = Plugin()
+
+ def test_name_is_class_name(self):
+ self.assertEqual(self.plugin.name, 'Plugin')
+
+ def test_description_is_empty_string(self):
+ self.assertEqual(self.plugin.description, '')
+
+ def test_version_is_zeroes(self):
+ self.assertEqual(self.plugin.version, '0.0.0')
+
+ def test_required_application_version_is_zeroes(self):
+ self.assertEqual(self.plugin.required_application_version, '0.0.0')
+
+ def test_enable_raises_exception(self):
+ self.assertRaises(Exception, self.plugin.enable)
+
+ def test_disable_raises_exception(self):
+ self.assertRaises(Exception, self.plugin.disable)
+
+ def test_enable_wrapper_calls_enable(self):
+ self.plugin.enable = lambda: setattr(self, 'enabled', True)
+ self.plugin.enable_wrapper()
+ self.assert_(self.enabled, True)
+
+ def test_disable_wrapper_calls_disable(self):
+ self.plugin.disable = lambda: setattr(self, 'disabled', True)
+ self.plugin.disable_wrapper()
+ self.assert_(self.disabled, True)
+
diff --git a/cliapp/pluginmgr.py b/cliapp/pluginmgr.py
new file mode 100644
index 0000000..54aee9b
--- /dev/null
+++ b/cliapp/pluginmgr.py
@@ -0,0 +1,176 @@
+# Copyright (C) 2009-2012 Lars Wirzenius
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+'''A generic plugin manager.
+
+The plugin manager finds files with plugins and loads them. It looks
+for plugins in a number of locations specified by the caller. To add
+a plugin to be loaded, it is enough to put it in one of the locations,
+and name it *_plugin.py. (The naming convention is to allow having
+other modules as well, such as unit tests, in the same locations.)
+
+'''
+
+
+import imp
+import inspect
+import os
+
+
+from cliapp import Plugin
+
+
+class PluginManager(object):
+
+ '''Manage plugins.
+
+ This class finds and loads plugins, and keeps a list of them that
+ can be accessed in various ways.
+
+ The locations are set via the locations attribute, which is a list.
+
+ When a plugin is loaded, an instance of its class is created. This
+ instance is initialized using normal and keyword arguments specified
+ in the plugin manager attributes plugin_arguments and
+ plugin_keyword_arguments.
+
+ The version of the application using the plugin manager is set via
+ the application_version attribute. This defaults to '0.0.0'.
+
+ '''
+
+ suffix = '_plugin.py'
+
+ def __init__(self):
+ self.locations = []
+ self._plugins = None
+ self._plugin_files = None
+ self.plugin_arguments = []
+ self.plugin_keyword_arguments = {}
+ self.application_version = '0.0.0'
+
+ @property
+ def plugin_files(self):
+ if self._plugin_files is None:
+ self._plugin_files = self.find_plugin_files()
+ return self._plugin_files
+
+ @property
+ def plugins(self):
+ if self._plugins is None:
+ self._plugins = self.load_plugins()
+ return self._plugins
+
+ def __getitem__(self, name):
+ for plugin in self.plugins:
+ if plugin.name == name:
+ return plugin
+ raise KeyError('Plugin %s is not known' % name)
+
+ def find_plugin_files(self):
+ '''Find files that may contain plugins.
+
+ This finds all files named *_plugin.py in all locations.
+ The returned list is sorted.
+
+ '''
+
+ pathnames = []
+
+ for location in self.locations:
+ try:
+ basenames = os.listdir(location)
+ except os.error:
+ continue
+ for basename in basenames:
+ s = os.path.join(location, basename)
+ if s.endswith(self.suffix) and os.path.exists(s):
+ pathnames.append(s)
+
+ return sorted(pathnames)
+
+ def load_plugins(self):
+ '''Load plugins from all plugin files.'''
+
+ plugins = dict()
+
+ for pathname in self.plugin_files:
+ for plugin in self.load_plugin_file(pathname):
+ if plugin.name in plugins:
+ p = plugins[plugin.name]
+ if self.is_older(p.version, plugin.version):
+ plugins[plugin.name] = plugin
+ else:
+ plugins[plugin.name] = plugin
+
+ return plugins.values()
+
+ def is_older(self, version1, version2):
+ '''Is version1 older than version2?'''
+ return self.parse_version(version1) < self.parse_version(version2)
+
+ def load_plugin_file(self, pathname):
+ '''Return plugin classes in a plugin file.'''
+
+ name, ext = os.path.splitext(os.path.basename(pathname))
+ f = file(pathname, 'r')
+ module = imp.load_module(name, f, pathname,
+ ('.py', 'r', imp.PY_SOURCE))
+ f.close()
+
+ plugins = []
+ for dummy, member in inspect.getmembers(module, inspect.isclass):
+ if issubclass(member, Plugin):
+ p = member(*self.plugin_arguments,
+ **self.plugin_keyword_arguments)
+ if self.compatible_version(p.required_application_version):
+ plugins.append(p)
+ p.setup()
+
+ return plugins
+
+ def compatible_version(self, required_application_version):
+ '''Check that the plugin is version-compatible with the application.
+
+ This checks the plugin's required_application_version against
+ the declared application version and returns True if they are
+ compatible, and False if not.
+
+ '''
+
+ req = self.parse_version(required_application_version)
+ app = self.parse_version(self.application_version)
+
+ return app[0] == req[0] and app >= req
+
+ def parse_version(self, version):
+ '''Parse a string represenation of a version into list of ints.'''
+
+ return [int(s) for s in version.split('.')]
+
+ def enable_plugins(self, plugins=None):
+ '''Enable all or selected plugins.'''
+
+ for plugin in plugins or self.plugins:
+ plugin.enable_wrapper()
+
+ def disable_plugins(self, plugins=None):
+ '''Disable all or selected plugins.'''
+
+ for plugin in plugins or self.plugins:
+ plugin.disable_wrapper()
+
diff --git a/cliapp/pluginmgr_tests.py b/cliapp/pluginmgr_tests.py
new file mode 100644
index 0000000..8c49633
--- /dev/null
+++ b/cliapp/pluginmgr_tests.py
@@ -0,0 +1,122 @@
+# Copyright (C) 2009-2012 Lars Wirzenius
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import unittest
+
+from cliapp import PluginManager
+
+
+class PluginManagerInitialStateTests(unittest.TestCase):
+
+ def setUp(self):
+ self.pm = PluginManager()
+
+ def test_locations_is_empty_list(self):
+ self.assertEqual(self.pm.locations, [])
+
+ def test_plugins_is_empty_list(self):
+ self.assertEqual(self.pm.plugins, [])
+
+ def test_application_version_is_zeroes(self):
+ self.assertEqual(self.pm.application_version, '0.0.0')
+
+ def test_plugin_files_is_empty(self):
+ self.assertEqual(self.pm.plugin_files, [])
+
+ def test_plugin_arguments_is_empty(self):
+ self.assertEqual(self.pm.plugin_arguments, [])
+
+ def test_plugin_keyword_arguments_is_empty(self):
+ self.assertEqual(self.pm.plugin_keyword_arguments, {})
+
+
+class PluginManagerTests(unittest.TestCase):
+
+ def setUp(self):
+ self.pm = PluginManager()
+ self.pm.locations = ['test-plugins', 'not-exist']
+ self.pm.plugin_arguments = ('fooarg',)
+ self.pm.plugin_keyword_arguments = { 'bar': 'bararg' }
+
+ self.files = sorted(['test-plugins/hello_plugin.py',
+ 'test-plugins/aaa_hello_plugin.py',
+ 'test-plugins/oldhello_plugin.py',
+ 'test-plugins/wrongversion_plugin.py'])
+
+ def test_finds_the_right_plugin_files(self):
+ self.assertEqual(self.pm.find_plugin_files(), self.files)
+
+ def test_plugin_files_attribute_implicitly_searches(self):
+ self.assertEqual(self.pm.plugin_files, self.files)
+
+ def test_loads_hello_plugin(self):
+ plugins = self.pm.load_plugins()
+ self.assertEqual(len(plugins), 1)
+ self.assertEqual(plugins[0].name, 'Hello')
+
+ def test_plugins_attribute_implicitly_searches(self):
+ self.assertEqual(len(self.pm.plugins), 1)
+ self.assertEqual(self.pm.plugins[0].name, 'Hello')
+
+ def test_initializes_hello_with_correct_args(self):
+ plugin = self.pm['Hello']
+ self.assertEqual(plugin.foo, 'fooarg')
+ self.assertEqual(plugin.bar, 'bararg')
+
+ def test_calls_setup_method_at_load_time(self):
+ plugin = self.pm['Hello']
+ self.assert_(plugin.setup_called)
+
+ def test_raises_keyerror_for_unknown_plugin(self):
+ self.assertRaises(KeyError, self.pm.__getitem__, 'Hithere')
+
+ def test_enable_plugins_enables_all_plugins(self):
+ enabled = set()
+ for plugin in self.pm.plugins:
+ plugin.enable = lambda: enabled.add(plugin)
+ self.pm.enable_plugins()
+ self.assertEqual(enabled, set(self.pm.plugins))
+
+ def test_disable_plugins_disables_all_plugins(self):
+ disabled = set()
+ for plugin in self.pm.plugins:
+ plugin.disable = lambda: disabled.add(plugin)
+ self.pm.disable_plugins()
+ self.assertEqual(disabled, set(self.pm.plugins))
+
+
+class PluginManagerCompatibleApplicationVersionTests(unittest.TestCase):
+
+ def setUp(self):
+ self.pm = PluginManager()
+ self.pm.application_version = '1.2.3'
+
+ def test_rejects_zero(self):
+ self.assertFalse(self.pm.compatible_version('0'))
+
+ def test_rejects_two(self):
+ self.assertFalse(self.pm.compatible_version('2'))
+
+ def test_rejects_one_two_four(self):
+ self.assertFalse(self.pm.compatible_version('1.2.4'))
+
+ def test_accepts_one(self):
+ self.assert_(self.pm.compatible_version('1'))
+
+ def test_accepts_one_two_three(self):
+ self.assert_(self.pm.compatible_version('1.2.3'))
+
diff --git a/cliapp/settings.py b/cliapp/settings.py
index f315fd3..d80719b 100644
--- a/cliapp/settings.py
+++ b/cliapp/settings.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2011 Lars Wirzenius
+# Copyright (C) 2009-2012 Lars Wirzenius
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
diff --git a/cliapp/settings_tests.py b/cliapp/settings_tests.py
index 59f357e..bafee7b 100644
--- a/cliapp/settings_tests.py
+++ b/cliapp/settings_tests.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2011 Lars Wirzenius
+# Copyright (C) 2009-2012 Lars Wirzenius
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
diff --git a/test-plugins/aaa_hello_plugin.py b/test-plugins/aaa_hello_plugin.py
new file mode 100644
index 0000000..b8f3253
--- /dev/null
+++ b/test-plugins/aaa_hello_plugin.py
@@ -0,0 +1,8 @@
+import cliapp
+
+class Hello(cliapp.Plugin):
+
+ def __init__(self, foo, bar=None):
+ self.foo = foo
+ self.bar = bar
+
diff --git a/test-plugins/hello_plugin.py b/test-plugins/hello_plugin.py
new file mode 100644
index 0000000..1a4f1ea
--- /dev/null
+++ b/test-plugins/hello_plugin.py
@@ -0,0 +1,15 @@
+import cliapp
+
+class Hello(cliapp.Plugin):
+
+ def __init__(self, foo, bar=None):
+ self.foo = foo
+ self.bar = bar
+
+ @property
+ def version(self):
+ return '0.0.1'
+
+ def setup(self):
+ self.setup_called = True
+
diff --git a/test-plugins/oldhello_plugin.py b/test-plugins/oldhello_plugin.py
new file mode 100644
index 0000000..e401ff7
--- /dev/null
+++ b/test-plugins/oldhello_plugin.py
@@ -0,0 +1,9 @@
+import cliapp
+
+class Hello(cliapp.Plugin):
+
+ def __init__(self, foo, bar=None):
+ self.foo = foo
+ self.bar = bar
+
+
diff --git a/test-plugins/wrongversion_plugin.py b/test-plugins/wrongversion_plugin.py
new file mode 100644
index 0000000..9f54908
--- /dev/null
+++ b/test-plugins/wrongversion_plugin.py
@@ -0,0 +1,12 @@
+# This is a test plugin that requires a newer application version than
+# what the test harness specifies.
+
+import cliapp
+
+class WrongVersion(cliapp.Plugin):
+
+ required_application_version = '9999.9.9'
+
+ def __init__(self, *args, **kwargs):
+ pass
+
diff --git a/without-tests b/without-tests
index b3bf74b..7c5b978 100644
--- a/without-tests
+++ b/without-tests
@@ -4,3 +4,7 @@
./setup.py
./cliapp/genman.py
./doc/conf.py
+./test-plugins/oldhello_plugin.py
+./test-plugins/hello_plugin.py
+./test-plugins/wrongversion_plugin.py
+./test-plugins/aaa_hello_plugin.py