summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcel Hellkamp <marc@gsites.de>2012-03-26 20:36:48 +0200
committerMarcel Hellkamp <marc@gsites.de>2012-03-26 20:36:48 +0200
commite644824aab83e3322793dc7260bcaf76e0ede0d0 (patch)
tree6fa90164a8c47b332438038a6d648481eb13c578
parent4285991bc9459f0f2824d728870b2d7ad8a49379 (diff)
parentfb6ca2b558d92e403182cb28ed2f40464270b50e (diff)
downloadbottle-app-template-config.tar.gz
Merge branch 'master' into app-template-configapp-template-config
-rw-r--r--bottle.py138
-rwxr-xr-xdocs/api.rst2
-rwxr-xr-xdocs/tutorial.rst1
-rw-r--r--test/test_resources.py70
4 files changed, 200 insertions, 11 deletions
diff --git a/bottle.py b/bottle.py
index e86125b..f55afa6 100644
--- a/bottle.py
+++ b/bottle.py
@@ -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()