diff options
author | Marcel Hellkamp <marc@gsites.de> | 2012-03-26 20:36:48 +0200 |
---|---|---|
committer | Marcel Hellkamp <marc@gsites.de> | 2012-03-26 20:36:48 +0200 |
commit | e644824aab83e3322793dc7260bcaf76e0ede0d0 (patch) | |
tree | 6fa90164a8c47b332438038a6d648481eb13c578 | |
parent | 4285991bc9459f0f2824d728870b2d7ad8a49379 (diff) | |
parent | fb6ca2b558d92e403182cb28ed2f40464270b50e (diff) | |
download | bottle-app-template-config.tar.gz |
Merge branch 'master' into app-template-configapp-template-config
-rw-r--r-- | bottle.py | 138 | ||||
-rwxr-xr-x | docs/api.rst | 2 | ||||
-rwxr-xr-x | docs/tutorial.rst | 1 | ||||
-rw-r--r-- | test/test_resources.py | 70 |
4 files changed, 200 insertions, 11 deletions
@@ -526,25 +526,38 @@ class Route(object): class Bottle(object): """ Each Bottle object represents a single, distinct web application and - consists of routes, callbacks, plugins and configuration. Instances are - callable WSGI applications. """ + consists of routes, callbacks, plugins, resources and configuration. + Instances are callable WSGI applications. - def __init__(self, catchall=True, autojson=True, config=None): - self.routes = [] # List of installed :class:`Route` instances. - self.router = Router() # Maps requests to :class:`Route` instances. - self.plugins = [] # List of installed plugins. + :param catchall: If true (default), handle all exceptions. Turn off to + let debugging middleware handle exceptions. + """ - self.error_handler = {} - self.config = ConfigDict(config or {}) + def __init__(self, catchall=True, autojson=True): #: If true, most exceptions are catched and returned as :exc:`HTTPError` self.catchall = catchall - #: The installed :class:`HooksPlugin`. + + #: A :cls:`ResourceManager` for application files + self.resources = ResourceManager() + + #: A :cls:`ConfigDict` for app specific configuration. + self.config = ConfigDict() + self.config.autojson = autojson + + self.routes = [] # List of installed :class:`Route` instances. + self.router = Router() # Maps requests to :class:`Route` instances. + self.error_handler = {} + + # Core plugins + self.plugins = [] # List of installed plugins. self.hooks = HooksPlugin() self.install(self.hooks) - if autojson: self.install(JSONPlugin()) + if self.config.autojson: + self.install(JSONPlugin()) #: The installed :class:`TemplatePlugin`. self.views = self.install(TemplatePlugin()) + def mount(self, prefix, app, **options): ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific URL prefix. Example:: @@ -1936,6 +1949,102 @@ class WSGIFileWrapper(object): yield part +class ResourceManager(object): + ''' This class manages a list of search paths and helps to find and open + aplication-bound resources (files). + + :param base: path used to resolve relative search paths. It works as a + default for :meth:`add_path`. + :param opener: callable used to open resources. + :param cachemode: controls which lookups are cached. One of 'all', + 'found' or 'none'. + ''' + + def __init__(self, base='./', opener=open, cachemode='all'): + + self.opener = open + self.base = './' + self.cachemode = cachemode + + #: A list of search paths. See :meth:`add_path` for details. + self.path = [] + #: A list of file masks. See :meth:`add_mask` for details. + self.mask = ['%s'] + #: A cache for resolved paths. `res.cache.clear()`` clears the cache. + self.cache = {} + + def add_path(self, path, base=None, index=None): + ''' Add a path to the :attr:`path` list. + + The path is turned into an absolute and normalized form. If it + looks like a file (not ending in `/`), the filename is stripped + off. The path is not required to exist. + + Relative paths are joined with `base` or :attr:`self.base`, which + defaults to the current working directory. This comes in handy if + you resources live in a sub-folder of your module or package:: + + res.add_path('./resources/', __file__) + + The :attr:`path` list is searched in order and new paths are + added to the end of the list. The *index* parameter can change + the position (e.g. ``0`` to prepend). Adding a path a second time + moves it to the new position. + ''' + base = os.path.abspath(os.path.dirname(base or self.base)) + path = os.path.abspath(os.path.join(base, os.path.dirname(path))) + path += os.sep + if path in self.path: + self.path.remove(path) + if index is None: + self.path.append(path) + else: + self.path.insert(index, path) + self.cache.clear() + + def add_mask(self, mask, index=None): + ''' Add a new format string to the :attr:`mask` list. + + Masks are used to turn resource names into actual filenames. The + mask string must contain exactly one occurence of ``%s``, which + is replaced by the supplied resource name on lookup. This can be + used to auto-append file extentions (e.g. ``%s.ext``). + ''' + if index is None: + self.masks.append(mask) + else: + self.masks.insert(index, mask) + self.cache.clear() + + def lookup(self, name): + ''' Search for a resource and return an absolute file path, or `None`. + + The :attr:`path` list is searched in order. For each path, the + :attr:`mask` entries are tried in order. The first path that points + to an existing file is returned. Symlinks are followed. The result + is cached to speed up future lookups. ''' + if name not in self.cache or DEBUG: + for path in self.path: + for mask in self.mask: + fpath = os.path.join(path, mask%name) + if os.path.isfile(fpath): + if self.cachemode in ('all', 'found'): + self.cache[name] = fpath + return fpath + if self.cachemode == 'all': + self.cache[name] = None + return self.cache[name] + + def open(self, name, *args, **kwargs): + ''' Find a resource and return an opened file object, or raise IOError. + + Additional parameters are passed to the ``open()`` built-in. + ''' + fname = self.lookup(name) + if not fname: raise IOError("Resource %r not found." % name) + return self.opener(name, *args, **kwargs) + + @@ -2291,6 +2400,12 @@ class CherryPyServer(ServerAdapter): server.stop() +class WaitressServer(ServerAdapter): + def run(self, handler): + from waitress import serve + serve(handler, host=self.host, port=self.port) + + class PasteServer(ServerAdapter): def run(self, handler): # pragma: no cover from paste import httpserver @@ -2437,7 +2552,7 @@ class BjoernServer(ServerAdapter): class AutoServer(ServerAdapter): """ Untested. """ - adapters = [PasteServer, TwistedServer, CherryPyServer, WSGIRefServer] + adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, WSGIRefServer] def run(self, handler): for sa in self.adapters: try: @@ -2449,6 +2564,7 @@ server_names = { 'cgi': CGIServer, 'flup': FlupFCGIServer, 'wsgiref': WSGIRefServer, + 'waitress': WaitressServer, 'cherrypy': CherryPyServer, 'paste': PasteServer, 'fapws3': FapwsServer, diff --git a/docs/api.rst b/docs/api.rst index a4616c7..f2c8129 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -98,6 +98,8 @@ Data Structures Return the current default application and remove it from the stack. +.. autoclass:: ResourceManager + :members: Exceptions --------------- diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 9c542b8..dad1b1b 100755 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -281,6 +281,7 @@ Error Pages If anything goes wrong, Bottle displays an informative but fairly boring error page. You can override the default for a specific HTTP status code with the :func:`error` decorator:: + from bottle import error @error(404) def error404(error): return 'Nothing here, sorry' diff --git a/test/test_resources.py b/test/test_resources.py new file mode 100644 index 0000000..24a8db5 --- /dev/null +++ b/test/test_resources.py @@ -0,0 +1,70 @@ +import bottle +from tools import ServerTestBase +from bottle import ResourceManager +import os.path +import unittest + +class TestResouceManager(unittest.TestCase): + + def test_path_normalize(self): + tests = ('/foo/bar/', '/foo/bar/baz', '/foo/baz/../bar/blub') + for test in tests: + rm = ResourceManager() + rm.add_path(test) + self.assertEqual(rm.path, ['/foo/bar/']) + + def test_path_absolutize(self): + tests = ('./foo/bar/', './foo/bar/baz', './foo/baz/../bar/blub') + abspath = os.path.abspath('./foo/bar/') + os.sep + for test in tests: + rm = ResourceManager() + rm.add_path(test) + self.assertEqual(rm.path, [abspath]) + + for test in tests: + rm = ResourceManager() + rm.add_path(test[2:]) + self.assertEqual(rm.path, [abspath]) + + def test_path_unique(self): + tests = ('/foo/bar/', '/foo/bar/baz', '/foo/baz/../bar/blub') + rm = ResourceManager() + [rm.add_path(test) for test in tests] + self.assertEqual(rm.path, ['/foo/bar/']) + + def test_root_path(self): + tests = ('/foo/bar/', '/foo/bar/baz', '/foo/baz/../bar/blub') + for test in tests: + rm = ResourceManager() + rm.add_path('./baz/', test) + self.assertEqual(rm.path, ['/foo/bar/baz/']) + + for test in tests: + rm = ResourceManager() + rm.add_path('baz/', test) + self.assertEqual(rm.path, ['/foo/bar/baz/']) + + def test_path_order(self): + rm = ResourceManager() + rm.add_path('/middle/') + rm.add_path('/first/', index=0) + rm.add_path('/last/') + self.assertEqual(rm.path, ['/first/', '/middle/', '/last/']) + + def test_get(self): + rm = ResourceManager() + rm.add_path('/first/') + rm.add_path(__file__) + rm.add_path('/last/') + self.assertEqual(None, rm.lookup('notexist.txt')) + self.assertEqual(__file__, rm.lookup(os.path.basename(__file__))) + + def test_open(self): + rm = ResourceManager() + rm.add_path(__file__) + fp = rm.open(os.path.basename(__file__)) + self.assertEqual(fp.read(), open(__file__).read()) + + +if __name__ == '__main__': #pragma: no cover + unittest.main() |