summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bottle.py88
-rwxr-xr-xdocs/tutorial.rst208
-rw-r--r--test/test_template_plugin.py55
-rwxr-xr-xtest/test_wsgi.py2
4 files changed, 256 insertions, 97 deletions
diff --git a/bottle.py b/bottle.py
index 26fb2b4..f55afa6 100644
--- a/bottle.py
+++ b/bottle.py
@@ -554,7 +554,8 @@ class Bottle(object):
self.install(self.hooks)
if self.config.autojson:
self.install(JSONPlugin())
- self.install(TemplatePlugin())
+ #: The installed :class:`TemplatePlugin`.
+ self.views = self.install(TemplatePlugin())
def mount(self, prefix, app, **options):
@@ -1578,25 +1579,86 @@ class HooksPlugin(object):
class TemplatePlugin(object):
- ''' This plugin applies the :func:`view` decorator to all routes with a
- `template` config parameter. If the parameter is a tuple, the second
- element must be a dict with additional options (e.g. `template_engine`)
- or default variables for the template. '''
+ ''' This plugin renders dictionaries returned by route callbacks into
+ the specified template. Routes without a `template` parameter are
+ ignored.
+
+ Example::
+ @app.route(..., template='template_name')
+ def callback():
+ return dict(key='value')
+ '''
+
name = 'template'
api = 2
+ def __init__(self):
+ #: Cache for precompiled templates
+ self.cache = {}
+
+ def add_path(self, path, root='./'):
+ ''' Helper to add a path to the :attr:`path` list. Example::
+
+ app.views.add_path('./views/', __file__)
+ '''
+ if os.path.isfile(root):
+ root = os.path.dirname(root)
+ path = os.path.abspath(os.path.join(root, path))
+ self.lookup_path.append(path)
+
+ def lookup(self, name):
+ if os.path.isfile(name): return name
+ for path in self.lookup_path:
+ for mask in ['%s', '%s.tpl'] + self.lookup_masks:
+ fname = os.path.join(path, mask % name)
+ if os.path.isfile(fname):
+ return fname
+
+ def render(self, name, tplvars):
+ if name not in self.cache or DEBUG:
+ if "\n" in name or "{" in name or "%" in name or '$' in name:
+ tpl = self.adapter(source=name, **self.options)
+ else:
+ filename = self.lookup(name)
+ if filename:
+ tpl = self.adapter(filename=filename, **self.options)
+ else:
+ depr('Template not found in normal lookup path. Note that'\
+ ' the TEMPLATE_PATH global is deprecated.') #0.11
+ tpl = self.adapter(name=name, **self.options)
+ if not tpl:
+ raise TemplateError('Template not found.')
+ self.cache[name] = tpl
+ return self.cache[name].render(self.globals, tplvars)
+
+ def setup(self, app):
+ self.app = app
+ self.globals = app.config.setdefault('view_globals', {})
+ self.options = app.config.setdefault('view_options', {})
+ self.adapter = app.config.setdefault('view_adapter', SimpleTemplate)
+ self.lookup_path = app.config.setdefault('view_path', [])
+ self.lookup_masks = app.config.setdefault('view_masks', [])
+
def apply(self, callback, route):
- conf = route.config.get('template')
+ conf = route.config.template
if isinstance(conf, (tuple, list)) and len(conf) == 2:
- return view(conf[0], **conf[1])(callback)
- elif isinstance(conf, str) and 'template_opts' in route.config:
- depr('The `template_opts` parameter is deprecated.') #0.9
- return view(conf, **route.config['template_opts'])(callback)
+ depr('Passing a tuple as template parameter is deprecated.') #0.11
+ name, defaults = conf
elif isinstance(conf, str):
- return view(conf)(callback)
+ name, defaults = conf, {}
else:
return callback
+ def wrapper(*args, **kwargs):
+ result = callback(*args, **kwargs)
+ if isinstance(result, (dict, DictMixin)):
+ tplvars = defaults.copy()
+ tplvars.update(result)
+ return self.render(name, tplvars)
+ return result
+
+ return wrapper
+
#: Not a plugin, but part of the plugin API. TODO: Find a better place.
class _ImportRedirect(object):
@@ -1606,7 +1668,7 @@ class _ImportRedirect(object):
self.impmask = impmask
self.module = sys.modules.setdefault(name, imp.new_module(name))
self.module.__dict__.update({'__file__': __file__, '__path__': [],
- '__all__': [], '__loader__': self})
+ '__all__': [], '__loader__': self})
sys.meta_path.append(self)
def find_module(self, fullname, path=None):
@@ -3142,7 +3204,7 @@ ERROR_PAGE_TEMPLATE = """
%end
"""
-#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a
+#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a
#: request callback, this instance always refers to the *current* request
#: (even on a multithreaded server).
request = LocalRequest()
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index 06ea678..dad1b1b 100755
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -74,21 +74,45 @@ This tutorial assumes you have Bottle either :ref:`installed <installation>` or
def hello():
return "Hello World!"
- run(host='localhost', port=8080, debug=True)
+ run(host='localhost', port=8080)
This is it. Run this script, visit http://localhost:8080/hello and you will see "Hello World!" in your browser. Here is how it works:
The :func:`route` decorator binds a piece of code to an URL path. In this case, we link the ``/hello`` URL to the ``hello()`` function. This is called a `route` (hence the decorator name) and is the most important concept of this framework. You can define as many routes as you want. Whenever a browser requests an URL, the associated function is called and the return value is sent back to the browser. Its as simple as that.
-The :func:`run` call in the last line starts a built-in development server. It runs on `localhost` port 8080 and serves requests until you hit :kbd:`Control-c`. You can switch the server backend later, but for now a development server is all we need. It requires no setup at all and is an incredibly painless way to get your application up and running for local tests.
-
-The :ref:`tutorial-debugging` is very helpful during early development, but should be switched off for public applications. Keep that in mind.
+The :func:`run` call in the last line starts a built-in development server. It runs on `localhost` port 8080 and serves requests until you hit :kbd:`Control-c`. You can switch the server backend and change the settings later, but for now a development server is all we need. It requires no setup at all and is an incredibly painless way to get your application up and running for local tests.
Of course this is a very simple example, but it shows the basic concept of how applications are built with Bottle. Continue reading and you'll see what else is possible.
+External Access
+------------------------------------------------------------------------------
+
+The example above starts the server on ``localhost``, which means that it is only available as a local service. If you are testing on a remote machine or want to make your project publicly available, change the host parameter to ``0.0.0.0``. This tells the server to listen on all interfaces and public IPs.
+
+.. _tutorial-debugging:
+
+Debug Mode and Code Reloading
+------------------------------------------------------------------------------
+
+There are two features that make development a whole lot easier:
+
+* ``debug=True`` turns Bottle into verbose mode and provides helpful debugging information whenever an error occurs. It also disables some optimizations that might get in your way and adds some checks that warn you about possible misconfiguration.
+
+* ``reloader=True`` enables code reloading. Every time you edit a source file, the reloader restarts the server process and loads the newest version of your code. This is very helpful but has some side-effects. Please read :ref:`reloading` first.
+
+Please note that both features slow down the server and debug mode exposes sensitive information (stack traces) to the user. You should never use these on a public server::
+
+ import sys
+ if name == '__main__':
+ if '--debug' in sys.args:
+ run(host='localhost', port=8080, debug=True, reloader=True)
+ else:
+ run(host='0.0.0.0', port=80)
+
+
.. _tutorial-default:
-The `Default Application`
+Working with the `Default Application`
------------------------------------------------------------------------------
For the sake of simplicity, most examples in this tutorial use a module-level :func:`route` decorator to define routes. This adds routes to a global "default application", an instance of :class:`Bottle` that is automatically created the first time you call :func:`route`. Several other module-level decorators and functions relate to this default application, but if you prefer a more object oriented approach and don't mind the extra typing, you can create a separate application object and use that instead of the global one::
@@ -383,18 +407,107 @@ You may provide a different HTTP status code as a second parameter.
All exceptions other than :exc:`HTTPResponse` or :exc:`HTTPError` will result in a ``500 Internal Server Error`` response, so they won't crash your WSGI server. You can turn off this behavior to handle exceptions in your middleware by setting ``bottle.app().catchall`` to ``False``.
+
+
+
+
+.. _tutorial-templates:
+
+Templates
+==============================================================================
+
+Bottle comes with a fast built-in template engine and support for many third-party engines including Jinja2 and Mako. No matter which engine you choose, the API to render templates is always the same::
+
+ @route('/hello/<name>', template='hello')
+ def hello(name='World'):
+ return {'name': name}
+
+The ``template`` route parameter names the template to render and the dictionary returned by the callback defines the template variables. On each request, the template is rendered with the provided variables and the result is sent to the browser. If you want to choose a template at runtime, you can pass `True` and name the template within the returned dictionary::
+
+ @route('/hello/<name>', template=True)
+ def hello(name='World'):
+ return {'template': 'hello', 'name': name}
+
+Instead of a template name, you can also provide a full template file path, or the template itself as a source string. The latter is particularly useful for small applications and allows you to bundle templates with your application in a single file::
+
+ hello_tpl = '<h1>Hello {{name}}!</h1>'
+
+ @route('/hello/<name>')
+ def hello(name='World', template=hello_tpl):
+ return {'name': name}
+
+Template Syntax
+------------------------------------------------------------------------------
+
+.. highlight:: html+django
+
+The build-in template engine syntax is a very thin layer around the Python language. Its main purpose is to ensure correct indentation of blocks, so you can format your template without worrying about indentation.
+
+Here is an example template::
+
+ %if name == 'World':
+ <h1>Hello {{name}}!</h1>
+ <p>This is a test.</p>
+ %else:
+ <h1>Hello {{name.title()}}!</h1>
+ <p>How are you?</p>
+ %end
+
+Follow the link for a full syntax description: :doc:`stpl`. For details on the other template engines, please see their respective documentation.
+
+.. highlight:: python
+
+Template Lookup and Configuration
+------------------------------------------------------------------------------
+
+Bottle looks for templates in all folders specified in ``bottle.TEMPLATE_PATH`` and adds ``.tpl`` to the name if it can't find a direct match. The path list defaults to ``['./', './views/']`` and is searched in order.
+
+.. versionchanged: 0.11
+
+Starting with Bottle 0.11 you can configure templates on a per-application basis::
+
+ app = Bottle() # or app = default_app()
+
+ #: Add a new absolute path to the list of lookup paths.
+ app.views.path.append('/some/app/specific/lookup/path/')
+
+ #: Add a path relative to the location of your module file.
+ app.views.add_path('./data/views/', __file__)
+
+ #: Add an additional format string that is used to turn template names
+ #: into real filenames.
+ app.views.masks.append('%.thtml')
+
+ #: Define some global variables that should be available in all templates.
+ app.views.globals['some_variable'] = some_value
+
+ #: Use a different template engine for this application.
+ app.views.adapter = bottle.Jinja2Template
+
+ #: Configure the adapter with engine-specific settings.
+ app.views.options['some_setting'] = some_value
+
+These only affects the ``template`` keyword parameter and does not work with the old :func:`template` functions. This may change in the future, though.
+
+
+
+
+
+
.. _tutorial-response:
The :class:`Response` Object
---------------------------------------------------------------------------------
+==============================================================================
Response metadata such as the HTTP status code, response headers and cookies are stored in an object called :data:`response` up to the point where they are transmitted to the browser. You can manipulate these metadata directly or use the predefined helper methods to do so. The full API and feature list is described in the API section (see :class:`Response`), but the most common use cases and features are covered here, too.
-.. rubric:: Status Code
+Status Code
+-------------------------------------------------------------------------------
The `HTTP status code <http_code>`_ controls the behavior of the browser and defaults to ``200 OK``. In most scenarios you won't need to set the :attr:`Response.status` attribute manually, but use the :func:`abort` helper or return an :exc:`HTTPResponse` instance with the appropriate status code. Any integer is allowed, but codes other than the ones defined by the `HTTP specification <http_code>`_ will only confuse the browser and break standards.
-.. rubric:: Response Header
+Response Header
+-------------------------------------------------------------------------------
Response headers such as ``Cache-Control`` or ``Location`` are defined via :meth:`Response.set_header`. This method takes two parameters, a header name and a value. The name part is case-insensitive::
@@ -480,7 +593,7 @@ In addition, Bottle automatically pickles and unpickles any data stored to signe
.. _tutorial-request:
-Request Data
+The :class:`Request` Object
==============================================================================
Bottle provides access to HTTP-related metadata such as cookies, headers and POST form data through a global ``request`` object. This object always contains information about the *current* request, as long as it is accessed from within a callback function. This works even in multi-threaded environments where multiple requests are handled at the same time. For details on how a global object can be thread-safe, see :doc:`contextlocal`.
@@ -581,55 +694,6 @@ Each :class:`BaseRequest` instance wraps a WSGI environment dictionary. The orig
-
-
-.. _tutorial-templates:
-
-Templates
-================================================================================
-
-Bottle comes with a fast and powerful built-in template engine called :doc:`stpl`. To render a template you can use the :func:`template` function or the :func:`view` decorator. All you have to do is to provide the name of the template and the variables you want to pass to the template as keyword arguments. Here’s a simple example of how to render a template::
-
- @route('/hello')
- @route('/hello/<name>')
- def hello(name='World'):
- return template('hello_template', name=name)
-
-This will load the template file ``hello_template.tpl`` and render it with the ``name`` variable set. Bottle will look for templates in the ``./views/`` folder or any folder specified in the ``bottle.TEMPLATE_PATH`` list.
-
-The :func:`view` decorator allows you to return a dictionary with the template variables instead of calling :func:`template`::
-
- @route('/hello')
- @route('/hello/<name>')
- @view('hello_template')
- def hello(name='World'):
- return dict(name=name)
-
-.. rubric:: Syntax
-
-.. highlight:: html+django
-
-The template syntax is a very thin layer around the Python language. Its main purpose is to ensure correct indentation of blocks, so you can format your template without worrying about indentation. Follow the link for a full syntax description: :doc:`stpl`
-
-Here is an example template::
-
- %if name == 'World':
- <h1>Hello {{name}}!</h1>
- <p>This is a test.</p>
- %else:
- <h1>Hello {{name.title()}}!</h1>
- <p>How are you?</p>
- %end
-
-.. rubric:: Caching
-
-Templates are cached in memory after compilation. Modifications made to the template files will have no affect until you clear the template cache. Call ``bottle.TEMPLATES.clear()`` to do so. Caching is disabled in debug mode.
-
-.. highlight:: python
-
-
-
-
.. _plugins:
Plugins
@@ -798,32 +862,10 @@ Both :func:`app` and :func:`default_app` are instance of :class:`AppStack` and i
app = default_app.pop()
-.. _tutorial-debugging:
-
-
-Debug Mode
---------------------------------------------------------------------------------
-
-During early development, the debug mode can be very helpful.
-
-.. highlight:: python
-
-::
-
- bottle.debug(True)
-
-In this mode, Bottle is much more verbose and provides helpful debugging information whenever an error occurs. It also disables some optimisations that might get in your way and adds some checks that warn you about possible misconfiguration.
-
-Here is an incomplete list of things that change in debug mode:
+.. _reloading:
-* The default error page shows a traceback.
-* Templates are not cached.
-* Plugins are applied immediately.
-
-Just make sure to not use the debug mode on a production server.
-
-Auto Reloading
---------------------------------------------------------------------------------
+Code Reloading
+------------------------------------------------------------------------------
During development, you have to restart the server a lot to test your
recent changes. The auto reloader can do this for you. Every time you
diff --git a/test/test_template_plugin.py b/test/test_template_plugin.py
new file mode 100644
index 0000000..272c0ed
--- /dev/null
+++ b/test/test_template_plugin.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+import unittest
+import bottle
+import tools
+
+class TestTemplatePlugin(tools.ServerTestBase):
+
+ def test_basic(self):
+ @self.app.route('/a', template='{{a}}-{{b}}')
+ def test():
+ return dict(a='foo', b='bar')
+ @self.app.route('/b', template='{{a}}-{{b}}')
+ def test():
+ return 'no-dict'
+ @self.app.route('/c',)
+ def test():
+ return {}
+ self.assertBody('foo-bar', '/a')
+ self.assertBody('no-dict', '/b')
+ self.assertBody('{}', '/c')
+
+ def test_globals(self):
+ @self.app.route('/', template='{{a}}-{{b}}')
+ def test():
+ return dict(b='bar')
+ self.app.views.globals['a'] = 'faa'
+ self.assertBody('faa-bar', '/')
+ self.app.views.globals['a'] = 'fee'
+ self.assertBody('fee-bar', '/')
+
+ def test_options(self):
+ @self.app.route('/', template='{{a}}-{{!b}}')
+ def test():
+ return dict(a='&a', b='&b')
+ self.assertBody('&amp;a-&b', '/')
+ self.app.views.options['noescape'] = True
+ self.app.views.cache.clear()
+ self.assertBody('&a-&amp;b', '/')
+
+ def test_appconfig(self):
+ self.app.config.view_globals['foo'] = 'bar'
+ self.assertEqual(self.app.views.globals, self.app.config.view_globals)
+
+ def test_path_helper(self):
+ self.assertFalse(self.app.views.lookup('stpl_simple.tpl'))
+ self.app.views.add_path('./views/', __file__)
+ self.assertTrue(self.app.views.lookup('stpl_simple.tpl'))
+ self.assertFalse(self.app.views.lookup('white_rabbit.tpl'))
+
+
+
+if __name__ == '__main__': #pragma: no cover
+ unittest.main()
+
+
diff --git a/test/test_wsgi.py b/test/test_wsgi.py
index ce2612f..60ff066 100755
--- a/test/test_wsgi.py
+++ b/test/test_wsgi.py
@@ -239,7 +239,7 @@ class TestRouteDecorator(ServerTestBase):
self.assertBody('test 5 6', '/test')
def test_template_opts(self):
- @bottle.route(template='test {{a}} {{b}}', template_opts={'b': 6})
+ @bottle.route(template=('test {{a}} {{b}}', {'b': 6}))
def test(): return dict(a=5)
self.assertBody('test 5 6', '/test')