Skip to content

Commit

Permalink
new: Immplemented Bottle.install(), Bottle.uninstall() and Bottle.clo…
Browse files Browse the repository at this point in the history
…se() and lots of tests.
  • Loading branch information
defnull committed Feb 18, 2011
1 parent 77621f3 commit 88c1c87
Show file tree
Hide file tree
Showing 2 changed files with 241 additions and 1 deletion.
37 changes: 36 additions & 1 deletion bottle.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,40 @@ def add_filter(self, ftype, func):
self.castfilter.append((ftype, func))
self.castfilter.sort()

def install(self, plugin):
''' Add a plugin to the list of plugins and prepare it for beeing
applied to all routes of this application. A plugin may be a simple
decorator or an object that implements the :class:`Plugin` API.
'''
self.ccache.clear()
if hasattr(plugin, 'setup'): plugin.setup(self)
if not callable(plugin) and not hasattr(plugin, 'apply'):
raise TypeError("Plugins must be callable or implement .apply()")
self.plugins.append(plugin)
return plugin

def uninstall(self, plugin):
''' Uninstall plugins. Pass an instance to remove a specific plugin.
Pass a type object to remove all plugins that match that type.
Subclasses are not removed. Pass a string to remove all plugins with
a matching ``name`` attribute. Pass ``True`` to remove all plugins.
The list of affected plugins is returned. '''
self.ccache.clear()
removed, remove = [], plugin
for i, plugin in list(enumerate(self.plugins))[::-1]:
if remove is True or remove is plugin or remove is type(plugin) \
or getattr(plugin, 'name', True) == remove:
removed.append(plugin)
del self.plugins[i]
if hasattr(plugin, 'close'): plugin.close()
return removed

def close(self):
''' Closes the application and all installed plugins. '''
for plugin in self.plugins:
if hasattr(plugin, 'close'): plugin.close()
self.stopped = True

