diff options
author | Marcel Hellkamp <marc@gsites.de> | 2010-08-11 12:15:17 +0200 |
---|---|---|
committer | Marcel Hellkamp <marc@gsites.de> | 2010-08-11 12:15:17 +0200 |
commit | a71b7a4744770c39984f41656fadd4a146b05bf3 (patch) | |
tree | fab1ac6cd126365a5fd3426e8df2965355056daa | |
parent | 21249978d2a6a7015c94cb8611af1d90a039fa89 (diff) | |
parent | b73f62b118275d45a98870ce2426c32735c83ffd (diff) | |
download | bottle-a71b7a4744770c39984f41656fadd4a146b05bf3.tar.gz |
Merge branch 'master' into contextlocal
Conflicts:
bottle.py
39 files changed, 1518 insertions, 380 deletions
@@ -10,3 +10,4 @@ release.sh test.sh apidoc/sphinx/build/ apidoc/html/ +*.log diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d1f9129 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: test coverage docs html_coverage release clean + + +test: + bash run_tests.sh + +coverage: + python test/testall.py coverage + +html_coverage: + python test/testall.py coverage html + +docs: + cd apidoc/; $(MAKE) html + mkdir -p build + rm -rf build/docs + mv apidoc/html build/docs + +release: + python setup.py release sdist upload + +clean: + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '._*' -exec rm -f {} + + find . -name '.coverage*' -exec rm -f {} + @@ -3,10 +3,11 @@ Bottle Web Framework <div style="float: right; padding: 0px 0px 2em 2em"><img src="http://bottle.paws.de/bottle-logo.png" alt="Bottle Logo" /></div> -Bottle is a fast and simple [WSGI][wsgi]-framework for the [Python Programming Language][py]. It -offers request dispatching with url parameter support (routes), templates, key/value -databases, a build-in HTTP server and adapters for many third party -WSGI/HTTP-server and template engines - all in a single file and with no dependencies other than the Python Standard Library. +Bottle is a fast and simple [WSGI][wsgi]-framework for the +[Python Programming Language][py]. It offers request dispatching with url +parameter support (routes), templates, a built-in HTTP server and adapters for +many third party WSGI/HTTP-server and template engines - all in a single file +and with no dependencies other than the Python Standard Library. For news, bugs and documentation visit the [bottle.py homepage][home]. @@ -19,7 +20,7 @@ For news, bugs and documentation visit the [bottle.py homepage][home]. Installation and Dependencies ----------------------------- -You can install bottle with `easy_install bottle` or just [download][bottle-dl] bottle.py and place it in your project directory. There are no (hard) dependencies other than the Python Standard Library. +You can install bottle with `pip install bottle` or just [download][bottle-dl] bottle.py and place it in your project directory. There are no (hard) dependencies other than the Python Standard Library. [mako]: http://www.makotemplates.org/ [cherrypy]: http://www.cherrypy.org/ diff --git a/apidoc/api.rst b/apidoc/api.rst index 170f932..d3db5c9 100755 --- a/apidoc/api.rst +++ b/apidoc/api.rst @@ -7,7 +7,7 @@ API Reference :synopsis: WSGI micro framework .. moduleauthor:: Marcel Hellkamp <marc@paws.de> -This is an API reference, NOT a documentation. If you are new to bottle, have a look at the :doc:`tutorial`. +This is an API reference, NOT documentation. If you are new to bottle, have a look at the :doc:`tutorial`. Module Contents ===================================== @@ -20,6 +20,10 @@ The module defines several functions, constants, and an exception. .. autofunction:: debug +.. autofunction:: run + +.. autofunction:: load_app + .. autodata:: request .. autodata:: response @@ -112,7 +116,7 @@ The :class:`Response` class on the other hand stores header and cookie data that .. note:: - You usually don't instantiate :class:`Request` or :class:`Response` yourself, but use the module-level instances :data:`bottle.request` and :data:`bottle.response` only. These hold the context for the current request cycle and are updated on every request. Their attributes are thread-local, so it is save to use the global instance in multi-threaded environments too. + You usually don't instantiate :class:`Request` or :class:`Response` yourself, but use the module-level instances :data:`bottle.request` and :data:`bottle.response` only. These hold the context for the current request cycle and are updated on every request. Their attributes are thread-local, so it is safe to use the global instance in multi-threaded environments too. .. autoclass:: Request :members: diff --git a/apidoc/changelog.rst b/apidoc/changelog.rst index e6b3201..bf70c58 100644..100755 --- a/apidoc/changelog.rst +++ b/apidoc/changelog.rst @@ -5,29 +5,48 @@ Release Notes and Changelog =========================== -Release 0.7 + +Release 0.9 =========== -.. rubric:: API changes +This changes are not released yet and are only part of the development documentation. + +.. rubric:: New Features + +* A new hook-API to inject code immediately before or after the execution of handler callbacks. + + +Bugfix Release 0.8.2 +===================== +* Added backward compatibility wrappers and deprecation warnings to some of the API changes. +* Fixed "FileCheckerThread seems to fail on eggs" (Issue #87) +* Fixed "Bottle.get_url() does not return correct path when SCRIPT_NAME is set." (Issue #83) + + +Release 0.8 +=========== + +.. rubric:: API changes These changes may break compatibility with previous versions. -* The build-in Key/Value database is not available anymore. It is marked deprecated since 0.6.4 +* The built-in Key/Value database is not available anymore. It is marked deprecated since 0.6.4 * The Route syntax and behaviour changed. * Regular expressions must be encapsulated with ``#``. In 0.6 all non-alphanumeric characters not present in the regular expression were allowed. * Regular expressions not part of a route wildcard are escaped automatically. You don't have to escape dots or other regular control characters anymore. In 0.6 the whole URL was interpreted as a regular expression. You can use anonymous wildcards (``/index:#(\.html)?#``) to achieve a similar behaviour. - * Wildcards are escaped with a second colon (``/path/::not_a_wildcard``). -* The ``BreakTheBottle`` exception is gone. Use :class:`HTTPResponse`` instead. +* The ``BreakTheBottle`` exception is gone. Use :class:`HTTPResponse` instead. * The :class:`SimpleTemplate` engine escapes HTML special characters in ``{{bad_html}}`` expressions automatically. Use the new ``{{!good_html}}`` syntax to get old behaviour (no escaping). * The :class:`SimpleTemplate` engine returns unicode strings instead of lists of byte strings. * ``bottle.optimize()`` and the automatic route optimization is obsolete. -* :attr:`Request._environ` was renamed to :attr:`Request.environ` +* Some functions and attributes were renamed: + * :attr:`Request._environ` is now :attr:`Request.environ` + * :attr:`Response.header` is now :attr:`Response.headers` + * :func:`default_app` is obsolete. Use :func:`app` instead. * The default :func:`redirect` code changed from 307 to 303. * Removed support for ``@default``. Use ``@error(404)`` instead. -* `default_app()` is obsolete. Use :func:`app` instead. .. rubric:: New features diff --git a/apidoc/contact.rst b/apidoc/contact.rst index 5e87246..4c7188e 100755 --- a/apidoc/contact.rst +++ b/apidoc/contact.rst @@ -6,9 +6,9 @@ Contact .. image:: _static/myface_small.png :alt: Photo - :align: left + :class: floatright -Hi, I'm *Marcel Hellkamp* (aka *defnull*), author of Bottle and the guy behind this blog and website. I'm 25 years old and studying computer science at the Georg-August-University in Göttingen, Germany. Python is my favorite language, but I also code in ruby and JavaScript a lot. Watch me on `twitter <http://twitter.com/bottlepy>`_ or visit my profile at `GitHub <http://github.com/defnull>`_ to get in contact. A `mailinglist <http://groups.google.de/group/bottlepy>`_ is open for Bottle related questions, too. +Hi, I'm *Marcel Hellkamp* (aka *defnull*), author of Bottle and the guy behind this website. I'm 26 years old and studying computer science at the Georg-August-University in Göttingen, Germany. Python is my favorite language, but I also code in ruby and JavaScript a lot. Watch me on `twitter <http://twitter.com/bottlepy>`_ or visit my profile at `GitHub <http://github.com/defnull>`_ to get in contact. A `mailinglist <http://groups.google.de/group/bottlepy>`_ is open for Bottle related questions, too. .. rubric:: About Bottle @@ -27,5 +27,4 @@ Zwecken ist ausdrücklich untersagt. * **Ort**: D - 37075 Göttingen * **Strasse**: Theodor-Heuss Strasse 13 * **Telefon**: +49 (0) 551 2509854 - * **E-Mail**: <img src='/email.png' alt='domain: paws.de, user: bottle' style="vertical-align:text-bottom;" /> - + * **E-Mail**: marc at gsites dot de diff --git a/apidoc/faq.rst b/apidoc/faq.rst index ea471e1..893e1d1 100644 --- a/apidoc/faq.rst +++ b/apidoc/faq.rst @@ -15,7 +15,7 @@ About Bottle Is bottle suitable for complex applications? --------------------------------------------- -Bottle is a *micro* framework designed for prototyping and building small web applications and services. It stays out of your way and allows you to get things done fast, but misses some advanced features and ready-to-use solutions found in other frameworks (MVC, ORM, Form validation, scaffolding, XML-RPC). Although it *is* possible to add these features and build complex applications with Bottle, you should consider using a full-stack Web Framework like pylons_ or paste_ instead. +Bottle is a *micro* framework designed for prototyping and building small web applications and services. It stays out of your way and allows you to get things done fast, but misses some advanced features and ready-to-use solutions found in other frameworks (MVC, ORM, form validation, scaffolding, XML-RPC). Although it *is* possible to add these features and build complex applications with Bottle, you should consider using a full-stack Web framework like pylons_ or paste_ instead. Common Problems and Pitfalls @@ -32,10 +32,6 @@ Bottle searches in ``./`` and ``./views/`` for templates. In a mod_python_ or mo bottle.TEMPLATE_PATH.insert(0,'/absolut/path/to/templates/') -or change the working directory:: - - os.chdir(os.path.dirname(__file__)) - so bottle searches the right paths. Dynamic Routes and Slashes diff --git a/apidoc/index.rst b/apidoc/index.rst index ae62f69..ff444ad 100755 --- a/apidoc/index.rst +++ b/apidoc/index.rst @@ -1,7 +1,6 @@ .. highlight:: python .. currentmodule:: bottle - .. _mako: http://www.makotemplates.org/ .. _cheetah: http://www.cheetahtemplate.org/ .. _jinja2: http://jinja.pocoo.org/2/ @@ -21,12 +20,11 @@ Bottle: Python Web Framework Bottle is a fast, simple and lightweight WSGI_ micro web-framework for Python_. It is distributed as a single file module and has no dependencies other than the `Python Standard Library <http://docs.python.org/library/>`_. -.. rubric:: Core Features * **Routing:** Requests to function-call mapping with support for clean and dynamic URLs. -* **Templates:** Fast and pythonic :ref:`build-in template engine <tutorial-templates>` and support for mako_, jinja2_ and cheetah_ templates. +* **Templates:** Fast and pythonic :ref:`built-in template engine <tutorial-templates>` and support for mako_, jinja2_ and cheetah_ templates. * **Utilities:** Convenient access to form data, file uploads, cookies, headers and other HTTP related metadata. -* **Server:** Build-in HTTP development server and support for paste_, fapws3_, flup_, cherrypy_ or any other WSGI_ capable HTTP server. +* **Server:** Built-in HTTP development server and support for paste_, fapws3_, `Google App Engine <http://code.google.com/intl/en-US/appengine/>`_, cherrypy_ or any other WSGI_ capable HTTP server. .. rubric:: Example: "Hello World" in a bottle @@ -48,20 +46,25 @@ Bottle is a fast, simple and lightweight WSGI_ micro web-framework for Python_. Install the latest stable release via PyPi_ (``easy_install -U bottle``) or download `bottle.py`__ (unstable) into your project directory. There are no hard [1]_ dependencies other than the Python standard library. Bottle runs with **Python 2.5+ and 3.x** (using 2to3) -Documentation +User's Guide =============== -The documentation is a work in progress. If you have questions not answered here, please check the :doc:`faq`, file a ticket at bottles issue_tracker_ or send an e-mail to the `mailing list <mailto:bottlepy@googlegroups.com>`_. +Start here if you want to learn how to use the bottle framework for web development. If you have any questions not answered here, feel free to ask the `mailing list <mailto:bottlepy@googlegroups.com>`_. .. toctree:: :maxdepth: 2 tutorial - recieps + stpl faq + +API Documentation +================== +Looking for a specific function, class or method? These chapters cover all the interfaces provided by the Framework and explain how to use them. + +.. toctree:: + :maxdepth: 2 + api - stpl - changelog - development Tutorials and Resources ======================= @@ -70,8 +73,19 @@ Tutorials and Resources :maxdepth: 2 tutorial_app + recipes +Development and Contribution +============================ +These chapters are intended for developers interested in the bottle development and release workflow. + +.. toctree:: + :maxdepth: 2 + + changelog + development + plugindev Licence ================== diff --git a/apidoc/plugindev.rst b/apidoc/plugindev.rst new file mode 100755 index 0000000..7b7fdb5 --- /dev/null +++ b/apidoc/plugindev.rst @@ -0,0 +1,249 @@ +.. module:: bottle + +================== +Plugin Development +================== + +Bottles core features cover most of the common use-cases, but as a micro-framework it has its limits. This is where "Plugins" come into play. Plugins add specific functionality to the framework in a convenient way and are portable and re-usable across applications. Browse the list of :doc:`available plugins <plugins>` and see if someone has solved your problem already. If not, then read ahead. This guide explains the use of middleware, decorators and the hook-api that makes writing plugins for bottle a snap. + +.. _best-practise: + +Best Practice +============= + +These rules are recommendations only, but following them makes it a lot easier for others to use your plugin. + +.. rubric:: Initializing Plugins + +Importing a plugin-module should not have any side-effects and particularly **never install the plugin automatically**. Instead, plugins should define a class or a function that handles initialization: + +The class-constructor or init-function should accept an instance of :class:`Bottle` as its first optional keyword argument and install the plugin to that application. If the `app`-argument is empty, the plugin should default to :func:`default_app`. All other arguments are specific to the plugin and optional. + +For consistency, function-names should start with "`init_`" and class-names should end in "`Plugin`". It is ok to add an ``init_*`` alias for a class, but the class itself should conform to PEP8. Example:: + + import bottle + + def init_myfeature(app=None): + if not app: + app = bottle.default_app() + + @app.hook('before_request') + def before_hook(): + pass + + class MyFeaturePlugin(object): + def __init__(app=None): + self.app = app or bottle.default_app() + self.app.add_hook('before_request', self.before_hook) + + def before_hook(): + pass + +.. rubric:: Plugin Configuration + +Plugins should use the :attr:`Bottle.config` dictionary and the ``plugin.[name]`` namespace for their configuration. This way it is possible to pre-configure plugins or change the configuration at runtime in a plugin-independent way. Example:: + + import bottle + + class MyFeaturePlugin(object): + def __init__(app=None): + self.app = app or bottle.default_app() + self.app.add_hook('before_request', self.before_hook) + + def before_hook(): + value = self.app.config.get('plugins.myfeature.key', 'default') + ... + +.. rubric:: WSGI Middleware + +WSGI middleware should not wrap the entire application object, but only the :meth:`Bottle.wsgi` method. This way the app object stays intact and more than one middleware can be applied without conflicts. + + + + +Writing Plugins +=============== + +In most cases, plug-ins are used to alter the the request/response circle in some way. They add, manipulate or remove information from the request and/or alter the data returned to the browser. Some plug-ins do not touch the request itself, but have other side effects such as opening and closing database connections or cleaning up temporary files. Apart from that, you can differentiate plug-ins by the point of contact with the application: + +Middleware + WSGI-middleware wraps an entire application. It is an application itself and calls the wrapped application internally. This way a middleware can alter both the incoming environ-dict and the response iterable before it is returned to the server. This is transparent to the wrapped application and does not require any special support or preparation. The downside of this approach is that the request and response objects are both not available and you have to deal with raw WSGI. + +Decorators + The decorator approach is best for wrapping a small number of routes while leaving all other callbacks untouched. If your application requires session support or database connections for only some of the routes, choose this approach. With a decorator you have full access to the request and response objects and the unfiltered return value of the wrapped callback. + +Hooks + .. versionadded:: 0.9 + + With `hooks` you can register functions to be called at specific stages during the request circle. The most interesting hooks are `before_request` and `after_request`. Both affect all routes in an application, have full control over the request and response objects and can manipulate the route-callback return value at will. This new API fills the gap between middleware and decorators and is described in detail further down this guide. + +Which technique is best for your plugin depends on the level and scope of interaction you need with the framework and application. Combinations are possible, too. The following table sums it up: + +========================== ========== ===== ========== +Aspect Middleware Hooks Decorators +========================== ========== ===== ========== +Affects whole application Yes Yes No +Access to Bottle features No Yes Yes +========================== ========== ===== ========== + +Writing Middleware +------------------- + +WSGI middleware is not specific to Bottle and there are `several <http://www.python.org/dev/peps/pep-0333/#middleware-components-that-play-both-sides>`_ `detailed <http://www.rufuspollock.org/2006/09/28/wsgi-middleware/>`_ `explanations <http://pylonshq.com/docs/en/0.9.7/concepts/#wsgi-middleware>`_ and `collections <http://wsgi.org/wsgi/Middleware_and_Utilities>`_ available. If you want to apply a WSGI middleware, wrap the :class:`Bottle` application object and you're done:: + + app = bottle.app() # Get the WSGI callable from bottle + app = MyMiddleware(app=app) # Wrap it + bottle.run(app) # Run it + +This approach works fine, but is not very portable (see :ref:`best-practise`). A more general approach is to define a function that takes care of the plugin initialization and keeps the original application object intact:: + + import bottle + def init_my_middleware(app=None, **config): + # Default to the global application object + if not app: + app = bottle.app() + # Do not wrap the entire application, but only the WSGI part + app.wsgi = MyMiddleware(app=app.wsgi, config=config) + +Now ``app`` is still an instance of :class:`Bottle` and all methods remain accessible. Other plugins can wrap ``app.wsgi`` again without any conflicts. + + + +Writing Decorators +------------------- + +Bottle uses decorators all over the place, so you should already now how to use them. Writing a decorator (or a decorator factory, see below) is not that hard, too. Basically a decorator is a function that takes a function object and returns either the same or a new function object. This way it is possible to `wrap` a function and alter its input and output whenever that function gets called. Decorators are an extremely flexible way to reduce repetitive work:: + + from bottle import route + + def integer_id(func): + ''' Make sure that the ``id`` keyword argument is an integer. ''' + def wrapper(*args, **kwargs): + if 'id' in kwargs and not isinstance(kwargs['id'], int): + kwargs['id'] = int(kwargs['id']) + return func(*args, **kwargs) + return wrapper + + @route('/get/:id#[0-9]+#') + @integer_id + def get_object(id, ...): + ... + +.. note:: + Decorators are applied in reverse order (the decorator closest to the 'def' statement is applied first). This is important if you want to apply more than one decorator. + +.. rubric:: Decorator factories: Configurable decorators + +Let's go one step further: A `decorator factory` is a function that return a decorator. Because inner functions have access to the local variables of the outer function they were defined in, we can use this to configure the behavior of our decorator. Here is an example:: + + from bottle import request, response, abort + + def auth_required(users, realm='Secure Area'): + def decorator(func): + def wrapper(*args, **kwargs): + name, password = request.auth() + if name not in users or users[name] != password: + response.headers['WWW-Authenticate'] = 'Basic realm="%s"' % realm + abort('401', 'Access Denied. You need to login first.') + kwargs['user'] = name + return func(*args, **kwargs) + return wrapper + return decorator + + @route('/secure/area') + @auth_required(users={'Bob':'1234'}) + def secure_area(user): + print 'Hello %s' % user + +Of cause it is a bad idea to store clear passwords in a dictionary. But besides that, this example is actually quite complete and usable as it is. + +Using Hooks +---------------- + +.. versionadded:: 0.9 + +As described above, hooks allow you to register functions to be called at specific stages during the request circle. There are currently only two hooks available: + +before_request + This hook is called immediately before each route callback. + +after_request + This hook is called immediately after each route callback. + +You can use the :func:`hook` or :meth:`Bottle.hook` decorator to register a function to a hook. This example shows how to open and close a database connection (SQLite 3) with each request:: + + import sqlite3 + import bottle + + def init_sqlite(app=None, dbfile=':memory:'): + if not app: + app = bottle.app() + + @app.hook('before_request') + def before_request(): + bottle.local.db = sqlite3.connect(dbfile) + + @app.hook('after_request') + def after_request(): + bottle.local.db.close() + +The :data:`local` object is used to store the database handle during the request. It is a thread-save object (just like :data:`request` and :data:`response` are) even if it looks like a global module variable. Here is an example for an application using this plugin:: + + from bottle import default_app, local, route, run + from plugins.sqlite import init_sqlite # Or whatever you named your plugin + + @route('/wiki/:name') + @view('wiki_page') + def show_page(name): + sql = 'select title, text rom wiki_pages where name = ?' + cursor = local.db.execute(sql, name) + entry = cursor.fetch() + return dict(name=name, title=entry[0], text=entry[1]) + + init_sqlite(dbfile='wiki.db') # Install plugin to default app + + if __name__ == '__main__': + run() # Run default app + +.. rubric:: Plugin Classes + +The problem with the last example is that you cannot access the plugin or the database object outside of a running server instance. Let's rewrite the plugin and use a class this time:: + + import sqlite3 + import bottle + + class SQlitePlugin(object): + def __init__(self, app=None, dbfile=':memory:'): + self.app = app or bottle.app() + self.dbfile = dbfile + + @self.app.hook('before_request') + def before_request(): + bottle.local.db = self.connect() + + @self.app.hook('after_request') + def after_request(): + bottle.local.db.close() + + def connect(self): + return sqlite3.connect(self.dbfile) + + init_sqlite = SQlitePlugin # Alias for consistency + +Now we can access the ``connect()`` method outside of a route callback and even reconfigure the plugin at runtime:: + + # [...] same as wiki-app example above + # but this time, we save the return value of init_sqlite() + sqlite_plugin = init_sqlite(dbfile='wiki.db') + + if __name__ == '__main__': + if 'development' in sys.argv: + sqlite_plugin.dbfile = ':memory:' # reconfigure plugin + db = sqlite_plugin.connect() # reuse plugin methods + db.execfile('test_database.sql') + db.commit() + db.close() + run() # Run default app + +Now if we call this script with a ``development`` command-line flag, it uses a memory-mapped test database instead of the real one. + diff --git a/apidoc/recipes.rst b/apidoc/recipes.rst new file mode 100644 index 0000000..47a7ad4 --- /dev/null +++ b/apidoc/recipes.rst @@ -0,0 +1,99 @@ +.. module:: bottle + +.. _beaker: http://beaker.groovie.org/ +.. _mod_python: http://www.modpython.org/ +.. _mod_wsgi: http://code.google.com/p/modwsgi/ +.. _werkzeug: http://werkzeug.pocoo.org/documentation/dev/debug.html +.. _paste: http://pythonpaste.org/modules/evalexception.html +.. _pylons: http://pylonshq.com/ + +Recipes +============= + +This is a collection of code snippets and examples for common use cases. + +Keeping track of Sessions +---------------------------- + +There is no built-in support for sessions because there is no *right* way to do it (in a micro framework). Depending on requirements and environment you could use beaker_ middleware with a fitting backend or implement it yourself. Here is an example for beaker sessions with a file-based backend:: + + import bottle + from beaker.middleware import SessionMiddleware + + session_opts = { + 'session.type': 'file', + 'session.cookie_expires': 300, + 'session.data_dir': './data', + 'session.auto': True + } + app = SessionMiddleware(bottle.app(), session_opts) + + @bottle.route('/test') + def test(): + s = bottle.request.environ.get('beaker.session') + s['test'] = s.get('test',0) + 1 + s.save() + return 'Test counter: %d' % s['test'] + + bottle.run(app=app) + +Debugging with Style: Debugging Middleware +-------------------------------------------------------------------------------- + +Bottle catches all Exceptions raised in your app code to prevent your WSGI server from crashing. If the built-in :func:`debug` mode is not enough and you need exceptions to propagate to a debugging middleware, you can turn off this behaviour:: + + import bottle + app = bottle.app() + app.catchall = False #Now most exceptions are re-raised within bottle. + myapp = DebuggingMiddleware(app) #Replace this with a middleware of your choice (see below) + bottle.run(app=myapp) + +Now, bottle only catches its own exceptions (:exc:`HTTPError`, :exc:`HTTPResponse` and :exc:`BottleException`) and your middleware can handle the rest. + +The werkzeug_ and paste_ libraries both ship with very powerfull debugging WSGI middleware. Look at :class:`werkzeug.debug.DebuggedApplication` for werkzeug_ and :class:`paste.evalexception.middleware.EvalException` for paste_. They both allow you do inspect the stack and even execute python code within the stack context, so **do not use them in production**. + + +Embedding other WSGI Apps +-------------------------------------------------------------------------------- + +This is not the recommend way (you should use a middleware in front of bottle to do this) but you can call other WSGI applications from within your bottle app and let bottle act as a pseudo-middleware. Here is an example:: + + from bottle import request, response, route + subproject = SomeWSGIApplication() + + @route('/subproject/:subpath#.*#', method='ALL') + def call_wsgi(subpath): + new_environ = request.environ.copy() + new_environ['SCRIPT_NAME'] = new_environ.get('SCRIPT_NAME','') + '/subproject' + new_environ['PATH_INFO'] = '/' + subpath + def start_response(status, headerlist): + response.status = int(status.split()[0]) + for key, value in headerlist: + response.add_header(key, value) + return app(new_environ, start_response) + +Again, this is not the recommend way to implement subprojects. It is only here because many people asked for this and to show how bottle maps to WSGI. + + +Ignore trailing slashes +-------------------------------------------------------------------------------- + +For Bottle, ``/example`` and ``/example/`` are two different routes. To treat both URLs the same you can add two ``@route`` decorators:: + + @route('/test') + @route('/test/') + def test(): return 'Slash? no?' + +or add a WSGI middleware that strips trailing slashes from all URLs:: + + class StripPathMiddleware(object): + def __init__(self, app): + self.app = app + def __call__(self, e, h): + e['PATH_INFO'] = e['PATH_INFO'].rstrip('/') + return self.app(e,h) + + app = bottle.app() + myapp = StripPathMiddleware(app) + bottle.run(app=appmy) + diff --git a/apidoc/sphinx/conf.py b/apidoc/sphinx/conf.py index d994c24..5f6263e 100644 --- a/apidoc/sphinx/conf.py +++ b/apidoc/sphinx/conf.py @@ -135,7 +135,10 @@ html_last_updated_fmt = '%b %d, %Y' #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +html_sidebars = { + 'index': ['sidebar-intro.html', 'sourcelink.html', 'searchbox.html'], + '**': ['localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'] +} # Additional templates that should be rendered to pages, maps page names to # template names. @@ -151,7 +154,7 @@ html_last_updated_fmt = '%b %d, %Y' #html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +#html_show_sourcelink = False # If true, an OpenSearch description file will be output, and all pages will # contain a <link> tag referring to it. The value of this option must be the diff --git a/apidoc/sphinx/static/bottle.css b/apidoc/sphinx/static/bottle.css deleted file mode 100644 index d809b1a..0000000 --- a/apidoc/sphinx/static/bottle.css +++ /dev/null @@ -1,58 +0,0 @@ -@import url("default.css"); -/* -body { -background: #eee url("http://bottle.paws.de/logo_bg.png") no-repeat scroll 0 0; -} - -div.body { -border: 1px solid #ccc; -margin-right: 20px; -} - -div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { -border-bottom: 3px solid #003333; -border: 0px; -font-weight: bold; -margin: 20px -20px 10px; -padding: 3px 0 3px 10px; -}*/ - -div.body { - background: #fff url("logo_bg.png") no-repeat scroll right 3em; -} - -div.sphinxsidebar ul, div.sphinxsidebar ul ul, div.sphinxsidebar ul.want-points { - list-style-image:none; - //list-style-position:inside; - list-style-type:disc; -} - -div.body dt { - font-weight: bold; -} - -pre { - background-color: #eee; - border: 1px solid #ddd; - border-width: 0 0 0 5px; - padding: 5px 10px; -} - -img.align-left { - margin: 5px; -} - -h1, h2, h3, h4, h5, h6, h7, p.rubric { - clear: both; -} - -div.sphinxsidebar ul { - font-size: 0.9em; -} - -p.rubric { - background: #eee; - border-left: 5px solid #1C4E63; - padding-left: 5px; -} - diff --git a/apidoc/sphinx/static/bottle.css_t b/apidoc/sphinx/static/bottle.css_t new file mode 100755 index 0000000..6dc7590 --- /dev/null +++ b/apidoc/sphinx/static/bottle.css_t @@ -0,0 +1,175 @@ +{% set page_width = '940px' %} +{% set sidebar_width = '230px' %} + +@import url("basic.css"); + +/* Positional Layout */ + +body { + width: {{ page_width }}; + margin: 0 auto 0 auto; + padding: 15px; +} + +div.document { +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 {{ sidebar_width }}; +} + +div.sphinxsidebar { + width: {{ sidebar_width }}; +} + +div.sphinxsidebarwrapper { + margin: 0 15px; + padding: 0; +} + +div.related { + line-height: 30px; + display: none; +} + +/* Design and Colors */ + +body { + background: #eee url("_static/logo_bg.png") no-repeat scroll 0 0; + font-family: 'Veranda', sans-serif; + font-size: 16px; +} + +div.body { + border: 1px solid #bbb; + background-color: #fff; + padding: 0 15px 15px 15px; + color: #222; + line-height: 1.4em; +} + +a { + color:#005566; + text-decoration: none; +} + +a:hover { + color: black; + text-decoration: underline; +} + +/* Notes and Codes */ + +pre, div.admonition { + background: #fafafa; + margin: 20px -15px; + padding: 10px 30px; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} + +dd pre, dd div.admonition { + margin: 1em 0; + padding: 5px 5px; + border: 1px solid #ccc; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.admonition p.admonition-title:after { + content:":"; +} + +div.warning { + background-color: #fee; +} + +div.note { + background-color: #ffd; +} + +pre { + font-size: 12px; + line-height: 1.2em; +} + +code, tt.docutils { + border: 1px dotted #ddd; + background-color: #eee; + font-size: 85%; +} + +tt.xref { + border: 0 !important; + background-color: transparent !important; +} + +/* Misc */ + +img.floatright { + float: right; + margin: 0.5em 1em; +} + +div.body { + border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; +} + +div.sphinxsidebar ul { + margin: 15px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar h3 { + background-color: #ddd; + margin: 0 -15px 0 -5px; + padding: 2px 5px; + border-top-left-radius: 10px; + -moz-border-radius-topleft: 10px; + -webkit-border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + -moz-border-radius-bottomleft: 10px; + -webkit-border-bottom-left-radius: 10px; +} + +div.sphinxsidebar a { + color: #002a32; +} + +div.sphinxsidebar h3 a { + color: #000; +} + +div.sphinxsidebar ul ul { + list-style: disc outside none; +} + +div.sphinxsidebar input { + border: 1px solid grey; +} + +div.sphinxsidebar input:hover { + border: 1px solid black; +} + +div.sphinxsidebar input:focus { + border: 1px solid black; +} + +div.footer { + text-align: center; + font-size: 75%; + padding: 1em; + opacity: 0.5; +} + diff --git a/apidoc/sphinx/static/logo_full.png b/apidoc/sphinx/static/logo_full.png Binary files differnew file mode 100644 index 0000000..e003c02 --- /dev/null +++ b/apidoc/sphinx/static/logo_full.png diff --git a/apidoc/sphinx/static/logo_icon.png b/apidoc/sphinx/static/logo_icon.png Binary files differnew file mode 100644 index 0000000..eb1bf16 --- /dev/null +++ b/apidoc/sphinx/static/logo_icon.png diff --git a/apidoc/sphinx/static/logo_nav.png b/apidoc/sphinx/static/logo_nav.png Binary files differindex 798e479..7086584 100644 --- a/apidoc/sphinx/static/logo_nav.png +++ b/apidoc/sphinx/static/logo_nav.png diff --git a/apidoc/sphinx/static/logo_reddit.png b/apidoc/sphinx/static/logo_reddit.png Binary files differnew file mode 100644 index 0000000..75d46ed --- /dev/null +++ b/apidoc/sphinx/static/logo_reddit.png diff --git a/apidoc/sphinx/templates/layout.html b/apidoc/sphinx/templates/layout.html index 18790ed..9e280c0 100755 --- a/apidoc/sphinx/templates/layout.html +++ b/apidoc/sphinx/templates/layout.html @@ -1,8 +1,16 @@ {% extends "!layout.html" %} + {% block extrahead %} <link rel="shortcut icon" type="image/x-icon" href="{{ pathto('_static/favicon.ico', 1) }}" /> -{{ super() }} + <link rel="image_src" type="image/png" href="http://bottle.paws.de/docs/dev/_static/logo_reddit.png" /> + {{ super() }} +{% endblock %} + +{% block rootrellink %} + <li><a href="/">Project Home</a> »</li> + {{ super() }} {% endblock %} + {% block footer %} <div class="footer"> © <a href="{{ pathto('index') }}licence">Copyright</a> {{ copyright|e }} - <a href="{{ pathto('contact') }}">Contact</a><br /> diff --git a/apidoc/sphinx/templates/sidebar-intro.html b/apidoc/sphinx/templates/sidebar-intro.html new file mode 100644 index 0000000..df0c59e --- /dev/null +++ b/apidoc/sphinx/templates/sidebar-intro.html @@ -0,0 +1,28 @@ +<p> + This is the documentation of Bottle, a fast, simple and lightweight WSGI micro web-framework for Python. +</p> +<h3>Some Links</h3> +<ul> + <li><a href="http://bottle.paws.de/">The Bottle Website</a></li> + <li><a target="_blank" href="http://github.com/defnull/bottle/issues">Bottle Issue Tracker</a></li> + <li><a target="_blank" href="http://pypi.python.org/pypi/bottle">Bottle @ PyPI</a></li> + <li><a target="_blank" href="http://github.com/defnull/bottle">Bottle @ GitHub</a></li> + <li><a target="_blank" href="http://groups.google.de/group/bottlepy">Bottle @ Google Groups</a></li> + <li><a target="_blank" href="http://twitter.com/bottlepy">Bottle @ Twitter</a></li> + <li><a target="_blank" href="http://flattr.com/thing/21888/Bottle-A-Python-Web-Framework">Bottle @ Flattr</a></li> +</ul> +<h3>Installation</h3> +<p>Install Bottle with <code>pip install bottle</code> or download the source package at <a href="http://pypi.python.org/pypi/bottle">PyPI</a>.</p> +<h3>Documentation</h3> +<p> + You can download this documentation as <a href="http://bottle.paws.de/docs/bottle-docs.pdf">PDF</a> or <a href="http://bottle.paws.de/docs/bottle-docs.zip">HTML (zip)</a> for offline use. +</p> +<h3>Sources</h3> +<p>You can browse the sources at <a href="http://github.com/defnull/bottle">GitHub</a>.</p> +<h3>Other Releases</h3> +<ul> + <li><a href="http://bottle.paws.de/docs/dev/">Bottle dev (in development)</a></li> + <li><a href="http://bottle.paws.de/docs/0.8/">Bottle 0.8 (stable)</a></li> +</ul> + + diff --git a/apidoc/stpl.rst b/apidoc/stpl.rst index a6991fe..cc48989 100755 --- a/apidoc/stpl.rst +++ b/apidoc/stpl.rst @@ -4,7 +4,7 @@ SimpleTemplate Engine .. currentmodule:: bottle -Bottle comes with a fast, powerful and easy to learn build-in template engine called *SimpleTemplate* or *stpl* for short. It is the default engine used by the :func:`view` and :func:`template` helpers but can be used as a stand-alone general purpose template engine, too. This document explains the template syntax and shows examples for common use cases. +Bottle comes with a fast, powerful and easy to learn built-in template engine called *SimpleTemplate* or *stpl* for short. It is the default engine used by the :func:`view` and :func:`template` helpers but can be used as a stand-alone general purpose template engine too. This document explains the template syntax and shows examples for common use cases. .. rubric:: Basic API Usage: @@ -21,20 +21,20 @@ In this document we use the :func:`template` helper in examples for the sake of >>> template('Hello {{name}}!', name='World') u'Hello World!' -Just keep in mind that compiling and rendering templates are two different actions, even if the :func:`template` helper hides this fact. Templates are usually compiled only once and cached internally, but rendered many times with differend keyword arguments. +Just keep in mind that compiling and rendering templates are two different actions, even if the :func:`template` helper hides this fact. Templates are usually compiled only once and cached internally, but rendered many times with different keyword arguments. :class:`SimpleTemplate` Syntax ============================== -Python is a very powerful language but its whitespace-aware syntax makes it difficult to use as a templates language. SimpleTemplate removes some of these restrictions and allows you to write clean, readable and maintainable templates while preserving full access to the features, libraries and speed of the Python language. +Python is a very powerful language but its whitespace-aware syntax makes it difficult to use as a template language. SimpleTemplate removes some of these restrictions and allows you to write clean, readable and maintainable templates while preserving full access to the features, libraries and speed of the Python language. .. warning:: - The :class:`SimpleTemplate` Syntax compiles directly to python bytecode and is executed on each :meth:`SimpleTemplate.render` call. Do not render untrusted templates! They may contain and execute harmfull python code. + The :class:`SimpleTemplate` syntax compiles directly to python bytecode and is executed on each :meth:`SimpleTemplate.render` call. Do not render untrusted templates! They may contain and execute harmful python code. .. rubric:: Inline Statements -You already learned the use of the ``{{...}}`` statement from the "Hello World!" example above, but there is more: Any python statement is allowed within the curly brackets as long as it returns a string or something that has a string representation:: +You already learned the use of the ``{{...}}`` statement from the "Hello World!" example above, but there is more: any python statement is allowed within the curly brackets as long as it returns a string or something that has a string representation:: >>> template('Hello {{name}}!', name='World') u'Hello World!' @@ -54,7 +54,7 @@ The contained python statement is executed at render-time and has access to all .. rubric:: Embedded python code -The ``%`` character marks a line of python code. The only difference between this and real python code is that you have to explicitly close blocks with an ``%end`` statement. In return you can align the code with the surrounding template and don't have to worry about correct indention of blocks. The *SimpleTemplate* parser handles that for you. Lines *not* starting with a ``%`` are rendered as text as usual:: +The ``%`` character marks a line of python code. The only difference between this and real python code is that you have to explicitly close blocks with an ``%end`` statement. In return you can align the code with the surrounding template and don't have to worry about correct indentation of blocks. The *SimpleTemplate* parser handles that for you. Lines *not* starting with a ``%`` are rendered as text as usual:: %if name: Hi <b>{{name}}</b> @@ -62,7 +62,7 @@ The ``%`` character marks a line of python code. The only difference between thi <i>Hello stranger</i> %end -The ``%`` character is only recognised if it is the first non-whitespace character in a line. To escape a heading ``%`` you can add a second one. ``%%`` is replaced by a single ``%`` in the resulting template:: +The ``%`` character is only recognised if it is the first non-whitespace character in a line. To escape a leading ``%`` you can add a second one. ``%%`` is replaced by a single ``%`` in the resulting template:: This line contains a % but no python code. %% This text-line starts with '%' @@ -94,7 +94,7 @@ You can include other templates using the ``%include sub_template [kwargs]`` sta The ``%rebase base_template [kwargs]`` statement causes ``base_template`` to be rendered instead of the original template. The base-template then includes the original template using an empty ``%include`` statement and has access to all variables specified by ``kwargs``. This way it is possible to wrap a template with another template or to simulate the inheritance feature found in some other template engines. -Lets say you have a content-templates and want to wrap it with a common HTML layout frame. Instead of including several header and foother templates, you can use a single base-template to render the layout frame. +Let's say you have a content template and want to wrap it with a common HTML layout frame. Instead of including several header and footer templates, you can use a single base-template to render the layout frame. Base-template named ``layout.tpl``:: @@ -129,7 +129,7 @@ Now you can render ``content.tpl``: </body> </html> -A more complex scenario involves chained rebases and multiple content blocks. The ``block_content.tpl`` template defines two functions and passes them to a ``colums.tpl`` base template:: +A more complex scenario involves chained rebases and multiple content blocks. The ``block_content.tpl`` template defines two functions and passes them to a ``columns.tpl`` base template:: %def leftblock(): Left block content. @@ -143,10 +143,10 @@ The ``columns.tpl`` base-template uses the two callables to render the content o %rebase layout title=title <div style="width: 50%; float:left"> - %left() + %leftblock() </div> <div style="width: 50%; float:right"> - %right() + %rightblock() </div> Lets see how ``block_content.tpl`` renders: @@ -182,7 +182,7 @@ Lets see how ``block_content.tpl`` renders: Known bugs ============================== -Some syntax construcs allowed in python are problematic within a template. The following syntaxes won't work with SimpleTemplate: +Some syntax constructions allowed in python are problematic within a template. The following syntaxes won't work with SimpleTemplate: - * Multi-line statements must end with a backslash (``\``) and a comment, if presend, must not contain any additional ``#`` characters. + * Multi-line statements must end with a backslash (``\``) and a comment, if present, must not contain any additional ``#`` characters. * Multi-line strings are not supported yet. diff --git a/apidoc/tutorial.rst b/apidoc/tutorial.rst index 0949d1d..f221aa7 100755 --- a/apidoc/tutorial.rst +++ b/apidoc/tutorial.rst @@ -35,7 +35,7 @@ This tutorial introduces you to the concepts and features of the Bottle web fram * :ref:`tutorial-routing`: Web development starts with binding URLs to code. This section tells you how to do it. * :ref:`tutorial-output`: You have to return something to the Browser. Bottle makes it easy for you, supporting more than just plain strings. * :ref:`tutorial-request`: Each client request carries a lot of information. HTTP-headers, form data and cookies to name just three. Here is how to use them. -* :ref:`tutorial-templates`: You don't want to write HTML within your python code, do you? Template separate code from presentation. +* :ref:`tutorial-templates`: You don't want to write HTML within your python code, do you? Templates separate code from presentation. * :ref:`tutorial-debugging`: These tools and features will help you during development. * :ref:`tutorial-deployment`: Get it up and running. @@ -62,11 +62,11 @@ Whats happening here? 4. In this exmaple we simply return a string to the browser. 5. Now it is time to start the actual HTTP server. The default is a development server running on 'localhost' port 8080 and serving requests until you hit :kbd:`Control-c`. -This is it. Run this script, visit http://localhost:8080/hello and you will see "Hello World!" in your Browser. Of cause this is a very simple example, but it shows the basic concept of how applications are build with bottle. Continue reading and you'll see what else is possible. +This is it. Run this script, visit http://localhost:8080/hello and you will see "Hello World!" in your browser. Of cause 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. .. rubric:: The Application Object -Several functions and decorators such as :func:`route` or :func:`run` rely on a global application object to store routes, callbacks and configuration. This makes writing small application easy, but can lead to problems in more complex scenarios. If you prefer a more explicit way to define your application and don't mind the extra typing, you can create your own concealed application object and use that instead of the global one:: +Several functions and decorators such as :func:`route` or :func:`run` rely on a global application object to store routes, callbacks and configuration. This makes writing a small application easy, but can lead to problems in more complex scenarios. If you prefer a more explicit way to define your application and don't mind the extra typing, you can create your own concealed application object and use that instead of the global one:: from bottle import Bottle, run @@ -176,18 +176,18 @@ Static files such as images or css files are not served automatically. You have def server_static(filename): return static_file(filename, root='/path/to/your/static/files') -The :func:`static_file` function is a helper to serve files in a save and convenient way (see :ref:`tutorial-static-files`). This example is limited to files directly within the ``/path/to/your/static/files`` directory because the ``:filename`` wildcard won't match a path with a slash in it. To serve files in subdirectories too, we can loosen the wildcard a bit:: +The :func:`static_file` function is a helper to serve files in a safe and convenient way (see :ref:`tutorial-static-files`). This example is limited to files directly within the ``/path/to/your/static/files`` directory because the ``:filename`` wildcard won't match a path with a slash in it. To serve files in subdirectories too, we can loosen the wildcard a bit:: @route('/static/:path#.+#') def server_static(path): return static_file(path, root='/path/to/your/static/files') -Be carefull when specifying a relative root-path such as ``root='./static/files'``. The working directory (``./``) and the project directory are not always the same. +Be careful when specifying a relative root-path such as ``root='./static/files'``. The working directory (``./``) and the project directory are not always the same. Error Pages ------------------------------------------------------------------------------ -If anything goes wrong, Bottle displays an informative but fairly booring error page. You can override the default error pages using the :func:`error` decorator. It works similar to the :func:`route` decorator, but expects an HTTP status code instead of a route:: +If anything goes wrong Bottle displays an informative but fairly boring error page. You can override the default error pages using the :func:`error` decorator. It works similar to the :func:`route` decorator but expects an HTTP status code instead of a route:: @error(404) def error404(error): @@ -208,7 +208,7 @@ In pure WSGI, the range of types you may return from your application is very li Bottle is much more flexible and supports a wide range of types. It even adds a ``Content-Length`` header if possible and encodes unicode automatically, so you don't have to. What follows is a list of data types you may return from your application callbacks and a short description of how these are handled by the framework: Dictionaries - As I have already mentioned above, Python dictionaries (or subclasses thereof) are automatically transformed into JSON strings and returned to the browser with the ``Content-Type`` header set to ``application/json``. This makes it easy to implement json-bases APIs. Data formats other than json are supported too. See the :ref:`tutorial-output-filter` to learn more. + As mentioned above, Python dictionaries (or subclasses thereof) are automatically transformed into JSON strings and returned to the browser with the ``Content-Type`` header set to ``application/json``. This makes it easy to implement json-based APIs. Data formats other than json are supported too. See the :ref:`tutorial-output-filter` to learn more. Empty Strings, ``False``, ``None`` or other non-true values: These produce an empty output with ``Content-Length`` header set to 0. @@ -217,13 +217,13 @@ Unicode strings Unicode strings (or iterables yielding unicode strings) are automatically encoded with the codec specified in the ``Content-Type`` header (utf8 by default) and then treated as normal byte strings (see below). Byte strings - Bottle returns strings as a whole (instead of iterating over each char) and adds a ``Content-Length`` header based on the string length. Lists of byte strings are joined first. Other iterables yielding byte strings are not joined because they may grow to big to fit into memory. The ``Content-Length`` header is not set in this case. + Bottle returns strings as a whole (instead of iterating over each char) and adds a ``Content-Length`` header based on the string length. Lists of byte strings are joined first. Other iterables yielding byte strings are not joined because they may grow too big to fit into memory. The ``Content-Length`` header is not set in this case. Instances of :exc:`HTTPError` or :exc:`HTTPResponse` - Returning these has the same effect than raising them as an exception. In case of an :exc:`HTTPError`, the error handler are applied. See :ref:`tutorial-errorhandling` for details. + Returning these has the same effect as when raising them as an exception. In case of an :exc:`HTTPError`, the error handler is applied. See :ref:`tutorial-errorhandling` for details. File objects - Everything that has a ``.read()`` method is treated as a file or file-like object and passed to the ``wsgi.file_wrapper`` callable defined by the WSGI server framework. Some WSGI server implementations can make use of optimized system calls (sendfile) to transmit files more efficiently. In other cases this just iterates over chuncks that fit into memory. Optional headers such as ``Content-Length`` or ``Content-Type`` are *not* set automatically. Use :func:`send_file` if possible. See :ref:`tutorial-static-files` for details. + Everything that has a ``.read()`` method is treated as a file or file-like object and passed to the ``wsgi.file_wrapper`` callable defined by the WSGI server framework. Some WSGI server implementations can make use of optimized system calls (sendfile) to transmit files more efficiently. In other cases this just iterates over chunks that fit into memory. Optional headers such as ``Content-Length`` or ``Content-Type`` are *not* set automatically. Use :func:`send_file` if possible. See :ref:`tutorial-static-files` for details. Iterables and generators You are allowed to use ``yield`` within your callbacks or return an iterable, as long as the iterable yields byte strings, unicode strings, :exc:`HTTPError` or :exc:`HTTPResponse` instances. Nested iterables are not supported, sorry. Please note that the HTTP status code and the headers are sent to the browser as soon as the iterable yields its first non-empty value. Changing these later has no effect. @@ -232,7 +232,7 @@ The ordering of this list is significant. You may for example return a subclass .. rubric:: Changing the Default Encoding -Bottle uses the `charset` parameter of the ``Content-Type`` header to decide how to encode unicode strings. This header defaults to ``text/html; charset=UTF8`` and can be changed using the :attr:`Response.content_type` attribute or by setting the :attr:`Response.charset` attribute directly. (The :class:`Response` object is described in the section: :ref:`tutorial-response`):: +Bottle uses the `charset` parameter of the ``Content-Type`` header to decide how to encode unicode strings. This header defaults to ``text/html; charset=UTF8`` and can be changed using the :attr:`Response.content_type` attribute or by setting the :attr:`Response.charset` attribute directly. (The :class:`Response` object is described in the section :ref:`tutorial-response`.) from bottle import response @route('/iso') @@ -245,7 +245,7 @@ Bottle uses the `charset` parameter of the ``Content-Type`` header to decide how response.content_type = 'text/html; charset=latin9' return u'ISO-8859-15 is also known as latin9.' -In some rare cases the Python encoding names differ from the names supported by the HTTP specification. Then, you have to do both: First set the :attr:`Response.content_type` header (which is sent to the client unchanged) and then set the :attr:`Response.charset` attribute (which is used to decode unicode). +In some rare cases the Python encoding names differ from the names supported by the HTTP specification. Then, you have to do both: first set the :attr:`Response.content_type` header (which is sent to the client unchanged) and then set the :attr:`Response.charset` attribute (which is used to encode unicode). .. _tutorial-static-files: @@ -353,7 +353,7 @@ TODO Accessing Request Data ============================================================================== -Bottle provides access to HTTP related meta-data 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-save, see :doc:`contextlocal`. +Bottle provides access to HTTP related meta-data 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`. .. note:: Bottle stores most of the parsed HTTP meta-data in :class:`MultiDict` instances. These behave like normal dictionaries but are able to store multiple values per key. The standard dictionary access methods will only return a single value. Use the :meth:`MultiDict.getall` method do receive a (possibly empty) list of all values for a specific key. The :class:`HeaderDict` class inherits from :class:`MultiDict` and additionally uses case insensitive keys. @@ -379,7 +379,7 @@ Cookies are stored in :attr:`Request.COOKIES` as a normal dictionary. The :meth: from bottle import route, request, response @route('/counter') def counter(): - count = int( request.COOKIES.get('counter', '0') ) + 1 + count = int( request.COOKIES.get('counter', '0') ) count += 1 response.set_cookie('counter', str(count)) return 'You visited this page %d times' % count @@ -455,7 +455,7 @@ The :class:`Request` object stores the WSGI environment dictionary in :attr:`Req Templates ================================================================================ -Bottle comes with a fast and powerful build-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:: +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') @@ -476,7 +476,7 @@ The :func:`view` decorator allows you to return a dictionary with the template v .. highlight:: html+django -The template syntax is a very thin layer around the Python language. It's main purpose is to ensure correct indention of blocks, so you can format your template without worrying about indentions. Follow the link for a full syntax description: :doc:`stpl` +The template syntax is a very thin layer around the Python language. It's 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:: @@ -504,14 +504,14 @@ Templates are cached in memory after compilation. Modifications made to the temp Development ================================================================================ -Bottle has two features that may be helpfull during development. +Bottle has two features that may be helpful during development. Debug Mode -------------------------------------------------------------------------------- -In debug mode, bottle is much more verbose and tries to help you finding +In debug mode, bottle is much more verbose and tries to help you find bugs. You should never use debug mode in production environments. .. highlight:: python @@ -523,8 +523,8 @@ bugs. You should never use debug mode in production environments. This does the following: -* Exceptions will print a stacktrace -* Error pages will contain that stacktrace +* Exceptions will print a stacktrace. +* Error pages will contain that stacktrace. * Templates will not be cached. @@ -542,12 +542,12 @@ the newest version of your code. from bottle import run run(reloader=True) -How it works: The main process will not start a server, but spawn a new -child process using the same command line agruments used to start the -main process. All module level code is executed at least twice! Be -carefull. +How it works: the main process will not start a server, but spawn a new +child process using the same command line arguments used to start the +main process. All module-level code is executed at least twice! Be +careful. -The child process will have ``os.environ['BOTTLE_CHILD']`` set to ``true`` +The child process will have ``os.environ['BOTTLE_CHILD']`` set to ``True`` and start as a normal non-reloading app server. As soon as any of the loaded modules changes, the child process is terminated and respawned by the main process. Changes in template files will not trigger a reload. @@ -565,7 +565,7 @@ finally clauses, etc., are not executed after a ``SIGTERM``. Deployment ================================================================================ -Bottle uses the build-in ``wsgiref.SimpleServer`` by default. This non-threading +Bottle uses the built-in ``wsgiref.SimpleServer`` by default. This non-threading HTTP server is perfectly fine for development and early production, but may become a performance bottleneck when server load increases. @@ -610,12 +610,12 @@ there are more CPU cores available. The trick is to balance the load between multiple independent Python processes to utilise all of your CPU cores. -Instead of a single Bottle application server, you start one instances +Instead of a single Bottle application server, you start one instance of your server for each CPU core available using different local port (localhost:8080, 8081, 8082, ...). Then a high performance load balancer acts as a reverse proxy and forwards each new requests to a random Bottle processes, spreading the load between all available -backed server instances. This way you can use all of your CPU cores and +back end server instances. This way you can use all of your CPU cores and even spread out the load between different physical servers. But there are a few drawbacks: @@ -623,7 +623,7 @@ But there are a few drawbacks: * You can't easily share data between multiple Python processes. * It takes a lot of memory to run several copies of Python and Bottle at the same time. -One of the fastest load balancer available is Pound_ but most common web servers have a proxy-module that can do the work just fine. +One of the fastest load balancers available is Pound_ but most common web servers have a proxy-module that can do the work just fine. I'll add examples for lighttpd_ and Apache_ web servers soon. @@ -643,7 +643,7 @@ A call to `bottle.default_app()` returns your WSGI application. After applying a .. rubric: How default_app() works -Bottle creates a single instance of `bottle.Bottle()` and uses it as a default for most of the modul-level decorators and the `bottle.run()` routine. +Bottle creates a single instance of `bottle.Bottle()` and uses it as a default for most of the module-level decorators and the `bottle.run()` routine. `bottle.default_app()` returns (or changes) this default. You may, however, create your own instances of `bottle.Bottle()`. :: @@ -664,7 +664,7 @@ mod_wsgi_ and Bottle's WSGI interface. All you need is an ``app.wsgi`` file that provides an ``application`` object. This object is used by mod_wsgi to start your -application and should be a WSGI conform Python callable. +application and should be a WSGI-compatible Python callable. File ``/var/www/yourapp/app.wsgi``:: @@ -747,12 +747,12 @@ Glossary handler function A function to handle some specific event or situation. In a web framework, the application is developed by attaching a handler function - as callback for each specific URL composing the application. + as callback for each specific URL comprising the application. secure cookie - bottle creates signed cookies with objects that can be pickled. A secure + Bottle creates signed cookies with objects that can be pickled. A secure cookie will be created automatically when a type that is not a string is - use as value in :meth:`request.set_cookie` and bottle's config + used as the value in :meth:`request.set_cookie` and bottle's config includes a `securecookie.key` entry with a salt. source directory diff --git a/apidoc/tutorial_app.rst b/apidoc/tutorial_app.rst index cb32421..4678534 100644 --- a/apidoc/tutorial_app.rst +++ b/apidoc/tutorial_app.rst @@ -21,12 +21,12 @@ Tutorial: Todo-List Application .. note:: - This tutorial is a work in progesss and written by `noisefloor <http://github.com/noisefloor>`_. + This tutorial is a work in progess and written by `noisefloor <http://github.com/noisefloor>`_. -This tutorial should give a brief introduction into the Bottle_ WSGI Framework. The main goal is to be able, after reading through this tutorial, to create a project using Bottle. Within this document, not all abilities will be shown, but at least the main and important ones like routing, utilizing the Bottle template abilities to format output and handling GET / POST parameters. +This tutorial should give a brief introduction to the Bottle_ WSGI Framework. The main goal is to be able, after reading through this tutorial, to create a project using Bottle. Within this document, not all abilities will be shown, but at least the main and important ones like routing, utilizing the Bottle template abilities to format output and handling GET / POST parameters. -To understand the content here, it is not necessary to have a basic knowledge of WSGI, as Bottle tries to keep WSGI away from the user anyway. You should have a fair understanding of the Python_ programming language. Furthermore, the example used in the tutorial retrieves and stores data in a SQL databse, so a basic idea about SQL helps, but is not a must to understand the concepts of Bottle. Right here, SQLite_ is used. The output of Bottle send to the browser is formated in some examples by the help of HTML. Thus, a basic idea about the common HTML tags does help as well. +To understand the content here, it is not necessary to have a basic knowledge of WSGI, as Bottle tries to keep WSGI away from the user anyway. You should have a fair understanding of the Python_ programming language. Furthermore, the example used in the tutorial retrieves and stores data in a SQL databse, so a basic idea about SQL helps, but is not a must to understand the concepts of Bottle. Right here, SQLite_ is used. The output of Bottle sent to the browser is formatted in some examples by the help of HTML. Thus, a basic idea about the common HTML tags does help as well. For the sake of introducing Bottle, the Python code "in between" is kept short, in order to keep the focus. Also all code within the tutorial is working fine, but you may not necessarily use it "in the wild", e.g. on a public web server. In order to do so, you may add e.g. more error handling, protect the database with a password, test and escape the input etc. @@ -37,9 +37,9 @@ Goals At the end of this tutorial, we will have a simple, web-based ToDo list. The list contains a text (with max 100 characters) and a status (0 for closed, 1 for open) for each item. Through the web-based user interface, open items can be view and edited and new items can be added. -During development, all pages will be available on ``localhost`` only, but later on it will be show how to adapt the application for a "real" server, including how to use with Apache's mod_wsgi. +During development, all pages will be available on ``localhost`` only, but later on it will be shown how to adapt the application for a "real" server, including how to use with Apache's mod_wsgi. -Bottle will do the routing and format the output, by the help of templates. The items of the list will be stored inside a SQLite database. Reading and writing from / the database will be done by Python code. +Bottle will do the routing and format the output, with the help of templates. The items of the list will be stored inside a SQLite database. Reading and writing the database will be done by Python code. We will end up with an application with the following pages and functionality: @@ -62,11 +62,11 @@ You can either manually install Bottle or use Python's easy_install: ``easy_inst .. rubric:: Further Software Necessities -As we use SQLite3 as a database, make sure it is installed. On Linux systems, most distributions have SQLite3 installed by default. SQLite is available for [Windows and MacOS X][sqlite_win] as well and the `sqlite3` module is part of the python standard library. +As we use SQLite3 as a database, make sure it is installed. On Linux systems, most distributions have SQLite3 installed by default. SQLite is available for Windows and MacOS X as well and the `sqlite3` module is part of the python standard library. .. rubric:: Create An SQL Database -First, we need to create the database we use later on. To do so, save the following script in your project directory and run it with python. You can use the interactive interpreter, too:: +First, we need to create the database we use later on. To do so, save the following script in your project directory and run it with python. You can use the interactive interpreter too:: import sqlite3 con = sqlite3.connect('todo.db') # Warning: This file is created in the current directory @@ -76,7 +76,7 @@ First, we need to create the database we use later on. To do so, save the follow con.execute("INSERT INTO todo (task,status) VALUES ('Test various editors for and check the syntax highlighting',1)") con.execute("INSERT INTO todo (task,status) VALUES ('Choose your favorite WSGI-Framework',0)") -This generates a database-file `todo.db` with a tables called ``todo`` and three columns ``id``, ``task``, and ``status``. ``id`` is a unique id for each row, which is used later on to reference the rows. The column ``task`` holds the text which describes the task, it can be max 100 characters long. Finally, the column ``status`` is used to mark a task as open (value 1) or closed (value 0). +This generates a database-file `todo.db` with tables called ``todo`` and three columns ``id``, ``task``, and ``status``. ``id`` is a unique id for each row, which is used later on to reference the rows. The column ``task`` holds the text which describes the task, it can be max 100 characters long. Finally, the column ``status`` is used to mark a task as open (value 1) or closed (value 0). Using Bottle for a Web-Based ToDo List ================================================ @@ -86,7 +86,7 @@ Now it is time to introduce Bottle in order to create a web-based application. B .. rubric:: Understanding routes -Basically, each page visible in the browser is dynamically generate when the page address is called. Thus, there is no static content. That is exactly what is called a "route" within Bottle: a certain address on the server. So, for example, when the page ``http://localhost:8080/todo`` is called from the browser, Bottle "grabs" the call and checks if there is any (Python) function defined for the route "todo". If so, Bottle will execute the corresponding Python code and return its result. +Basically, each page visible in the browser is dynamically generated when the page address is called. Thus, there is no static content. That is exactly what is called a "route" within Bottle: a certain address on the server. So, for example, when the page ``http://localhost:8080/todo`` is called from the browser, Bottle "grabs" the call and checks if there is any (Python) function defined for the route "todo". If so, Bottle will execute the corresponding Python code and return its result. .. rubric:: First Step - Showing All Open Items @@ -106,7 +106,7 @@ So, after understanding the concept of routes, let's create the first one. The g run() -Save the code a ``todo.py``, preferable in the same directory as the file ``todo.db``. Otherwise, you need to add the path to ``todo.db`` in the ``sqlite3.connect()`` statement. +Save the code a ``todo.py``, preferably in the same directory as the file ``todo.db``. Otherwise, you need to add the path to ``todo.db`` in the ``sqlite3.connect()`` statement. Let's have a look what we just did: We imported the necessary module ``sqlite3`` to access to SQLite database and from Bottle we imported ``route`` and ``run``. The ``run()`` statement simply starts the web server included in Bottle. By default, the web server serves the pages on localhost and port 8080. Furthermore, we imported ``route``, which is the function responsible for Bottle's routing. As you can see, we defined one function, ``todo_list()``, with a few lines of code reading from the database. The important point is the `decorator statement`_ ``@route('/todo')`` right before the ``def todo_list()`` statement. By doing this, we bind this function to the route ``/todo``, so every time the browsers calls ``http://localhost:8080/todo``, Bottle returns the result of the function ``todo_list()``. That is how routing within bottle works. @@ -121,20 +121,20 @@ will work fine, too. What will not work is to bind one route to more than one fu What you will see in the browser is what is returned, thus the value given by the ``return`` statement. In this example, we need to convert ``result`` in to a string by ``str()``, as Bottle expects a string or a list of strings from the return statement. But here, the result of the database query is a list of tuples, which is the standard defined by the `Python DB API`_. -Now, after understanding the little script above, it is time to execute it and watch the result yourself. Remember that on Linux- / Unix-based systems the file ``todo.py`` need to be executable first. Then, just run ``python todo.py`` and call the page ``http://localhost:8080/todo`` in your browser. In case you made no mistake writing the script, the output should look like this:: +Now, after understanding the little script above, it is time to execute it and watch the result yourself. Remember that on Linux- / Unix-based systems the file ``todo.py`` needs to be executable first. Then, just run ``python todo.py`` and call the page ``http://localhost:8080/todo`` in your browser. In case you made no mistake writing the script, the output should look like this:: [(2, u'Visit the Python website'), (3, u'Test various editors for and check the syntax highlighting')] If so - congratulations! You are now a successful user of Bottle. In case it did not work and you need to make some changes to the script, remember to stop Bottle serving the page, otherwise the revised version will not be loaded. -Actually, the output is not really exciting nor nice to read. It is the raw result returned from the SQL-Query. +Actually, the output is not really exciting nor nice to read. It is the raw result returned from the SQL query. So, in the next step we format the output in a nicer way. But before we do that, we make our life easier. .. rubric:: Debugging and Auto-Reload -Maybe you already experienced the Bottle sends a short error message to the browser in case something within the script is wrong, e.g. the connection to the database is not working. For debugging purposes it is quiet helpful to get more details. This can be easily achieved by adding the following statement to the script:: +Maybe you already noticed that Bottle sends a short error message to the browser in case something within the script is wrong, e.g. the connection to the database is not working. For debugging purposes it is quiet helpful to get more details. This can be easily achieved by adding the following statement to the script:: from bottle import run, route, debug ... @@ -142,11 +142,11 @@ Maybe you already experienced the Bottle sends a short error message to the brow debug(True) run() -By enabling "debug", you will get a full stacktrace of the Python interpreter, which usually contains useful information for finding bugs. Furthermore, templates (see below) are not cached, thus changes to template will take effect without stopping the server. +By enabling "debug", you will get a full stacktrace of the Python interpreter, which usually contains useful information for finding bugs. Furthermore, templates (see below) are not cached, thus changes to templates will take effect without stopping the server. .. warning:: - That ``debug(True)`` is supposed to be used for development only, it should *not* be used in productive environments. + That ``debug(True)`` is supposed to be used for development only, it should *not* be used in production environments. @@ -163,17 +163,17 @@ Again, the feature is mainly supposed to be used while development, not on produ .. rubric:: Bottle Template To Format The Output -Now let's have a look to cast the output of the script into a proper format. +Now let's have a look at casting the output of the script into a proper format. -Actually Bottle expects to receive a string or a list of strings from a function and returns them by the help of the build-in server to the browser. Bottle does not bother about the content of the string itself, so it can be text formated with HTML markup, too. +Actually Bottle expects to receive a string or a list of strings from a function and returns them by the help of the built-in server to the browser. Bottle does not bother about the content of the string itself, so it can be text formatted with HTML markup, too. -Bottle brings its own easy-to-use template engine with it. Templates are stored as separate files having a ``.tpl`` extension. The template can be called then from within a function. Templates can contain any type of text (which will be most likely HTML-markup mixed with Python statements). Furthermore, templates can take arguments, e.g. the result set of a database query, which will be then formated nicely within the template. +Bottle brings its own easy-to-use template engine with it. Templates are stored as separate files having a ``.tpl`` extension. The template can be called then from within a function. Templates can contain any type of text (which will be most likely HTML-markup mixed with Python statements). Furthermore, templates can take arguments, e.g. the result set of a database query, which will be then formatted nicely within the template. Right here, we are going to cast the result of our query showing the open ToDo items into a simple table with two columns: the first column will contain the ID of the item, the second column the text. The result set is, as seen above, a list of tuples, each tuple contains one set of results. -To include the template into our example, just add the following lines:: +To include the template in our example, just add the following lines:: - from bottle import from bottle import route, run, debug, template + from bottle import route, run, debug, template ... result = c.fetchall() c.close() @@ -181,7 +181,7 @@ To include the template into our example, just add the following lines:: return output ... -So we do here two things: First, we import ``template`` from Bottle in order to be able to use templates. Second, we assign the output of the template ``make_table`` to the variable ``output``, which is then returned. In addition to calling the template, we assign ``result``, which we received from the database query, to the variable ``rows``, which is later on used within the template. If necessary, you can assign more than one variable / value to a template. +So we do here two things: first, we import ``template`` from Bottle in order to be able to use templates. Second, we assign the output of the template ``make_table`` to the variable ``output``, which is then returned. In addition to calling the template, we assign ``result``, which we received from the database query, to the variable ``rows``, which is later on used within the template. If necessary, you can assign more than one variable / value to a template. Templates always return a list of strings, thus there is no need to convert anything. Of course, we can save one line of code by writing ``return template('make_table', rows=result)``, which gives exactly the same result as above. @@ -201,20 +201,20 @@ Now it is time to write the corresponding template, which looks like this:: Save the code as ``make_table.tpl`` in the same directory where ``todo.py`` is stored. -Let's have a look at the code: Every line starting with % is interpreted as Python code. Please note that, of course, only valid Python statements are allowed, otherwise the template will raise an exception, just as any other Python code. The other lines are plain HTML-markup. +Let's have a look at the code: every line starting with % is interpreted as Python code. Please note that, of course, only valid Python statements are allowed, otherwise the template will raise an exception, just as any other Python code. The other lines are plain HTML markup. -As you can see, we use Python's ``for``-statement two times, in order to go through ``rows``. As seen above, ``rows`` is a variable which holds the result of the database query, so it is a list of tuples. The first ``for``-statement accesses the tuples within the list, the second one the items within the tuple, which are put each into a cell of the table. Important is the fact that you need additionally close all ``for``, ``if``, ``while`` etc. statements with ``%end``, otherwise the output may not be what you expect. +As you can see, we use Python's ``for`` statement two times, in order to go through ``rows``. As seen above, ``rows`` is a variable which holds the result of the database query, so it is a list of tuples. The first ``for`` statement accesses the tuples within the list, the second one the items within the tuple, which are put each into a cell of the table. It is important that you close all ``for``, ``if``, ``while`` etc. statements with ``%end``, otherwise the output may not be what you expect. If you need to access a variable within a non-Python code line inside the template, you need to put it into double curly braces. This tells the template to insert the actual value of the variable right in place. -Run the script again and look at the output. Still not really nice, but at least better readable than the list of tuples. Of course, you can spice-up the very simple HTML-markup above, e.g. by using in-line styles to get a better looking output. +Run the script again and look at the output. Still not really nice, but at least more readable than the list of tuples. Of course, you can spice-up the very simple HTML markup above, e.g. by using in-line styles to get a better looking output. .. rubric:: Using GET and POST Values -As we can review all open items properly, we move to the next step, which is adding new items to the ToDo list. The new item should be received from a regular HTML-based form, which sends its data by the GET-method. +As we can review all open items properly, we move to the next step, which is adding new items to the ToDo list. The new item should be received from a regular HTML-based form, which sends its data by the GET method. -To do so, we first add a new route to our script and tell the route that it should get GET-data:: +To do so, we first add a new route to our script and tell the route that it should get GET data:: from bottle import route, run, debug, template, request ... @@ -237,13 +237,13 @@ To do so, we first add a new route to our script and tell the route that it shou return '<p>The new task was inserted into the database, the ID is %s</p>' % new_id -To access GET (or POST) data, we need to import ``request`` from Bottle. To assign the actual data to a variable, we use the statement ``request.GET.get('task','').strip()`` statement, where ``task`` is the name of the GET-data we want to access. That's all. If your GET-data has more than one variable, multiple ``request.GET.get()`` statements can be used and assigned to other variables. +To access GET (or POST) data, we need to import ``request`` from Bottle. To assign the actual data to a variable, we use the statement ``request.GET.get('task','').strip()`` statement, where ``task`` is the name of the GET data we want to access. That's all. If your GET data has more than one variable, multiple ``request.GET.get()`` statements can be used and assigned to other variables. The rest of this piece of code is just processing of the gained data: writing to the database, retrieve the corresponding id from the database and generate the output. -But where do we get the GET-data from? Well, we can use a static HTML page holding the form. Or, what we do right now, is to use a template which is output when the route ``/new`` is called without GET-data. +But where do we get the GET data from? Well, we can use a static HTML page holding the form. Or, what we do right now, is to use a template which is output when the route ``/new`` is called without GET data. -The code need to be extended to:: +The code needs to be extended to:: ... @route('/new', method='GET') @@ -278,14 +278,14 @@ That's all. As you can see, the template is plain HTML this time. Now we are able to extend our to do list. -By the way, if you prefer to use POST-data: This works exactly the same way, just use ``request.POST.get()`` instead. +By the way, if you prefer to use POST data: this works exactly the same way, just use ``request.POST.get()`` instead. .. rubric:: Editing Existing Items The last point to do is to enable editing of existing items. -By using the routes we know so far only it is possible, but may be quiet tricky. But Bottle knows something called "dynamic routes", which makes this task quiet easy. +By using only the routes we know so far it is possible, but may be quite tricky. But Bottle knows something called "dynamic routes", which makes this task quiet easy. The basic statement for a dynamic route looks like this:: @@ -323,7 +323,7 @@ The code looks like this:: return template('edit_task', old=cur_data, no=no) -It is basically pretty much the same what we already did above when adding new items, like using ``GET``-data etc. The main addition here is using the dynamic route ``:no``, which here passes the number to the corresponding function. As you can see, ``no`` is used within the function to access the right row of data within the database. +It is basically pretty much the same what we already did above when adding new items, like using ``GET`` data etc. The main addition here is using the dynamic route ``:no``, which here passes the number to the corresponding function. As you can see, ``no`` is used within the function to access the right row of data within the database. The template ``edit_task.tpl`` called within the function looks like this:: @@ -347,7 +347,7 @@ A last word on dynamic routes: you can even use a regular expression for a dynam .. rubric:: Validating Dynamic Routes -Using dynamic routes is fine, but for many cases it makes sense to validate the dynamic part of the route. For example, we expect a integer number in our route for editing above. But if a float, characters or so are received, the Python interpreter throws an exception, which is not what we want. +Using dynamic routes is fine, but for many cases it makes sense to validate the dynamic part of the route. For example, we expect an integer number in our route for editing above. But if a float, characters or so are received, the Python interpreter throws an exception, which is not what we want. For those cases, Bottle offers the ``@valdiate`` decorator, which validates the "input" prior to passing it to the function. In order to apply the validator, extend the code as follows:: @@ -360,7 +360,7 @@ For those cases, Bottle offers the ``@valdiate`` decorator, which validates the At first, we imported ``validate`` from the Bottle framework, than we apply the @validate-decorator. Right here, we validate if ``no`` is an integer. Basically, the validation works with all types of data like floats, lists etc. -Save the code and call the page again using a "403 forbidden" value for ``:no``, e.g. a float. You will receive not an exception, but a "403 - Forbidden" error, saying that a integer was expected. +Save the code and call the page again using a "403 forbidden" value for ``:no``, e.g. a float. You will receive not an exception, but a "403 - Forbidden" error, saying that an integer was expected. .. rubric:: Dynamic Routes Using Regular Expressions @@ -382,7 +382,7 @@ As said above, the solution is a regular expression:: else: return 'Task: %s' %result[0] -Of course, this example is somehow artificially constructed - it would be easier to use a plain dynamic route only combined with a validation. Nevertheless, we want to see how regular expression routes work: The line ``@route(/item:item_#[1-9]+#)`` starts like a normal route, but the part surrounded by # is interpreted as a regular expression, which is the dynamic part of the route. So in this case, we want to match any digit between 0 and 9. The following function "show_item" just checks whether the given item is present in the database or not. In case it is present, the corresponding text of the task is returned. As you can see, only the regular expression part of the route is passed forward. Furthermore, it is always forwarded as a string, even if it is a plain integer number, like in this case. +Of course, this example is somehow artificially constructed - it would be easier to use a plain dynamic route only combined with a validation. Nevertheless, we want to see how regular expression routes work: the line ``@route(/item:item_#[1-9]+#)`` starts like a normal route, but the part surrounded by # is interpreted as a regular expression, which is the dynamic part of the route. So in this case, we want to match any digit between 0 and 9. The following function "show_item" just checks whether the given item is present in the database or not. In case it is present, the corresponding text of the task is returned. As you can see, only the regular expression part of the route is passed forward. Furthermore, it is always forwarded as a string, even if it is a plain integer number, like in this case. .. rubric:: Returning Static Files @@ -395,12 +395,12 @@ Sometimes it may become necessary to associate a route not to a Python function, def help(): send_file('help.html', root='/path/to/file') -At first, we need to import ``send_file`` from Bottle. As you can see, the ``send_file`` statement replace the ``return`` statement. It takes at least two arguments: The name of the file to be returned and the path to the file. Even if the file is in the same directory as your application, the path needs to be stated. But in this case, you can use ``'.'`` as a path, too. Bottle guesses the MIME-type of the file automatically, but in case you like to state it explicitly, add a third argument to ``send_file``, which would be here ``mimetype='text/html'``. ``send_file`` works with any type of route, including the dynamic ones. +At first, we need to import ``send_file`` from Bottle. As you can see, the ``send_file`` statement replaces the ``return`` statement. It takes at least two arguments: the name of the file to be returned and the path to the file. Even if the file is in the same directory as your application, the path needs to be stated. But in this case, you can use ``'.'`` as a path, too. Bottle guesses the MIME-type of the file automatically, but in case you like to state it explicitly, add a third argument to ``send_file``, which would be here ``mimetype='text/html'``. ``send_file`` works with any type of route, including the dynamic ones. .. rubric:: Returning JSON Data -There may be cases where you do not want your application to generate the output directly, but return data to be processed further on, e.g. by JavaScript. For those cases, Bottle offers to possibility to return JSON objects, which is sort of standard for exchanging data between web applications. Furthermore, JSON can be processed by many programming languages, including Python +There may be cases where you do not want your application to generate the output directly, but return data to be processed further on, e.g. by JavaScript. For those cases, Bottle offers the possibility to return JSON objects, which is sort of standard for exchanging data between web applications. Furthermore, JSON can be processed by many programming languages, including Python So, let's assume we want to return the data generated in the regular expression route example as a JSON object. The code looks like this:: @@ -417,7 +417,7 @@ So, let's assume we want to return the data generated in the regular expression else: return {'Task': result[0]} -As you can, that is fairly simple: Just return a regular Python dictionary and Bottle will convert it automatically into a JSON object prior to sending. So if you e.g. call "http://localhost/json1" Bottle should in this case return the JSON object ``{"Task": ["Read A-byte-of-python to get a good introduction into Python"]}``. +As you can, that is fairly simple: just return a regular Python dictionary and Bottle will convert it automatically into a JSON object prior to sending. So if you e.g. call "http://localhost/json1" Bottle should in this case return the JSON object ``{"Task": ["Read A-byte-of-python to get a good introduction into Python"]}``. @@ -433,7 +433,7 @@ In our case, we want to catch a 403 error. The code is as follows:: def mistake(code): return 'The parameter you passed has the wrong format!' -So, at first we need to import ``error`` from Bottle and define a route by ``error(403)``, which catches all "403 forbidden" errors. The function "mistake" is assigned to that. Please note that ``error()`` always passed the error-code to the function - even if you do not need it. Thus, the function always needs to accept one argument, otherwise it will not work. +So, at first we need to import ``error`` from Bottle and define a route by ``error(403)``, which catches all "403 forbidden" errors. The function "mistake" is assigned to that. Please note that ``error()`` always passes the error-code to the function - even if you do not need it. Thus, the function always needs to accept one argument, otherwise it will not work. Again, you can assign more than one error-route to a function, or catch various errors with one function each. So this code:: @@ -455,9 +455,9 @@ works fine, the following one as well:: .. rubric:: Summary -After going through all the sections above, you should have a brief understanding how the Bottle WSGI framework works. Furthermore you have all the knowledge necessary to use Bottle for you applications. +After going through all the sections above, you should have a brief understanding how the Bottle WSGI framework works. Furthermore you have all the knowledge necessary to use Bottle for your applications. -The following chapter give a short introduction how to adapt Bottle for larger projects. Furthermore, we will show how to operate Bottle with web servers which performs better on a higher load / more web traffic than the one we used so far. +The following chapter give a short introduction how to adapt Bottle for larger projects. Furthermore, we will show how to operate Bottle with web servers which perform better on a higher load / more web traffic than the one we used so far. Server Setup ================================ @@ -467,7 +467,7 @@ So far, we used the standard server used by Bottle, which is the `WSGI reference .. rubric:: Running Bottle on a different port and IP -As a standard, Bottle does serve the pages on the IP-adress 127.0.0.1, also known as ``localhost``, and on port ``8080``. To modify there setting is pretty simple, as additional parameters can be passed to Bottle's ``run()`` function to change the port and the address. +As standard, Bottle servse the pages on the IP adress 127.0.0.1, also known as ``localhost``, and on port ``8080``. To modify the setting is pretty simple, as additional parameters can be passed to Bottle's ``run()`` function to change the port and the address. To change the port, just add ``port=portnumber`` to the run command. So, for example:: @@ -475,7 +475,7 @@ To change the port, just add ``port=portnumber`` to the run command. So, for exa would make Bottle listen to port 80. -To change the IP-address where Bottle is listing / serving can be change by:: +To change the IP address where Bottle is listening:: run(host='123.45.67.89') @@ -483,14 +483,14 @@ Of course, both parameters can be combined, like:: run(port=80, host='123.45.67.89') -The ``port`` and ``host`` parameter can also be applied when Bottle is running with a different server, as shown in the following section +The ``port`` and ``host`` parameter can also be applied when Bottle is running with a different server, as shown in the following section. .. rubric:: Running Bottle with a different server -As said above, the standard server is perfectly suitable for development, personal use or a small group of people only using your application based on Bottle. For larger task, the standard server may become a Bottle neck, as it is single-threaded, thus it can only serve on request at a time. +As said above, the standard server is perfectly suitable for development, personal use or a small group of people only using your application based on Bottle. For larger tasks, the standard server may become a bottleneck, as it is single-threaded, thus it can only serve one request at a time. -But Bottle has already various adapters to multi-threaded server on board, which perform better on higher load. Bottle supports Cherrypy_, Fapws3_, Flup_ and Paste_. +But Bottle has already various adapters to multi-threaded servers on board, which perform better on higher load. Bottle supports Cherrypy_, Fapws3_, Flup_ and Paste_. If you want to run for example Bottle with the past server, use the following code:: @@ -503,15 +503,15 @@ This works exactly the same way with ``FlupServer``, ``CherryPyServer`` and ``Fa .. rubric:: Running Bottle on Apache with mod_wsgi -Maybe you already have an Apache_ or you want to run a Bottle-based application large scale - than it is time to think about Apache with mod_wsgi_. +Maybe you already have an Apache_ or you want to run a Bottle-based application large scale - then it is time to think about Apache with mod_wsgi_. We assume that your Apache server is up and running and mod_wsgi is working fine as well. On a lot of Linux distributions, mod_wsgi can be installed via the package management easily. -Bottle brings a adapter for mod_wsgi with it, so serving your application is an easy task. +Bottle brings an adapter for mod_wsgi with it, so serving your application is an easy task. -In the following example, we assume that you want to make your application "ToDO list" accessible through ``http://www.mypage.com/todo`` and your code, templates and SQLite database is stored in the path ``var/www/todo``. +In the following example, we assume that you want to make your application "ToDO list" accessible through ``http://www.mypage.com/todo`` and your code, templates and SQLite database are stored in the path ``/var/www/todo``. -When you run your application via mod_wsgi, it is imperative to remove the ``run()`` statement from you code, otherwise it won't work here. +When you run your application via mod_wsgi, it is imperative to remove the ``run()`` statement from your code, otherwise it won't work here. After that, create a file called ``adapter.wsgi`` with the following content:: @@ -524,7 +524,7 @@ After that, create a file called ``adapter.wsgi`` with the following content:: application = bottle.default_app() -and save it in the same path, ``/var/www/todo``. Actually the name of the file can be anything, as long as the extensions is ``.wsgi``. The name is only used to reference the file from your virtual host. +and save it in the same path, ``/var/www/todo``. Actually the name of the file can be anything, as long as the extension is ``.wsgi``. The name is only used to reference the file from your virtual host. Finally, we need to add a virtual host to the Apache configuration, which looks like this:: @@ -542,14 +542,14 @@ Finally, we need to add a virtual host to the Apache configuration, which looks </Directory> </VirtualHost> -After restarting the server, your the ToDo list should be accessible at ``http://www.mypage.com/todo`` +After restarting the server, your ToDo list should be accessible at ``http://www.mypage.com/todo`` Final Words ========================= -Now we are at the end of this introduction and tutorial to Bottle. We learned about the basic concepts of Bottle and wrote a first application using the Bottle framework. In addition to that, we saw how to adapt Bottle for large task and server Bottle through a Apache web server with mod_wsgi. +Now we are at the end of this introduction and tutorial to Bottle. We learned about the basic concepts of Bottle and wrote a first application using the Bottle framework. In addition to that, we saw how to adapt Bottle for large task and servr Bottle through an Apache web server with mod_wsgi. -As said in the introduction, this tutorial is not showing all shades and possibilities of Bottle. What we skipped here is e.g. receiving File Objects and Streams and how to handle authentication data. Furthermore, we did not show how templates can be called from within another template. For an introduction into those points, please refer to the full `Bottle documentation`_ . +As said in the introduction, this tutorial is not showing all shades and possibilities of Bottle. What we skipped here is e.g. receiving file objects and streams and how to handle authentication data. Furthermore, we did not show how templates can be called from within another template. For an introduction into those points, please refer to the full `Bottle documentation`_ . Complete Example Listing ========================= @@ -81,6 +81,7 @@ import thread import threading import time import tokenize +import tempfile from Cookie import SimpleCookie from tempfile import TemporaryFile @@ -121,18 +122,40 @@ if sys.version_info >= (3,0,0): # pragma: no cover wrapped buffer. This subclass keeps it open. ''' def close(self): pass StringType = bytes - def touni(x, enc='utf8'): # Convert anything to unicode (py3) + def touni(x, enc='utf8'): + """ Convert anything to unicode """ return str(x, encoding=enc) if isinstance(x, bytes) else str(x) else: from StringIO import StringIO as BytesIO from types import StringType NCTextIOWrapper = None - def touni(x, enc='utf8'): # Convert anything to unicode (py2) + def touni(x, enc='utf8'): + """ Convert anything to unicode """ return x if isinstance(x, unicode) else unicode(str(x), encoding=enc) -def tob(data, enc='utf8'): # Convert strings to bytes (py2 and py3) - return data.encode(enc) if isinstance(data, unicode) else data +def tob(data, enc='utf8'): + """ Convert anything to bytes """ + return data.encode(enc) if isinstance(data, unicode) else StringType(data) +# Convert strings and unicode to native strings +if sys.version_info >= (3,0,0): + tonat = touni +else: + tonat = tob +tonat.__doc__ = """ Convert anything to native strings """ + + +# Background compatibility +import warnings +def depr(message, critical=False): + if critical: raise DeprecationWarning(message) + warnings.warn(message, DeprecationWarning, stacklevel=3) + +# Small helpers +def makelist(data): + if isinstance(data, (tuple, list, set, dict)): return list(data) + elif data: return [data] + else: return [] @@ -146,7 +169,7 @@ class BottleException(Exception): class HTTPResponse(BottleException): - """ Used to break execution and imediately finish the response """ + """ Used to break execution and immediately finish the response """ def __init__(self, output='', status=200, header=None): super(BottleException, self).__init__("HTTP Response %d" % status) self.status = int(status) @@ -204,14 +227,14 @@ class Route(object): self.route = route self.target = target self.name = name - if static: - self.route = self.route.replace(':','\\:') + self.flat = static self._tokens = None def tokens(self): """ Return a list of (type, value) tokens. """ if not self._tokens: - self._tokens = list(self.tokenise(self.route)) + r = self.route.replace(':','\\:') if self.flat else self.route + self._tokens = list(self.tokenise(r)) return self._tokens @classmethod @@ -267,7 +290,7 @@ class Route(object): return "<Route(%s) />" % repr(self.route) def __eq__(self, other): - return self.route == other.route + return (self.route, self.flat) == (other.route, other.flat) class Router(object): ''' A route associates a string (e.g. URL) with an object (e.g. function) @@ -281,6 +304,7 @@ class Router(object): self.named = {} # Cache for named routes and their format strings self.static = {} # Cache for static routes self.dynamic = [] # Search structure for dynamic routes + self.compiled = False def add(self, route, target=None, **ka): """ Add a route->target pair or a :class:`Route` object to the Router. @@ -291,6 +315,7 @@ class Router(object): if self.get_route(route): return RouteError('Route %s is not uniqe.' % route) self.routes.append(route) + self.compiled = False return route def get_route(self, route, target=None, **ka): @@ -315,13 +340,19 @@ class Router(object): target, args_re = subroutes[match.lastindex - 1] args = args_re.match(uri).groupdict() if args_re else {} return target, args + if not self.compiled: # Late check to reduce overhead on hits + self.compile() # Compile and try again. + return self.match(uri) return None, {} def build(self, _name, **args): - ''' Build an URI out of a named route and values for te wildcards. ''' + ''' Build an URI out of a named route and values for the wildcards. ''' try: return self.named[_name] % args except KeyError: + if not self.compiled: # Late check to reduce overhead on hits + self.compile() # Compile and try again. + return self.build(_name, **args) raise RouteBuildError("No route found with name '%s'." % _name) def compile(self): @@ -344,9 +375,11 @@ class Router(object): self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1]) self.dynamic[-1][1].append((route.target, gregexp)) except (AssertionError, IndexError), e: # AssertionError: Too many groups - self.dynamic.append((re.compile('(^%s$)'%fpatt),[(route.target, gregexp)])) + self.dynamic.append((re.compile('(^%s$)'%fpatt), + [(route.target, gregexp)])) except re.error, e: raise RouteSyntaxError("Could not add Route: %s (%s)" % (route, e)) + self.compiled = True def __eq__(self, other): return self.routes == other.routes @@ -373,6 +406,10 @@ class Bottle(object): self.castfilter = [] if autojson and json_dumps: self.add_filter(dict, dict2json) + self.hooks = {'before_request': [], 'after_request': []} + + def optimize(self, *a, **ka): + depr("Bottle.optimize() is obsolete.") def mount(self, app, script_path): ''' Mount a Bottle application to a specific URL prefix ''' @@ -393,7 +430,7 @@ class Bottle(object): def add_filter(self, ftype, func): ''' Register a new output filter. Whenever bottle hits a handler output - matching `ftype`, `func` is applyed to it. ''' + matching `ftype`, `func` is applied to it. ''' if not isinstance(ftype, type): raise TypeError("Expected type object, got %s" % type(ftype)) self.castfilter = [(t, f) for (t, f) in self.castfilter if t != ftype] @@ -423,31 +460,65 @@ class Bottle(object): def get_url(self, routename, **kargs): """ Return a string that matches a named route """ - return '/' + self.routes.build(routename, **kargs) - - def route(self, path=None, method='GET', **kargs): - """ Decorator: Bind a function to a GET request path. - - If the path parameter is None, the signature of the decorated - function is used to generate the paths. See yieldroutes() - for details. - - The method parameter (default: GET) specifies the HTTP request - method to listen to. You can specify a list of methods, too. + scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/' + location = self.routes.build(routename, **kargs).lstrip('/') + return urljoin(urljoin('/', scriptname), location) + + def route(self, path=None, method='GET', no_hooks=False, decorate=None, + template=None, template_opts={}, callback=None, **kargs): + """ Decorator: Bind a callback function to a request path. + + :param path: The request path or a list of paths to listen to. See + :class:`Router` for syntax details. If no path is specified, it + is automatically generated from the callback signature. See + :func:`yieldroutes` for details. + :param method: The HTTP method (POST, GET, ...) or a list of + methods to listen to. (default: GET) + :param decorate: A decorator or a list of decorators. These are + applied to the callback in reverse order. + :param no_hooks: If true, application hooks are not triggered + by this route. (default: False) + :param template: The template to use for this callback. + (default: no template) + :param template_opts: A dict with additional template parameters. + :param static: If true, all paths are static even if they contain + dynamic syntax tokens. (default: False) + :param name: The name for this route. (default: None) + :param callback: If set, the route decorator is directly applied + to the callback and the callback is returned instead. This + equals ``Bottle.route(...)(callback)``. """ - def wrapper(callback): - routes = [path] if path else yieldroutes(callback) - methods = method.split(';') if isinstance(method, str) else method - for r in routes: - for m in methods: - r, m = r.strip().lstrip('/'), m.strip().upper() - old = self.routes.get_route(r, **kargs) + # @route can be used without any parameters + if callable(path): path, callback = None, path + # Build up the list of decorators + decorators = makelist(decorate) + if template: decorators.insert(0, view(template, **template_opts)) + if not no_hooks: decorators.append(self._add_hook_wrapper) + def wrapper(func): + callback = func + for decorator in reversed(decorators): + callback = decorator(callback) + functools.update_wrapper(callback, func) + for route in makelist(path) or yieldroutes(func): + for meth in makelist(method): + route = route.strip().lstrip('/') + meth = meth.strip().upper() + old = self.routes.get_route(route, **kargs) if old: - old.target[m] = callback + old.target[meth] = callback else: - self.routes.add(r, {m: callback}, **kargs) - self.routes.compile() - return callback + self.routes.add(route, {meth: callback}, **kargs) + return func + return wrapper(callback) if callback else wrapper + + def _add_hook_wrapper(self, func): + ''' Add hooks to a callable. See #84 ''' + @functools.wraps(func) + def wrapper(*a, **ka): + for hook in self.hooks['before_request']: hook() + response.output = func(*a, **ka) + for hook in self.hooks['after_request']: hook() + return response.output return wrapper def get(self, path=None, method='GET', **kargs): @@ -471,12 +542,34 @@ class Bottle(object): return self.route(path, method, **kargs) def error(self, code=500): - """ Decorator: Registrer an output handler for a HTTP error code""" + """ Decorator: Register an output handler for a HTTP error code""" def wrapper(handler): self.error_handler[int(code)] = handler return handler return wrapper + def hook(self, name): + """ Return a decorator that adds a callback to the specified hook. """ + def wrapper(func): + self.add_hook(name, func) + return func + return wrapper + + def add_hook(self, name, func): + ''' Add a callback from a hook. ''' + if name not in self.hooks: + raise ValueError("Unknown hook name %s" % name) + if name in ('after_request'): + self.hooks[name].insert(0, func) + else: + self.hooks[name].append(func) + + def remove_hook(self, name, func): + ''' Remove a callback from a hook. ''' + if name not in self.hooks: + raise ValueError("Unknown hook name %s" % name) + self.hooks[name].remove(func) + def handle(self, url, method): """ Execute the handler bound to the specified url and method and return its output. If catchall is true, exceptions are catched and returned as @@ -561,7 +654,7 @@ class Bottle(object): return self._cast(HTTPError(500, 'Unsupported response type: %s'\ % type(first)), request, response) - def __call__(self, environ, start_response): + def wsgi(self, environ, start_response): """ The bottle WSGI-interface. """ try: environ['bottle.app'] = self @@ -571,6 +664,7 @@ class Bottle(object): out = self._cast(out, request, response) # rfc2616 section 4.3 if response.status in (100, 101, 204, 304) or request.method == 'HEAD': + if hasattr(out, 'close'): out.close() out = [] status = '%d %s' % (response.status, HTTP_CODES[response.status]) start_response(status, response.headerlist) @@ -578,8 +672,7 @@ class Bottle(object): except (KeyboardInterrupt, SystemExit, MemoryError): raise except Exception, e: - if not self.catchall: - raise + if not self.catchall: raise err = '<h1>Critical error while processing request: %s</h1>' \ % environ.get('PATH_INFO', '/') if DEBUG: @@ -588,11 +681,14 @@ class Bottle(object): environ['wsgi.errors'].write(err) #TODO: wsgi.error should not get html start_response('500 INTERNAL SERVER ERROR', [('Content-Type', 'text/html')]) return [tob(err)] + + def __call__(self, environ, start_response): + return self.wsgi(environ, start_response) class BaseRequest(DictMixin): """ Represents a single HTTP request using thread-local attributes. - The Request object wrapps a WSGI environment and can be used as such. + The Request object wraps a WSGI environment and can be used as such. """ def __init__(self, environ=None): """ Create a new Request instance. @@ -613,15 +709,20 @@ class BaseRequest(DictMixin): self.path = '/' + environ.get('PATH_INFO', '/').lstrip('/') self.method = environ.get('REQUEST_METHOD', 'GET').upper() + @property + def _environ(self): + depr("Request._environ renamed to Request.environ") + return self.environ + def copy(self): ''' Returns a copy of self ''' return Request(self.environ.copy()) - + def path_shift(self, shift=1): ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. - :param shift: The number of path fragemts to shift. May be negative to - change ths shift direction. (default: 1) + :param shift: The number of path fragments to shift. May be negative to + change the shift direction. (default: 1) ''' script_name = self.environ.get('SCRIPT_NAME','/') self['SCRIPT_NAME'], self.path = path_shift(script_name, self.path, shift) @@ -712,7 +813,7 @@ class BaseRequest(DictMixin): This supports urlencoded and multipart POST requests. Multipart is commonly used for file uploads and may result in some of the - values beeing cgi.FieldStorage objects instead of strings. + values being cgi.FieldStorage objects instead of strings. Multiple values per key are possible. See MultiDict for details. """ @@ -832,6 +933,11 @@ class BaseResponse(): self.headers = HeaderDict() self.content_type = 'text/html; charset=UTF-8' + @property + def header(self): + depr("Response.header renamed to Response.headers") + return self.headers + def copy(self): ''' Returns a copy of self ''' copy = Response() @@ -877,9 +983,11 @@ class BaseResponse(): def set_cookie(self, key, value, secret=None, **kargs): """ Add a new cookie with various options. - If the cookie value is not a string, a secure cookie is created. + If the cookie value is not a string, the value is pickled and a secure + cookie is created. For this you have to provide a secret key which + is used to sign the cookie. - Possible options are: + Possible cookie options are: expires, path, comment, domain, max_age, secure, version, httponly See http://de.wikipedia.org/wiki/HTTP-Cookie#Aufbau for details """ @@ -1079,7 +1187,7 @@ def redirect(url, code=303): def send_file(*a, **k): #BC 0.6.4 - """ Raises the output of static_file() """ + """ Raises the output of static_file(). (deprecated) """ raise static_file(*a, **k) @@ -1163,14 +1271,14 @@ def cookie_encode(data, key): ''' Encode and sign a pickle-able object. Return a string ''' msg = base64.b64encode(pickle.dumps(data, -1)) sig = base64.b64encode(hmac.new(key, msg).digest()) - return u'!'.encode('ascii') + sig + u'?'.encode('ascii') + msg #2to3 hack + return tob('!') + sig + tob('?') + msg def cookie_decode(data, key): ''' Verify and decode an encoded string. Return an object or None''' - if isinstance(data, unicode): data = data.encode('ascii') #2to3 hack + data = tob(data) if cookie_is_encoded(data): - sig, msg = data.split(u'?'.encode('ascii'),1) #2to3 hack + sig, msg = data.split(tob('?'), 1) if sig[1:] == base64.b64encode(hmac.new(key, msg).digest()): return pickle.loads(base64.b64decode(msg)) return None @@ -1178,24 +1286,18 @@ def cookie_decode(data, key): def cookie_is_encoded(data): ''' Return True if the argument looks like a encoded cookie.''' - return bool(data.startswith(u'!'.encode('ascii')) and u'?'.encode('ascii') in data) #2to3 hack - - -def tonativefunc(enc='utf-8'): - ''' Returns a function that turns everything into 'native' strings using enc ''' - if sys.version_info >= (3,0,0): - return lambda x: x.decode(enc) if isinstance(x, bytes) else str(x) - return lambda x: x.encode(enc) if isinstance(x, unicode) else str(x) + return bool(data.startswith(tob('!')) and tob('?') in data) def yieldroutes(func): """ Return a generator for routes that match the signature (name, args) of the func parameter. This may yield more than one route if the function - takes optional keyword arguments. The output is best described by example: - a() -> '/a' - b(x, y) -> '/b/:x/:y' - c(x, y=5) -> '/c/:x' and '/c/:x/:y' - d(x=5, y=6) -> '/d' and '/d/:x' and '/d/:x/:y' + takes optional keyword arguments. The output is best described by example:: + + a() -> '/a' + b(x, y) -> '/b/:x/:y' + c(x, y=5) -> '/c/:x' and '/c/:x/:y' + d(x=5, y=6) -> '/d' and '/d/:x' and '/d/:x/:y' """ path = func.__name__.replace('__','/').lstrip('/') spec = inspect.getargspec(func) @@ -1212,7 +1314,7 @@ def path_shift(script_name, path_info, shift=1): :return: The modified paths. :param script_name: The SCRIPT_NAME path. :param script_name: The PATH_INFO path. - :param shift: The number of path fragemts to shift. May be negative to + :param shift: The number of path fragments to shift. May be negative to change ths shift direction. (default: 1) ''' if shift == 0: return script_name, path_info @@ -1238,7 +1340,6 @@ def path_shift(script_name, path_info, shift=1): - # Decorators #TODO: Replace default_app() with app() @@ -1261,17 +1362,22 @@ def validate(**vkargs): return decorator -route = functools.wraps(Bottle.route)(lambda *a, **ka: app().route(*a, **ka)) -get = functools.wraps(Bottle.get)(lambda *a, **ka: app().get(*a, **ka)) -post = functools.wraps(Bottle.post)(lambda *a, **ka: app().post(*a, **ka)) -put = functools.wraps(Bottle.put)(lambda *a, **ka: app().put(*a, **ka)) -delete = functools.wraps(Bottle.delete)(lambda *a, **ka: app().delete(*a, **ka)) -error = functools.wraps(Bottle.error)(lambda *a, **ka: app().error(*a, **ka)) -url = functools.wraps(Bottle.get_url)(lambda *a, **ka: app().get_url(*a, **ka)) -mount = functools.wraps(Bottle.get_url)(lambda *a, **ka: app().mount(*a, **ka)) +def make_default_app_wrapper(name): + ''' Return a callable that relays calls to the current default app. ''' + @functools.wraps(getattr(Bottle, name)) + def wrapper(*a, **ka): + return getattr(app(), name)(*a, **ka) + return wrapper + +for name in 'route get post put delete error mount hook'.split(): + globals()[name] = make_default_app_wrapper(name) + +url = make_default_app_wrapper('get_url') + def default(): - raise DeprecationWarning("Use @error(404) instead.") + depr("The default() decorator is deprecated. Use @error(404) instead.") + return error(404) @@ -1282,7 +1388,6 @@ def default(): class ServerAdapter(object): quiet = False - def __init__(self, host='127.0.0.1', port=8080, **kargs): self.options = kargs self.host = host @@ -1333,9 +1438,11 @@ class CherryPyServer(ServerAdapter): class PasteServer(ServerAdapter): def run(self, handler): from paste import httpserver - from paste.translogger import TransLogger - app = TransLogger(handler) - httpserver.serve(app, host=self.host, port=str(self.port), **self.options) + if not self.quiet: + from paste.translogger import TransLogger + handler = TransLogger(handler) + httpserver.serve(handler, host=self.host, port=str(self.port), + **self.options) class FapwsServer(ServerAdapter): @@ -1345,13 +1452,21 @@ class FapwsServer(ServerAdapter): """ def run(self, handler): import fapws._evwsgi as evwsgi - from fapws import base - evwsgi.start(self.host, self.port) + from fapws import base, config + port = self.port + if float(config.SERVER_IDENT[-2:]) > 0.4: + # fapws3 silently changed its API in 0.5 + port = str(port) + evwsgi.start(self.host, port) + # fapws3 never releases the GIL. Complain upstream. I tried. No luck. + if 'BOTTLE_CHILD' in os.environ and not self.quiet: + print "WARNING: Auto-reloading does not work with Fapws3." + print " (Fapws3 breaks python thread support)" evwsgi.set_base_module(base) def app(environ, start_response): environ['wsgi.multiprocess'] = False return handler(environ, start_response) - evwsgi.wsgi_cb(('',app)) + evwsgi.wsgi_cb(('', app)) evwsgi.run() @@ -1398,12 +1513,22 @@ class DieselServer(ServerAdapter): app.run() +class GeventServer(ServerAdapter): + """ Untested. """ + def run(self, handler): + from gevent import wsgi + from gevent.hub import getcurrent + self.set_context_ident(getcurrent, weakref=True) # see contextlocal + wsgi.WSGIServer((self.host, self.port), handler).serve_forever() + + class GunicornServer(ServerAdapter): """ Untested. """ def run(self, handler): from gunicorn.arbiter import Arbiter from gunicorn.config import Config arbiter = Arbiter(Config({'bind': "%s:%d" % (self.host, self.port), 'workers': 4}), handler) + arbiter.run() class EventletServer(ServerAdapter): @@ -1434,7 +1559,7 @@ class RocketServer(ServerAdapter): class AutoServer(ServerAdapter): """ Untested. """ - adapters = [CherryPyServer, PasteServer, TwistedServer, WSGIRefServer] + adapters = [PasteServer, CherryPyServer, TwistedServer, WSGIRefServer] def run(self, handler): for sa in self.adapters: try: @@ -1443,79 +1568,170 @@ class AutoServer(ServerAdapter): pass -def run(app=None, server=WSGIRefServer, host='127.0.0.1', port=8080, +server_names = { + 'cgi': CGIServer, + 'flup': FlupFCGIServer, + 'wsgiref': WSGIRefServer, + 'cherrypy': CherryPyServer, + 'paste': PasteServer, + 'fapws3': FapwsServer, + 'tornado': TornadoServer, + 'gae': AppEngineServer, + 'twisted': TwistedServer, + 'diesel': DieselServer, + 'gunicorn': GunicornServer, + 'eventlet': EventletServer, + 'gevent': GeventServer, + 'rocket': RocketServer, + 'auto': AutoServer, +} + + +def load_app(target): + """ Load a bottle application based on a target string and return the app + object. + + The target should be a valid python import path + (e.g. mypackage.mymodule). The default application is returned. + If the targed contains a colon (e.g. mypackage.mymodule:myapp) the + module variable specified after the colon is returned instead. + """ + path, name = target.split(":", 1) if ':' in target else (target, None) + rv = None if name else app.push() + __import__(path) + module = sys.modules[path] + if rv and rv in app: app.remove(rv) + return rv if rv else getattr(module, target) + + +def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, interval=1, reloader=False, quiet=False, **kargs): - """ Runs bottle as a web server. """ - app = app if app else default_app() - # Instantiate server, if it is a class instead of an instance + """ Start a server instance. This method blocks until the server + terminates. + + :param app: WSGI application or target string supported by + :func:`load_app`. (default: :func:`default_app`) + :param server: Server adapter to use. See :data:`server_names` dict + for valid names or pass a :class:`ServerAdapter` subclass. + (default: wsgiref) + :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on + all interfaces including the external one. (default: 127.0.0.1) + :param host: Server port to bind to. Values below 1024 require root + privileges. (default: 8080) + :param reloader: Start auto-reloading server? (default: False) + :param interval: Auto-reloader interval in seconds (default: 1) + :param quiet: Supress output to stdout and stderr? (default: False) + :param options: Options passed to the server adapter. + """ + app = app or default_app() + if isinstance(app, basestring): + app = load_app(app) + if isinstance(server, basestring): + server = server_names.get(server) if isinstance(server, type): server = server(host=host, port=port, **kargs) if not isinstance(server, ServerAdapter): - raise RuntimeError("Server must be a subclass of WSGIAdapter") + raise RuntimeError("Server must be a subclass of ServerAdapter") server.quiet = server.quiet or quiet - if not server.quiet: # pragma: no cover - if not reloader or os.environ.get('BOTTLE_CHILD') == 'true': - print "Bottle server starting up (using %s)..." % repr(server) - print "Listening on http://%s:%d/" % (server.host, server.port) - print "Use Ctrl-C to quit." - print - else: - print "Bottle auto reloader starting up..." + if not server.quiet and not os.environ.get('BOTTLE_CHILD'): + print "Bottle server starting up (using %s)..." % repr(server) + print "Listening on http://%s:%d/" % (server.host, server.port) + print "Use Ctrl-C to quit." + print try: - if reloader and interval: - reloader_run(server, app, interval) + if reloader: + interval = min(interval, 1) + if os.environ.get('BOTTLE_CHILD'): + _reloader_child(server, app, interval) + else: + _reloader_observer(server, app, interval) else: server.run(app) except KeyboardInterrupt: - if not server.quiet: # pragma: no cover - print "Shutting Down..." + pass + if not server.quiet and not os.environ.get('BOTTLE_CHILD'): + print "Shutting down..." + +class FileCheckerThread(threading.Thread): + ''' Thread that periodically checks for changed module files. ''' -def reloader_run(server, app, interval): - if os.environ.get('BOTTLE_CHILD') == 'true': - # We are a child process + def __init__(self, lockfile, interval): + threading.Thread.__init__(self) + self.lockfile, self.interval = lockfile, interval + #1: lockfile to old; 2: lockfile missing + #3: module file changed; 5: external exit + self.status = 0 + + def run(self): + exists = os.path.exists + mtime = lambda path: os.stat(path).st_mtime files = dict() for module in sys.modules.values(): - file_path = getattr(module, '__file__', None) - if file_path and os.path.isfile(file_path): - file_split = os.path.splitext(file_path) - if file_split[1] in ('.py', '.pyc', '.pyo'): - file_path = file_split[0] + '.py' - files[file_path] = os.stat(file_path).st_mtime - thread.start_new_thread(server.run, (app,)) - parent_pid = int(os.environ.get('BOTTLE_PID')) - while True: - time.sleep(interval) - for file_path, file_mtime in files.iteritems(): - if not os.path.exists(file_path): - print "File changed: %s (deleted)" % file_path - elif os.stat(file_path).st_mtime > file_mtime: - print "File changed: %s (modified)" % file_path - else: - # check wether parent process is still alive - try: - os.kill(parent_pid, 0) - except OSError: - print - print 'Parent Bottle process killed' - print 'Use Ctrl-C to exit.' - else: - print "Restarting..." - continue - app.serve = False - time.sleep(interval) # be nice and wait for running requests - sys.exit(3) - while True: - args = [sys.executable] + sys.argv - environ = os.environ.copy() - environ['BOTTLE_CHILD'] = 'true' - environ['BOTTLE_PID'] = str(os.getpid()) - exit_status = subprocess.call(args, env=environ) - if exit_status != 3: - sys.exit(exit_status) - + try: + path = inspect.getsourcefile(module) + if path and exists(path): files[path] = mtime(path) + except TypeError: + pass + while not self.status: + for path, lmtime in files.iteritems(): + if not exists(path) or mtime(path) > lmtime: + self.status = 3 + if not exists(self.lockfile): + self.status = 2 + elif mtime(self.lockfile) < time.time() - self.interval * 2: + self.status = 1 + if not self.status: + time.sleep(self.interval) + if self.status != 5: + thread.interrupt_main() + + +def _reloader_child(server, app, interval): + ''' Start the server and check for modified files in a background thread. + As soon as an update is detected, KeyboardInterrupt is thrown in + the main thread to exit the server loop. The process exists with status + code 3 to request a reload by the observer process. If the lockfile + is not modified in 2*interval second or missing, we assume that the + observer process died and exit with status code 1 or 2. + ''' + lockfile = os.environ.get('BOTTLE_LOCKFILE') + bgcheck = FileCheckerThread(lockfile, interval) + try: + bgcheck.start() + server.run(app) + except KeyboardInterrupt: + pass + bgcheck.status, status = 5, bgcheck.status + bgcheck.join() # bgcheck.status == 5 --> silent exit + if status: sys.exit(status) +def _reloader_observer(server, app, interval): + ''' Start a child process with identical commandline arguments and restart + it as long as it exists with status code 3. Also create a lockfile and + touch it (update mtime) every interval seconds. + ''' + fd, lockfile = tempfile.mkstemp(prefix='bottle-reloader.', suffix='.lock') + os.close(fd) # We only need this file to exist. We never write to it + try: + while os.path.exists(lockfile): + args = [sys.executable] + sys.argv + environ = os.environ.copy() + environ['BOTTLE_CHILD'] = 'true' + environ['BOTTLE_LOCKFILE'] = lockfile + p = subprocess.Popen(args, env=environ) + while p.poll() is None: # Busy wait... + os.utime(lockfile, None) # I am alive! + time.sleep(interval) + if p.poll() != 3: + if os.path.exists(lockfile): os.unlink(lockfile) + sys.exit(p.poll()) + elif not server.quiet: + print "Reloading server..." + except KeyboardInterrupt: + pass + if os.path.exists(lockfile): os.unlink(lockfile) @@ -1560,8 +1776,8 @@ class BaseTemplate(object): @classmethod def search(cls, name, lookup=[]): - """ Search name in all directiries specified in lookup. - First without, then with common extentions. Return first hit. """ + """ Search name in all directories specified in lookup. + First without, then with common extensions. Return first hit. """ if os.path.isfile(name): return name for spath in lookup: fname = os.path.join(spath, name) @@ -1580,7 +1796,7 @@ class BaseTemplate(object): return cls.settings[key] def prepare(self, **options): - """ Run preparatios (parsing, caching, ...). + """ Run preparations (parsing, caching, ...). It should be possible to call this again to refresh a template or to update settings. """ @@ -1589,7 +1805,7 @@ class BaseTemplate(object): def render(self, **args): """ Render the template with the specified local variables and return a single byte or unicode string. If it is a byte string, the encoding - must match self.encoding. This method must be thread save! + must match self.encoding. This method must be thread-safe! """ raise NotImplementedError @@ -1683,8 +1899,7 @@ class SimpleTemplate(BaseTemplate): lineno = 0 # Current line of code ptrbuffer = [] # Buffer for printable strings and token tuple instances codebuffer = [] # Buffer for generated python code - touni = functools.partial(unicode, encoding=self.encoding) - multiline = dedent = False + multiline = dedent = oneline = False def yield_tokens(line): for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)): @@ -1826,11 +2041,11 @@ cheetah_template = functools.partial(template, template_adapter=CheetahTemplate) jinja2_template = functools.partial(template, template_adapter=Jinja2Template) def view(tpl_name, **defaults): - ''' Decorator: Renders a template for a handler. + ''' Decorator: renders a template for a handler. The handler can control its behavior like that: - return a dict of template vars to fill out the template - - return other than a dict and the view decorator will not + - return something other than a dict and the view decorator will not process the template, but return the handler result as is. This includes returning a HTTPResponse(dict) to get, for instance, JSON with autojson or other castfilters @@ -1949,7 +2164,7 @@ metadata about the current request into this instance of :class:`Request`. It is thread-safe and can be accessed from within handler functions. """ response = Response() -""" The :class:`Bottle` WSGI handler uses metasata assigned to this instance +""" The :class:`Bottle` WSGI handler uses metadata assigned to this instance of :class:`Response` to generate the WSGI response. """ # Initialize app stack (create first empty Bottle app) diff --git a/run_tests.sh b/run_tests.sh new file mode 100644 index 0000000..d081af7 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +root=`pwd` +if [ -d "$root/tbuild/opt/bin" ]; then + PATH="$root/tbuild/opt/bin:$PATH" +fi + +function fail { + cat test.log + echo -e "\e[0;31mFAILED! :(\e[0m" + exit 1 +} + +function runtest { + if type $1 &>/dev/null ; then + $1 $2/testall.py &> test.log || fail + else + echo "Warning: Skipping test for $1 (Not installed)" + fi +} + +runtest python2.5 test +runtest python2.6 test +runtest python2.7 test + +if type 2to3 &> /dev/null ; then + rm -rf test3k &> /dev/null + mkdir test3k + cp -a test/* test3k + cp bottle.py test3k + 2to3 -w test3k/*.py &> 2to3.log || fail + + runtest python3.0 test3k + runtest python3.1 test3k + runtest python3.2 test3k + + rm -rf test3k +fi + +echo -e "\e[0;32mPASSED :)\e[0m" + @@ -1,7 +1,7 @@ #!/usr/bin/env python import sys -import os.path +import os from distutils.core import setup if sys.version_info < (2,5): diff --git a/test/servertest.py b/test/servertest.py new file mode 100644 index 0000000..8ce87b0 --- /dev/null +++ b/test/servertest.py @@ -0,0 +1,30 @@ +import sys, os +test_root = os.path.dirname(os.path.abspath(__file__)) +os.chdir(test_root) +sys.path.insert(0, os.path.dirname(test_root)) +sys.path.insert(0, test_root) + +import bottle +from bottle import route, run + +if 'coverage' in sys.argv: + import coverage + cov = coverage.coverage(data_suffix=True, branch=True) + cov.start() + +@route() +def test(): + return "OK" + +if __name__ == '__main__': + server = getattr(bottle, sys.argv[1]) + port = int(sys.argv[2]) + try: + run(port=port, server=server) + except ImportError: + print "Warning: Could not test %s. Import error." % server + + if 'coverage' in sys.argv: + cov.stop() + cov.save() + diff --git a/test/test_environ.py b/test/test_environ.py index 8a6e32e..2e026bb 100755 --- a/test/test_environ.py +++ b/test/test_environ.py @@ -237,5 +237,5 @@ class TestMultipart(unittest.TestCase): self.assertEqual(2, len(request.POST.getall('field2'))) self.assertEqual(['value2', 'value3'], request.POST.getall('field2')) -if __name__ == '__main__': +if __name__ == '__main__': #pragma: no cover unittest.main() diff --git a/test/test_jinja2.py b/test/test_jinja2.py index 5057467..1a91270 100644 --- a/test/test_jinja2.py +++ b/test/test_jinja2.py @@ -56,6 +56,6 @@ except ImportError: print "WARNING: No Jinja2 template support. Skipping tests." del TestJinja2Template -if __name__ == '__main__': +if __name__ == '__main__': #pragma: no cover unittest.main() diff --git a/test/test_mako.py b/test/test_mako.py index 4ad577e..74ff224 100644 --- a/test/test_mako.py +++ b/test/test_mako.py @@ -40,6 +40,6 @@ except ImportError: print "WARNING: No Mako template support. Skipping tests." del TestMakoTemplate -if __name__ == '__main__': +if __name__ == '__main__': #pragma: no cover unittest.main() diff --git a/test/test_mdict.py b/test/test_mdict.py index 55be52a..8515323 100644 --- a/test/test_mdict.py +++ b/test/test_mdict.py @@ -46,6 +46,6 @@ class TestMultiDict(unittest.TestCase): -if __name__ == '__main__': +if __name__ == '__main__': #pragma: no cover unittest.main() diff --git a/test/test_outputfilter.py b/test/test_outputfilter.py index 7604727..5bbb0ab 100644 --- a/test/test_outputfilter.py +++ b/test/test_outputfilter.py @@ -68,8 +68,11 @@ class TestOutputFilter(ServerTestBase): def test_json(self): self.app.route('/')(lambda: {'a': 1}) - self.assertBody(bottle.json_dumps({'a': 1})) - self.assertHeader('Content-Type','application/json') + if bottle.json_dumps: + self.assertBody(bottle.json_dumps({'a': 1})) + self.assertHeader('Content-Type','application/json') + else: + print "Warning: No json module installed." def test_custom(self): self.app.route('/')(lambda: {'a': 1, 'b': 2}) @@ -151,5 +154,5 @@ class TestOutputFilter(ServerTestBase): self.assertTrue('b=b' in c) self.assertTrue('c=c; Path=/' in c) -if __name__ == '__main__': +if __name__ == '__main__': #pragma: no cover unittest.main() diff --git a/test/test_router.py b/test/test_router.py index 38c8337..0960a15 100755 --- a/test/test_router.py +++ b/test/test_router.py @@ -57,5 +57,5 @@ class TestRouter(unittest.TestCase): #self.assertRaises(bottle.RouteBuildError, build, 'anonroute') # RouteBuildError: Anonymous pattern found. Can't generate the route 'anonroute'. -if __name__ == '__main__': +if __name__ == '__main__': #pragma: no cover unittest.main() diff --git a/test/test_securecookies.py b/test/test_securecookies.py index 6d72945..978b30f 100644 --- a/test/test_securecookies.py +++ b/test/test_securecookies.py @@ -28,5 +28,5 @@ class TestSecureCookies(unittest.TestCase): self.assertEqual(repr(dict(value=5)), repr(bottle.request.get_cookie('key', secret=tob('1234')))) bottle.app.pop() -if __name__ == '__main__': +if __name__ == '__main__': #pragma: no cover unittest.main() diff --git a/test/test_sendfile.py b/test/test_sendfile.py index 6372c8a..a97797e 100755 --- a/test/test_sendfile.py +++ b/test/test_sendfile.py @@ -85,6 +85,6 @@ class TestSendFile(unittest.TestCase): self.assertEqual(open(__file__,'rb').read(), f.output.read()) -if __name__ == '__main__': +if __name__ == '__main__': #pragma: no cover unittest.main() diff --git a/test/test_server.py b/test/test_server.py new file mode 100644 index 0000000..83a9b40 --- /dev/null +++ b/test/test_server.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +import unittest +import bottle +import urllib2 +import time +from tools import tob +import sys +import os +import signal +import socket +from subprocess import Popen, PIPE + +serverscript = os.path.join(os.path.dirname(__file__), 'servertest.py') + +def ping(server, port): + ''' Check if a server acccepts connections on a specific TCP port ''' + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((server, port)) + s.close() + return True + except socket.error, e: + return False + +class TestServer(unittest.TestCase): + server = 'WSGIRefServer' + port = 12643 + + def setUp(self): + # Start servertest.py in a subprocess + cmd = [sys.executable, serverscript, self.server, str(self.port)] + cmd += sys.argv[1:] # pass cmdline arguments to subprocesses + self.p = Popen(cmd, stdout=PIPE, stderr=PIPE) + # Wait for the socket to accept connections + for i in xrange(100): + time.sleep(0.1) + # Check if the process has died for some reason + if self.p.poll() != None: break + if ping('127.0.0.1', self.port): break + + def tearDown(self): + while self.p.poll() is None: + os.kill(self.p.pid, signal.SIGINT) + time.sleep(0.1) + if self.p.poll() is None: + os.kill(self.p.pid, signal.SIGTERM) + for stream in (self.p.stdout, self.p.stderr): + for line in stream: + if tob('Warning') in line \ + or tob('Error') in line: + print line.strip().decode('utf8') + + def fetch(self, url): + try: + return urllib2.urlopen('http://127.0.0.1:%d/%s' % (self.port, url)).read() + except Exception, e: + return repr(e) + + def test_test(self): + ''' Test a simple static page with this server adapter. ''' + if self.p.poll() == None: + self.assertEqual(tob('OK'), self.fetch('test')) + #else: + # self.assertTrue(False, "Server process down") + + +class TestCherryPyServer(TestServer): + server = 'CherryPyServer' + +class TestPasteServer(TestServer): + server = 'PasteServer' + +class TestTornadoServer(TestServer): + server = 'TornadoServer' + +class TestTwistedServer(TestServer): + server = 'TwistedServer' + +class TestDieselServer(TestServer): + server = 'DieselServer' + +class TestGunicornServer(TestServer): + server = 'GunicornServer' + +class TestGeventServer(TestServer): + server = 'GeventServer' + +class TestEventletServer(TestServer): + server = 'EventletServer' + +class TestRocketServer(TestServer): + server = 'RocketServer' + +class TestFapwsServer(TestServer): + server = 'FapwsServer' + +if __name__ == '__main__': #pragma: no cover + unittest.main() diff --git a/test/test_stpl.py b/test/test_stpl.py index babbffe..51f4893 100755 --- a/test/test_stpl.py +++ b/test/test_stpl.py @@ -202,6 +202,6 @@ class TestSimpleTemplate(unittest.TestCase): self.assertEqual(u'\n\nöäü?@€', t.render()) self.assertEqual(t.encoding, 'utf8') -if __name__ == '__main__': +if __name__ == '__main__': #pragma: no cover unittest.main() diff --git a/test/test_wsgi.py b/test/test_wsgi.py index 9fc91a3..2f2c4ea 100755 --- a/test/test_wsgi.py +++ b/test/test_wsgi.py @@ -135,6 +135,154 @@ class TestWsgi(ServerTestBase): self.assertTrue('b=b' in c) self.assertTrue('c=c; Path=/' in c) +class TestRouteDecorator(ServerTestBase): + def test_decorators(self): + app = bottle.Bottle() + def foo(): pass + app.route('/g')(foo) + bottle.route('/g')(foo) + app.route('/g2', method='GET')(foo) + bottle.get('/g2')(foo) + app.route('/p', method='POST')(foo) + bottle.post('/p')(foo) + app.route('/p2', method='PUT')(foo) + bottle.put('/p2')(foo) + app.route('/d', method='DELETE')(foo) + bottle.delete('/d')(foo) + self.assertEqual(app.routes, bottle.app().routes) + + def test_single_path(self): + @bottle.route('/a') + def test(): return 'ok' + self.assertBody('ok', '/a') + self.assertStatus(404, '/b') + + def test_path_list(self): + @bottle.route(['/a','/b']) + def test(): return 'ok' + self.assertBody('ok', '/a') + self.assertBody('ok', '/b') + self.assertStatus(404, '/c') + + def test_no_path(self): + @bottle.route() + def test(x=5): return str(x) + self.assertBody('5', '/test') + self.assertBody('6', '/test/6') + + def test_no_params_at_all(self): + @bottle.route + def test(x=5): return str(x) + self.assertBody('5', '/test') + self.assertBody('6', '/test/6') + + def test_method(self): + @bottle.route(method=' gEt ') + def test(): return 'ok' + self.assertBody('ok', '/test', method='GET') + self.assertStatus(200, '/test', method='HEAD') + self.assertStatus(405, '/test', method='PUT') + + def test_method_list(self): + @bottle.route(method=['GET','post']) + def test(): return 'ok' + self.assertBody('ok', '/test', method='GET') + self.assertBody('ok', '/test', method='POST') + self.assertStatus(405, '/test', method='PUT') + + def test_decorate(self): + def revdec(func): + def wrapper(*a, **ka): + return reversed(func(*a, **ka)) + return wrapper + + @bottle.route('/nodec') + @bottle.route('/dec', decorate=revdec) + def test(): return '1', '2' + self.assertBody('21', '/dec') + self.assertBody('12', '/nodec') + + def test_decorate_list(self): + def revdec(func): + def wrapper(*a, **ka): + return reversed(func(*a, **ka)) + return wrapper + def titledec(func): + def wrapper(*a, **ka): + return ''.join(func(*a, **ka)).title() + return wrapper + + @bottle.route('/revtitle', decorate=[revdec, titledec]) + @bottle.route('/titlerev', decorate=[titledec, revdec]) + def test(): return 'a', 'b', 'c' + self.assertBody('cbA', '/revtitle') + self.assertBody('Cba', '/titlerev') + + def test_hooks(self): + @bottle.route() + def test(): + return bottle.request.environ.get('hooktest','nohooks') + @bottle.hook('before_request') + def hook(): + bottle.request.environ['hooktest'] = 'before' + @bottle.hook('after_request') + def hook(): + if isinstance(bottle.response.output, str): + bottle.response.output += '-after' + self.assertBody('before-after', '/test') + + def test_no_hooks(self): + @bottle.route(no_hooks=True) + def test(): + return 'nohooks' + bottle.hook('before_request')(lambda: 1/0) + bottle.hook('after_request')(lambda: 1/0) + self.assertBody('nohooks', '/test') + + def test_hook_order(self): + @bottle.route() + def test(): return bottle.request.environ.get('hooktest','nohooks') + @bottle.hook('before_request') + def hook(): bottle.request.environ.setdefault('hooktest', []).append('b1') + @bottle.hook('before_request') + def hook(): bottle.request.environ.setdefault('hooktest', []).append('b2') + @bottle.hook('after_request') + def hook(): bottle.response.output += 'a1' + @bottle.hook('after_request') + def hook(): bottle.response.output += 'a2' + self.assertBody('b1b2a2a1', '/test') + + def test_template(self): + @bottle.route(template='test {{a}} {{b}}') + def test(): return dict(a=5, b=6) + self.assertBody('test 5 6', '/test') + + def test_template_opts(self): + @bottle.route(template='test {{a}} {{b}}', template_opts={'b': 6}) + def test(): return dict(a=5) + self.assertBody('test 5 6', '/test') + + def test_static(self): + @bottle.route('/:foo', static=True) + def test(): return 'ok' + print bottle.app().routes.static + self.assertBody('ok', '/:foo') + + def test_name(self): + @bottle.route(name='foo') + def test(x=5): return 'ok' + self.assertEquals('/test/6', bottle.url('foo', x=6)) + + def test_callback(self): + def test(x=5): return str(x) + rv = bottle.route(callback=test) + self.assertBody('5', '/test') + self.assertBody('6', '/test/6') + self.assertEqual(rv, test) + + + + class TestDecorators(ServerTestBase): ''' Tests Decorators ''' @@ -182,25 +330,18 @@ class TestDecorators(ServerTestBase): self.assertBody('', '/test/304') def test_routebuild(self): - """ WSGI: Test validate-decorator""" - @bottle.route('/a/:b/c', name='named') - def test(var): pass + """ WSGI: Test route builder """ + def foo(): pass + bottle.route('/a/:b/c', name='named')(foo) + bottle.request.environ['SCRIPT_NAME'] = '' self.assertEqual('/a/xxx/c', bottle.url('named', b='xxx')) self.assertEqual('/a/xxx/c', bottle.app().get_url('named', b='xxx')) - - def test_decorators(self): - app = bottle.Bottle() - app.route('/g')('foo') - bottle.route('/g')('foo') - app.route('/g2', method='GET')('foo') - bottle.get('/g2')('foo') - app.route('/p', method='POST')('foo') - bottle.post('/p')('foo') - app.route('/p2', method='PUT')('foo') - bottle.put('/p2')('foo') - app.route('/d', method='DELETE')('foo') - bottle.delete('/d')('foo') - self.assertEqual(app.routes, bottle.app().routes) + bottle.request.environ['SCRIPT_NAME'] = '/app' + self.assertEqual('/app/a/xxx/c', bottle.url('named', b='xxx')) + bottle.request.environ['SCRIPT_NAME'] = '/app/' + self.assertEqual('/app/a/xxx/c', bottle.url('named', b='xxx')) + bottle.request.environ['SCRIPT_NAME'] = 'app/' + self.assertEqual('/app/a/xxx/c', bottle.url('named', b='xxx')) def test_autoroute(self): app = bottle.Bottle() @@ -240,5 +381,5 @@ class TestAppMounting(ServerTestBase): -if __name__ == '__main__': +if __name__ == '__main__': #pragma: no cover unittest.main() diff --git a/test/testall.py b/test/testall.py index 2a82bb6..cb45b28 100755 --- a/test/testall.py +++ b/test/testall.py @@ -4,12 +4,38 @@ import unittest import sys, os, glob -os.chdir(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, '../') -sys.path.insert(0, './') +test_root = os.path.dirname(os.path.abspath(__file__)) +test_files = glob.glob(os.path.join(test_root, 'test_*.py')) -unittests = [name[2:-3] for name in glob.glob('./test_*.py')] -suite = unittest.defaultTestLoader.loadTestsFromNames(unittests) +os.chdir(test_root) +sys.path.insert(0, os.path.dirname(test_root)) +sys.path.insert(0, test_root) +test_names = [os.path.basename(name)[:-3] for name in test_files] + +if 'help' in sys.argv or '-h' in sys.argv: + print + print "Command line arguments:" + print + print "fast: Skip server adapter tests." + print "verbose: Print tests even if they pass." + print "coverage: Measure code coverage." + print "html: Create a html coverage report. Requires 'coverage'" + print "clean: Delete coverage or temporary files" + print + sys.exit(0) + + +if 'fast' in sys.argv: + print "Warning: The 'fast' keyword skipps server tests." + test_names.remove('test_server') + +cov = None +if 'coverage' in sys.argv: + import coverage + cov = coverage.coverage(data_suffix=True, branch=True) + cov.start() + +suite = unittest.defaultTestLoader.loadTestsFromNames(test_names) #import doctest #doctests = glob.glob('./doctest_*.txt') @@ -18,7 +44,25 @@ suite = unittest.defaultTestLoader.loadTestsFromNames(unittests) def run(): import bottle bottle.debug(True) - result = unittest.TextTestRunner(verbosity=0).run(suite) + vlevel = 2 if 'verbose' in sys.argv else 0 + result = unittest.TextTestRunner(verbosity=vlevel).run(suite) + print + + if cov: + cov.stop() + cov.save() + # Recreate coverage object so new files created in other processes are + # recognized + cnew = coverage.coverage(data_suffix=True, branch=True) + cnew.combine() + print "Coverage:" + cnew.report(morfs=['bottle.py']+test_files, show_missing=False) + if 'html' in sys.argv: + print + cnew.html_report(morfs=['bottle.py']+test_files, directory='coverage') + print "Coverage report is in %s" % \ + os.path.abspath('coverage/index.html') + sys.exit((result.errors or result.failures) and 1 or 0) if __name__ == '__main__': diff --git a/test/tools.py b/test/tools.py index c78cfdd..221c362 100755 --- a/test/tools.py +++ b/test/tools.py @@ -69,7 +69,8 @@ class ServerTestBase(unittest.TestCase): return result def postmultipart(self, path, fields, files): - return self.urlopen(path, multipart_environ(env), method='POST') + env = multipart_environ(fields, files) + return self.urlopen(path, method='POST', env=env) def tearDown(self): bottle.app.pop() |