diff options
author | Lars Wirzenius <liw@liw.fi> | 2012-02-22 17:23:04 +0000 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2012-02-22 17:23:04 +0000 |
commit | 4560577fce7e64fdd393d9e54dac237f35e8a25f (patch) | |
tree | 11e364bc87e98cc236d24f843d72f8a49c22f5da | |
parent | d9f7efcde37f8208b8679b5aae0f1d38875ccc73 (diff) | |
download | cliapp-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__.py | 7 | ||||
-rw-r--r-- | cliapp/hook.py | 76 | ||||
-rw-r--r-- | cliapp/hook_tests.py | 79 | ||||
-rw-r--r-- | cliapp/hookmgr.py | 49 | ||||
-rw-r--r-- | cliapp/hookmgr_tests.py | 59 | ||||
-rw-r--r-- | cliapp/plugin.py | 125 | ||||
-rw-r--r-- | cliapp/plugin_tests.py | 55 | ||||
-rw-r--r-- | cliapp/pluginmgr.py | 176 | ||||
-rw-r--r-- | cliapp/pluginmgr_tests.py | 122 | ||||
-rw-r--r-- | cliapp/settings.py | 2 | ||||
-rw-r--r-- | cliapp/settings_tests.py | 2 | ||||
-rw-r--r-- | test-plugins/aaa_hello_plugin.py | 8 | ||||
-rw-r--r-- | test-plugins/hello_plugin.py | 15 | ||||
-rw-r--r-- | test-plugins/oldhello_plugin.py | 9 | ||||
-rw-r--r-- | test-plugins/wrongversion_plugin.py | 12 | ||||
-rw-r--r-- | without-tests | 4 |
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 |