summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcel Hellkamp <marc@gsites.de>2011-10-01 14:33:18 +0200
committerMarcel Hellkamp <marc@gsites.de>2011-10-01 14:52:43 +0200
commit8b023afd027f1d9026c69dd13e94d954676085f5 (patch)
tree9766389c89d9ca7daf449b9bfcb5cf9c750da097
parent239d4b9f536fc962a8db8361ffec3a469a3a36bb (diff)
downloadbottle-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-xbottle.py60
-rw-r--r--test/test_mount.py92
-rwxr-xr-xtest/test_wsgi.py22
3 files changed, 99 insertions, 75 deletions
diff --git a/bottle.py b/bottle.py
index 43afde3..eb57c1c 100755
--- a/bottle.py
+++ b/bottle.py
@@ -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