def match(self, environ):
""" Search for a matching route and return a (callback, urlargs) tuple.
The first element is the associated route callback with plugins
Expand Down Expand Up @@ -456,7 +490,7 @@ def _build_callback(self, config):
if plugin in skip or type(plugin) in skip: continue
if getattr(plugin, 'name', True) in skip: continue
if hasattr(plugin, 'apply'):
wrapped = plugin.apply(wrapped, self, config)
wrapped = plugin.apply(wrapped, config)
else:
wrapped = plugin(wrapped)
if not wrapped: break
Expand Down Expand Up @@ -525,6 +559,7 @@ def decorator(callback):
verb = verb.upper()
cfg = config.copy()
cfg.update(rule=rule, method=verb, callback=callback)
cfg.update(app=self)
self.routes.append(cfg)
handle = self.routes.index(cfg)
self.router.add(rule, verb, handle, name=name, static=static)
Expand Down
205 changes: 205 additions & 0 deletions test/test_plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# -*- coding: utf-8 -*-
import unittest
import bottle
import tools


class MyTestPlugin(object):
def __init__(self):
self.app = None
self.add_args = {}
self.add_content = ''

def setup(self, app):
self.app = app

def apply(self, func, config):
def wrapper(*a, **ka):
ka.update(self.add_args)
self.lastcall = func, a, ka
return ''.join(func(*a, **ka)) + self.add_content
return wrapper


def my_test_decorator(func):
def wrapper(*a, **ka):
return list(func(*a, **ka))[-1]



class TestPluginManagement(tools.ServerTestBase):

def verify_installed(self, plugin, otype, **config):
self.assertEqual(type(plugin), otype)
self.assertEqual(plugin.config, config)
self.assertEqual(plugin.app, self.app)
self.assertTrue(plugin in self.app.plugins)

def test_install_plugin(self):
plugin = MyTestPlugin()
installed = self.app.install(plugin)
self.assertEqual(plugin, installed)
self.assertTrue(plugin in self.app.plugins)

def test_install_decorator(self):
installed = self.app.install(my_test_decorator)
self.assertEqual(my_test_decorator, installed)
self.assertTrue(my_test_decorator in self.app.plugins)

def test_install_non_plugin(self):
self.assertRaises(TypeError, self.app.install, 'I am not a plugin')

def test_uninstall_by_instance(self):
plugin = self.app.install(MyTestPlugin())
plugin2 = self.app.install(MyTestPlugin())
self.app.uninstall(plugin)
self.assertTrue(plugin not in self.app.plugins)
self.assertTrue(plugin2 in self.app.plugins)

def test_uninstall_by_type(self):
plugin = self.app.install(MyTestPlugin())
plugin2 = self.app.install(MyTestPlugin())
self.app.uninstall(MyTestPlugin)
self.assertTrue(plugin not in self.app.plugins)
self.assertTrue(plugin2 not in self.app.plugins)

def test_uninstall_by_name(self):
plugin = self.app.install(MyTestPlugin())
plugin2 = self.app.install(MyTestPlugin())
plugin.name = 'myplugin'
self.app.uninstall('myplugin')
self.assertTrue(plugin not in self.app.plugins)
self.assertTrue(plugin2 in self.app.plugins)

def test_uninstall_all(self):
plugin = self.app.install(MyTestPlugin())
plugin2 = self.app.install(MyTestPlugin())
self.app.uninstall(True)
self.assertFalse(self.app.plugins)

def test_route_plugin(self):
plugin = MyTestPlugin()
plugin.add_content = ';foo'
@self.app.route('/a')
@self.app.route('/b', apply=[plugin])
def a(): return 'plugin'
self.assertBody('plugin', '/a')
self.assertBody('plugin;foo', '/b')

def test_plugin_oder(self):
self.app.install(MyTestPlugin()).add_content = ';global-1'
self.app.install(MyTestPlugin()).add_content = ';global-2'
l1 = MyTestPlugin()
l1.add_content = ';local-1'
l2 = MyTestPlugin()
l2.add_content = ';local-2'
@self.app.route('/a')
@self.app.route('/b', apply=[l1, l2])
def a(): return 'plugin'
self.assertBody('plugin;global-2;global-1', '/a')
self.assertBody('plugin;local-2;local-1;global-2;global-1', '/b')

def test_skip_by_instance(self):
g1 = self.app.install(MyTestPlugin())
g1.add_content = ';global-1'
g2 = self.app.install(MyTestPlugin())
g2.add_content = ';global-2'
l1 = MyTestPlugin()
l1.add_content = ';local-1'
l2 = MyTestPlugin()
l2.add_content = ';local-2'
@self.app.route('/a', skip=[g2, l2])
@self.app.route('/b', apply=[l1, l2], skip=[g2, l2])
def a(): return 'plugin'
self.assertBody('plugin;global-1', '/a')
self.assertBody('plugin;local-1;global-1', '/b')

def test_skip_by_class(self):
g1 = self.app.install(MyTestPlugin())
g1.add_content = ';global-1'
@self.app.route('/a')
@self.app.route('/b', skip=[MyTestPlugin])
def a(): return 'plugin'
self.assertBody('plugin;global-1', '/a')
self.assertBody('plugin', '/b')

def test_skip_by_name(self):
g1 = self.app.install(MyTestPlugin())
g1.add_content = ';global-1'
g1.name = 'test'
@self.app.route('/a')
@self.app.route('/b', skip=['test'])
def a(): return 'plugin'
self.assertBody('plugin;global-1', '/a')
self.assertBody('plugin', '/b')

def test_skip_all(self):
g1 = self.app.install(MyTestPlugin())
g1.add_content = ';global-1'
@self.app.route('/a')
@self.app.route('/b', skip=[True])
def a(): return 'plugin'
self.assertBody('plugin;global-1', '/a')
self.assertBody('plugin', '/b')

def test_skip_nonlist(self):
g1 = self.app.install(MyTestPlugin())
g1.add_content = ';global-1'
@self.app.route('/a')
@self.app.route('/b', skip=g1)
def a(): return 'plugin'
self.assertBody('plugin;global-1', '/a')
self.assertBody('plugin', '/b')



class TestPluginAPI(tools.ServerTestBase):

def setUp(self):
super(TestPluginAPI, self).setUp()
@self.app.route('/', test='plugin.cfg')
def test(**args):
return ', '.join('%s:%s' % (k,v) for k,v in args.items())

def test_callable(self):
def plugin(func):
def wrapper(*a, **ka):
return func(*a, test='me', **ka) + '; tail'
return wrapper
self.app.install(plugin)
self.assertBody('test:me; tail', '/')


def test_apply(self):
class Plugin(object):
def apply(self, func, config):
def wrapper(*a, **ka):
return func(*a, test=config['test'], **ka) + '; tail'
return wrapper
def __call__(self, func):
raise AssertionError("Plugins must not be called "\
"if they implement 'apply'")
self.app.install(Plugin())
self.assertBody('test:plugin.cfg; tail', '/')

def test_setup(self):
class Plugin(object):
def __call__(self, func): return func
def setup(self, app): self.app = app
plugin = self.app.install(Plugin())
self.assertEquals(getattr(plugin, 'app', None), self.app)

def test_close(self):
class Plugin(object):
def __call__(self, func): return func
def close(self): self.closed = True
plugin = self.app.install(Plugin())
plugin2 = self.app.install(Plugin())
self.app.uninstall(plugin)
self.assertTrue(getattr(plugin, 'closed', False))
self.app.close()
self.assertTrue(getattr(plugin2, 'closed', False))


if __name__ == '__main__': #pragma: no cover
unittest.main()

0 comments on commit 88c1c87

Please sign in to comment.