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>

运行只需要执行即可

1
python demo.py

主程序

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

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

下面是Bottle这部分源码

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
27
28
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..."

路由

Bottle中,添加一条路由规则很简单,只需要在对应该路由规则的处理函数上面加上一个@route装饰器即可,如:

1
2
3
@route('/sayhi')
def hello_world():
return 'Hello World!'

表示,所有请求/sayhi的都是函数hello_world来处理。并且,Bottle支持动态路由,下面的示例展示了在Bottle中使用动态路由:

1
2
3
@route('/hello/:name')
def hello(name = 'World')
return 'Hello {}!'.format(name)

当请求为/hello/pfchai时,会返回一个包含’Hello pfchai’页面。

Bottle中,装饰器route源码如下,作用就是把路由与该路由对应的处理函数添加到路由处理列表中。

1
2
3
4
5
6
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

其中,add_route函数源码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
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])

如果是简单路由规则,将该规则添加到ROUTES_SIMPLE中,动态路由则添加到ROUTES_REGEXP中

模板

BaseTemplate类

BaseTemplate类的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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))

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

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

SimpleTemplate类

SimpleTemplate类的源码:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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'])

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

全局函数template

template函数源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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方法。