秘诀

这里收集了一些常见用例的代码片段和例子.

使用Session

Bottle自身并没有提供Session的支持,因为在一个迷你框架里面,没有合适的方法来实现。根据需求和使用环境,你可以使用 beaker 中间件或自己来实现。下面是一个使用beaker的例子,Session数据存放在"./data"目录里面:

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: 调试中间件

Bottle捕获所有应用抛出的异常,防止异常导致WSGI服务器崩溃。如果内置的 debug() 模式不能满足你的要求,你想在你自己写的中间件里面处理这些异常,那么你可以关闭这个功能。

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)

现在,Bottle仅会捕获并处理它自己抛出的异常( HTTPError , HTTPResponseBottleException ),你的中间件可以处理剩下的那些异常。

werkzeugpaste 这两个第三方库都提供了非常强大的调试中间件。如果是 werkzeug ,可看看 werkzeug.debug.DebuggedApplication ,如果是 paste ,可看看 paste.evalexception.middleware.EvalException 。它们都可让你检查运行栈,甚至在保持运行栈上下文的情况下,执行Python代码。所以 不要在生产环境中使用它们

Bottle 应用单元测试

Unit测试一般用于测试应用中的函数,但不需要一个WSGI环境。

使用 Nose 的简单例子。

import bottle

@bottle.route('/')
def index():
    return 'Hi!'

if __name__ == '__main__':
    bottle.run()

测试代码:

import mywebapp

def test_webapp_index():
    assert mywebapp.index() == 'Hi!'

在这个例子中,Bottle的route()函数没有被执行,仅测试了index()函数。

如果要测试的代码需要访问 bottle.request 你可以用 Boddle ::

import bottle

@bottle.route('/')
def index():
    return 'Hi %s!' % bottle.request.params['name']

测试代码:

import mywebapp
from boddle import boddle

def test_webapp_index():
    with boddle(params={'name':'Derek'}):
        assert mywebapp.index() == 'Hi Derek!'

功能测试

任何基于HTTP的测试系统都可用于测试WSGI服务器,但是有些测试框架与WSGI服务器工作得更好。它们可以在一个可控环境里运行WSGI应用,充分利用traceback和调试工具。 Testing tools for WSGI 是一个很好的上手工具。

使用 WebTestNose 的例子。

from webtest import TestApp
import mywebapp

def test_functional_login_logout():
    app = TestApp(mywebapp.app)

    app.post('/login', {'user': 'foo', 'pass': 'bar'}) # log in and get a cookie

    assert app.get('/admin').status == '200 OK'        # fetch a page successfully

    assert app.get('/logout').status_code = 200        # log out
    app.reset()                                        # drop the cookie

    # fetch the same page, unsuccessfully
    assert app.get('/admin', expect_errors=True).status == '401 Unauthorized'

嵌入其他WSGI应用

并不建议你使用这个方法,你应该在Bottle前面使用一个中间件来做这样的事情。但你确实可以在Bottle里面调用其他WSGI应用,让Bottle扮演一个中间件的角色。下面是一个例子。

from bottle import request, response, route
subproject = SomeWSGIApplication()

@route('/subproject/<subpath:re:.*>', method='ANY')
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)

再次强调,并不建议使用这种方法。之所以介绍这种方法,是因为很多人问起,如何在Bottle中调用WSGI应用。

忽略尾部的反斜杠

在Bottle看来, /example/example/ 是两个不同的route [1] 。为了一致对待这两个URL,你应该添加两个route。

@route('/test')
@route('/test/')
def test(): return 'Slash? no?'

添加一个wsgi中间件,从所有URL中删除尾随斜杠::

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=myapp)

或添加 before_request 钩住以去除尾随斜杠:

@hook('before_request')
def strip_path():
    request.environ['PATH_INFO'] = request.environ['PATH_INFO'].rstrip('/')

脚注

[1]因为确实如此,见 <http://www.ietf.org/rfc/rfc3986.txt>

Keep-alive 请求

注解

详见 异步应用入门

像XHR这样的"push"机制,需要在HTTP响应头中加入 "Connection: keep-alive" ,以便在不关闭连接的情况下,写入响应数据。WSGI并不支持这种行为,但如果在Bottle中使用 gevent 这个异步框架,还是可以实现的。下面是一个例子,可配合 gevent HTTP服务器或 paste HTTP服务器使用(也许支持其他服务器,但是我没试过)。在run()函数里面使用 server='gevent'server='paste' 即可使用这两种服务器。

from gevent import monkey; monkey.patch_all()

import gevent
from bottle import route, run

@route('/stream')
def stream():
    yield 'START'
    gevent.sleep(3)
    yield 'MIDDLE'
    gevent.sleep(5)
    yield 'END'

run(host='0.0.0.0', port=8080, server='gevent')

通过浏览器访问 http://localhost:8080/stream ,可看到'START','MIDDLE',和'END'这三个字眼依次出现,一共用了8秒。

Gzip压缩

注解

详见 compression

Gzip压缩,可加速网站静态资源(例如CSS和JS文件)的访问。人们希望Bottle支持Gzip压缩,(但是不支持)......

支持Gzip压缩并不简单,一个合适的Gzip实现应该满足以下条件。

  • 压缩速度要快
  • 如果浏览器不支持,则不压缩
  • 不压缩那些已经充分压缩的文件(图像,视频)
  • 不压缩动态文件
  • 支持两种压缩算法(gzip和deflate)
  • 缓存那些不经常变化的压缩文件
  • 不验证缓存中那些已经变化的文件(De-validate the cache if one of the files changed anyway)
  • 确保缓存不太大
  • 不缓存小文件,因为寻道时间或许比压缩时间还长

因为有上述种种限制,建议由WSGI服务器来处理Gzip压缩而不是Bottle。像 cherrypy 就提供了一个 GzipFilter 中间件来处理Gzip压缩。

使用钩子

例如,你想提供跨域资源共享,可参考下面的例子。

from bottle import hook, response, route

@hook('after_request')
def enable_cors():
    response.headers['Access-Control-Allow-Origin'] = '*'

@route('/foo')
def say_foo():
    return 'foo!'

@route('/bar')
def say_bar():
    return {'type': 'friendly', 'content': 'Hi!'}

你也可以使用 before_request ,这样在route的回调函数被调用之前,都会调用你的钩子函数。

在Heroku中使用Bottle

Heroku ,一个流行的云应用平台,提供Python支持。

这份教程基于 Heroku Quickstart, 用Bottle特有的代码替换了 Getting Started with Python on Heroku/Cedar 中的`Write Your App <http://devcenter.heroku.com/articles/python#write_your_app>`_ 这部分:

import os
from bottle import route, run

@route("/")
def hello_world():
    return "Hello World!"

run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))

Heroku使用 os.environ 字典来提供Bottle应用需要监听的端口。