diff options
author | Marcel Hellkamp <marc@gsites.de> | 2011-10-01 14:33:18 +0200 |
---|---|---|
committer | Marcel Hellkamp <marc@gsites.de> | 2011-10-01 14:52:43 +0200 |
commit | 8b023afd027f1d9026c69dd13e94d954676085f5 (patch) | |
tree | 9766389c89d9ca7daf449b9bfcb5cf9c750da097 | |
parent | 239d4b9f536fc962a8db8361ffec3a469a3a36bb (diff) | |
download | bottle-8b023afd027f1d9026c69dd13e94d954676085f5.tar.gz |
Improved Bottle.mount()
new: You can now mount any WSGI application, not only Bottle instances.
api: If the prefix does not end in a slash, it is no longer mandatory. (fix #88)
api: The parameter order changed (for the better).
api: The 'Bottle.mount' dict is gone.
-rwxr-xr-x | bottle.py | 60 | ||||
-rw-r--r-- | test/test_mount.py | 92 | ||||
-rwxr-xr-x | test/test_wsgi.py | 22 |
3 files changed, 99 insertions, 75 deletions
@@ -499,7 +499,6 @@ class Bottle(object): self.router = Router() # Maps requests to :class:`Route` instances. self.plugins = [] # List of installed plugins. - self.mounts = {} self.error_handler = {} #: If true, most exceptions are catched and returned as :exc:`HTTPError` self.catchall = catchall @@ -511,33 +510,44 @@ class Bottle(object): self.install(JSONPlugin()) self.install(TemplatePlugin()) - def mount(self, app, prefix, **options): - ''' Mount an application to a specific URL prefix. The prefix is added - to SCIPT_PATH and removed from PATH_INFO before the sub-application - is called. + def mount(self, prefix, app, **options): + ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific + URL prefix. Example:: - :param app: an instance of :class:`Bottle`. - :param prefix: path prefix used as a mount-point. + root_app.mount('/admin/', admin_app) + + :param prefix: path prefix or `mount-point`. If it ends in a slash, + that slash is mandatory. + :param app: an instance of :class:`Bottle` or a WSGI application. All other parameters are passed to the underlying :meth:`route` call. ''' - if not isinstance(app, Bottle): - raise TypeError('Only Bottle instances are supported for now.') - prefix = '/'.join(filter(None, prefix.split('/'))) - if not prefix: - raise TypeError('Empty prefix. Perhaps you want a merge()?') - for other in self.mounts: - if other.startswith(prefix): - raise TypeError('Conflict with existing mount: %s' % other) - path_depth = prefix.count('/') + 1 - options.setdefault('method', 'ANY') + if isinstance(app, basestring): + prefix, app = app, prefix + depr('Parameter order of Bottle.mount() changed.') # 0.10 + + parts = filter(None, prefix.split('/')) + if not parts: raise ValueError('Empty path prefix.') + path_depth = len(parts) options.setdefault('skip', True) - self.mounts[prefix] = app - @self.route('/%s/:#.*#' % prefix, **options) + options.setdefault('method', 'ANY') + + @self.route('/%s/:#.*#' % '/'.join(parts), **options) def mountpoint(): - request.path_shift(path_depth) - # TODO: This sucks. Make it better. - return app._cast(app._handle(request.environ), request, response) + try: + request.path_shift(path_depth) + rs = BaseResponse([], 200) + def start_response(status, header): + rs.status = status + [rs.add_header(name, value) for name, value in header] + return lambda x: out.append(x) + rs.body.extend(app(request.environ, start_response)) + return HTTPResponse(rs.body, rs.status, rs.headers) + finally: + request.path_shift(-path_depth) + + if not prefix.endswith('/'): + self.route('/' + '/'.join(parts), callback=mountpoint, **options) def install(self, plugin): ''' Add a plugin to the list of plugins and prepare it for being @@ -1423,7 +1433,7 @@ class HooksPlugin(object): hooks = self.hooks[name] if ka.pop('reversed', False): hooks = hooks[::-1] return [hook(*a, **ka) for hook in hooks] - + def apply(self, callback, context): if self._empty(): return callback def wrapper(*a, **ka): @@ -1563,7 +1573,7 @@ class FormsDict(MultiDict): #: Encoding used for attribute values. input_encoding = 'utf8' - + def getunicode(self, name, default=None, encoding=None): value, enc = self.get(name, default), encoding or self.input_encoding try: @@ -1574,7 +1584,7 @@ class FormsDict(MultiDict): return value except UnicodeError, e: return default - + __getattr__ = getunicode diff --git a/test/test_mount.py b/test/test_mount.py index 13af7a6..e2c4f8e 100644 --- a/test/test_mount.py +++ b/test/test_mount.py @@ -1,28 +1,64 @@ -import unittest
-import sys, os.path
-import bottle
-import urllib2
-from StringIO import StringIO
-import thread
-import time
-from tools import ServerTestBase
-from bottle import tob, touni, tonat, Bottle
-
-class TestWsgi(ServerTestBase):
- ''' Tests sub-application support. '''
-
- def test_mount_no_plugins(self):
- def plugin(func):
- def wrapper(*a, **ka):
- return 'Plugin'
- return wrapper
- self.app.install(plugin)
- app = Bottle()
- self.app.mount(app, '/prefix')
- app.route('/foo', callback=lambda: 'bar')
- self.app.route('/foo', callback=lambda: 'baz')
- self.assertBody('Plugin', '/foo')
- self.assertBody('bar', '/prefix/foo')
-
-if __name__ == '__main__': #pragma: no cover
- unittest.main()
+import unittest +import sys, os.path +import bottle +import urllib2 +from StringIO import StringIO +import thread +import time +from tools import ServerTestBase +from bottle import tob, touni, tonat, Bottle + +class TestAppMounting(ServerTestBase): + def setUp(self): + ServerTestBase.setUp(self) + self.subapp = bottle.Bottle() + @self.subapp.route('/') + @self.subapp.route('/test/:test') + def test(test='foo'): + return test + + def test_mount(self): + self.app.mount('/test/', self.subapp) + self.assertStatus(404, '/') + self.assertStatus(404, '/test') + self.assertStatus(200, '/test/') + self.assertBody('foo', '/test/') + self.assertStatus(200, '/test/test/bar') + self.assertBody('bar', '/test/test/bar') + + def test_no_slash_prefix(self): + self.app.mount('/test', self.subapp) + self.assertStatus(404, '/') + self.assertStatus(200, '/test') + self.assertBody('foo', '/test') + self.assertStatus(200, '/test/') + self.assertBody('foo', '/test/') + self.assertStatus(200, '/test/test/bar') + self.assertBody('bar', '/test/test/bar') + + def test_mount_no_plugins(self): + def plugin(func): + def wrapper(*a, **ka): + return 'Plugin' + return wrapper + self.app.install(plugin) + self.app.route('/foo', callback=lambda: 'baz') + self.app.mount('/test/', self.subapp) + self.assertBody('Plugin', '/foo') + self.assertBody('foo', '/test/') + + def test_mount_wsgi(self): + status = {} + def app(environ, start_response): + start_response('200 OK', [('X-Test', 'WSGI')]) + return 'WSGI ' + environ['PATH_INFO'] + self.app.mount('/test', app) + self.assertStatus(200, '/test/') + self.assertBody('WSGI /', '/test') + self.assertBody('WSGI /', '/test/') + self.assertHeader('X-Test', 'WSGI', '/test/') + self.assertBody('WSGI /test/bar', '/test/test/bar') + + +if __name__ == '__main__': #pragma: no cover + unittest.main() diff --git a/test/test_wsgi.py b/test/test_wsgi.py index 345c09a..19543c9 100755 --- a/test/test_wsgi.py +++ b/test/test_wsgi.py @@ -353,28 +353,6 @@ class TestAppShortcuts(ServerTestBase): -class TestAppMounting(ServerTestBase): - def setUp(self): - ServerTestBase.setUp(self) - self.subapp = bottle.Bottle() - - def test_basicmounting(self): - bottle.app().mount(self.subapp, '/test') - self.assertStatus(404, '/') - self.assertStatus(404, '/test') - self.assertStatus(404, '/test/') - self.assertStatus(404, '/test/test/bar') - @self.subapp.route('/') - @self.subapp.route('/test/:test') - def test(test='foo'): - return test - self.assertStatus(404, '/') - self.assertStatus(404, '/test') - self.assertStatus(200, '/test/') - self.assertBody('foo', '/test/') - self.assertStatus(200, '/test/test/bar') - self.assertBody('bar', '/test/test/bar') - if __name__ == '__main__': #pragma: no cover |