Bottle是一个快速、简单、轻量级的WSGI微框架,整个项目只有一个文件,并且,除了Python标准库以外,Bottle不依赖其他第三方库。

为了对Bottle项目有个大致的了解,我们选择0.4.10版本研究,这个版本中,Bottle代码量不超过1000行,基本功能都已经实现并且研究起来也会容易些。下面的内容,如果没有特殊说明,都是指0.4.10版本的源码。

准备

先从Github上下载Bottle的源码

1
git clone https://github.com/bottlepy/bottle.git

下载完以后,进入bottle项目目录,同时切换到0.4.10版本

1
2
cd bottle
git checkout -b 0.4.10

此时,bottle.py就是我们要研究的代码了。

Bottle 示例程序

想要研究源码,必然要先让程序跑起来,看看程序的大概过程是怎么样的。

在Bottle项目中新建demo.py,写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from bottle import route, run, request, send_file, template

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

@route('/hello/:name')
def hello_name(name):
return 'Hello %s!' % name

@route('/hello', method='POST')
def hello_post():
name = request.POST['name']
return 'Hello %s!' % name

@route('/static/:filename#.*#')
def static_file(filename):
send_file(filename, root='/path/to/static/files/')

@route('/template/test')
def template_test():
return template('index', title='Template Test', items=[1,2,3,'fly'])

run(host='localhost', port=8080)

新建模板文件index.tpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
%message = 'Hello world!'
<html>
<head>
<title>{{title.title()}}</title>
</head>
<body>
<h1>{{title.title()}}</h1>
<p>{{message}}</p>
<p>Items in list: {{len(items)}}</p>
<ul>
%for item in items:
<li>
%if isinstance(item, int):
Zahl: {{item}}
%else:
%try:
Other type: ({{type(item).__name__}}) {{repr(item)}}
%except:
Error: Item has no string representation.
%end try-block (yes, you may add comments here)
%end
</li>
%end
</ul>
</body>
</html>

运行只需要执行即可

python demo.py
1
2
3
4
5
6
7
8
9


## 主程序

Bottle是在Python自带的wsgiref的基础上做了一层包装,主要重心放在处理路由、模板、数据库等部分,具体涉及到网络的部分则交给wsgiref处理。

主程序部分逻辑很简单,判断server是否是ServerAdapter的子类,如果是,则交给server类处理。在server类的run方法中,传入具体处理函数WSGIHandler。WSGIHandler会根据请求的路径再交给相应的函数来处理各个请求。

下面是Bottle这部分源码
def run(server=WSGIRefServer, host='127.0.0.1', port=8080, optinmize = False, **kargs): """ Runs bottle as a web server, using Python's built-in wsgiref implementation by default. You may choose between WSGIRefServer, CherryPyServer, FlupServer and PasteServer or write your own server adapter. """ global OPTIMIZER OPTIMIZER = bool(optinmize) quiet = bool('quiet' in kargs and kargs['quiet']) # Instanciate server, if it is a class instead of an instance if isinstance(server, type) and issubclass(server, ServerAdapter): server = server(host=host, port=port, **kargs) if not isinstance(server, ServerAdapter): raise RuntimeError("Server must be a subclass of ServerAdapter") if not quiet: 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: server.run(WSGIHandler) except KeyboardInterrupt: print "Shuting down..."
1
2
3
4
5


## 路由

Bottle中,添加一条路由规则很简单,只需要在对应该路由规则的处理函数上面加上一个`@route`装饰器即可,如:
@route('/sayhi') def hello_world(): return 'Hello World!'
1
表示,所有请求`/sayhi`的都是函数hello_world来处理。并且,Bottle支持动态路由,下面的示例展示了在Bottle中使用动态路由:
@route('/hello/:name') def hello(name = 'World') return 'Hello {}!'.format(name)
1
2
3
当请求为`/hello/pfchai`时,会返回一个包含'Hello pfchai'页面。

Bottle中,装饰器`route`源码如下,作用就是把路由与该路由对应的处理函数添加到路由处理列表中。
def route(url, **kargs): """ Decorator for request handler. Same as add_route(url, handler).""" def wrapper(handler): add_route(url, handler, **kargs) return handler return wrapper
1
其中,add_route函数源码为:
def add_route(route, handler, method='GET', simple=False): """ Adds a new route to the route mappings. Example: def hello(): return "Hello world!" add_route(r'/hello', hello)""" method = method.strip().upper() if re.match(r'^/(\w+/)*\w*$', route) or simple: ROUTES_SIMPLE.setdefault(method, {})[route] = handler else: route = compile_route(route) ROUTES_REGEXP.setdefault(method, []).append([route, handler])
1
2
3
4
5
6
7
如果是简单路由规则,将该规则添加到ROUTES_SIMPLE中,动态路由则添加到ROUTES_REGEXP中

## 模板

### BaseTemplate类

BaseTemplate类的源码
class BaseTemplate(object): def __init__(self, template='', filename='<template>'): self.source = filename if self.source != '<template>': fp = open(filename) template = fp.read() fp.close() self.parse(template) def parse(self, template): raise NotImplementedError def render(self, **args): raise NotImplementedError @classmethod def find(cls, name): files = [path % name for path in TEMPLATE_PATH if os.path.isfile(path % name)] if files: return cls(filename = files[0]) else: raise TemplateError('Template not found: %s' % repr(name))
1
2
3
4
5
6
7
8
9
10
11
12

Bottle中模板类都是继承BaseTemplate类,BaseTemplate类是一个抽象类,子类必须实现parse和render方法。BaseTemplate类中各个方法的意义:

- parse:解析模板文件中的Python语句
- render:将模板文件渲染成html页面
- find:类方法,查找模板文件



### SimpleTemplate类

SimpleTemplate类的源码:
class SimpleTemplate(BaseTemplate): re_python = re.compile(r'^\s*%\s*(?:(if|elif|else|try|except|finally|for|while|with|def|class)|(include.*)|(end.*)|(.*))') re_inline = re.compile(r'\{\{(.*?)\}\}') dedent_keywords = ('elif', 'else', 'except', 'finally') def parse(self, template): indent = 0 strbuffer = [] code = [] self.subtemplates = {} class PyStmt(str): def __repr__(self): return 'str(' + self + ')' def flush(): if len(strbuffer): code.append(" " * indent + "stdout.append(%s)" % repr(''.join(strbuffer))) code.append("\n" * len(strbuffer)) # to preserve line numbers del strbuffer[:] for line in template.splitlines(True): m = self.re_python.match(line) if m: flush() keyword, include, end, statement = m.groups() if keyword: if keyword in self.dedent_keywords: indent -= 1 code.append(" " * indent + line[m.start(1):]) indent += 1 elif include: tmp = line[m.end(2):].strip().split(None, 1) name = tmp[0] args = tmp[1:] and tmp[1] or '' self.subtemplates[name] = SimpleTemplate.find(name) code.append(" " * indent + "stdout.append(_subtemplates[%s].render(%s))\n" % (repr(name), args)) elif end: indent -= 1 code.append(" " * indent + '#' + line[m.start(3):]) elif statement: code.append(" " * indent + line[m.start(4):]) else: splits = self.re_inline.split(line) # text, (expr, text)* if len(splits) == 1: strbuffer.append(line) else: flush() for i in xrange(1, len(splits), 2): splits[i] = PyStmt(splits[i]) code.append(" " * indent + "stdout.extend(%s)\n" % repr(splits)) flush() self.co = compile("".join(code), self.source, 'exec') def render(self, **args): ''' Returns the rendered template using keyword arguments as local variables. ''' args['stdout'] = [] args['_subtemplates'] = self.subtemplates eval(self.co, args, globals()) return ''.join(args['stdout'])
1
2
3
4
5
6

SimpleTemplate类中,parse方法会首先通过正则把模板内容转换为Python字符串代码,然后再使用compile函数将代码编译成字节码,在调用render方法时,字节码被执行,渲染为包含数据的html页面。

### 全局函数template

template函数源码
def template(template, template_adapter=SimpleTemplate, **args): ''' Returns a string from a template ''' if template not in TEMPLATES: if template.find("\n") == -1 and template.find("{") == -1 and template.find("%") == -1: try: TEMPLATES[template] = template_adapter.find(template) except TemplateNotFoundError: pass else: TEMPLATES[template] = template_adapter(template) if template not in TEMPLATES: abort(500, 'Template not found') args['abort'] = abort args['request'] = request args['response'] = response return TEMPLATES[template].render(**args) ``` template函数根据不同的模板渲染解析引擎,将模板文件渲染成html页面。在template中,每个模板会生成一个模板解析引擎对象,并放在全局变量TEMPLATES中,如果该模板的对象已经存在,直接调用该对象的render方法,如果不存在,创建一个并放入TEMPLATES中,执行render方法。