Bottle 源码学习

-
-
2016-06-16

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

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

准备

先从Github上下载Bottle的源码

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

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

cd bottle
git checkout -b 0.4.10

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

Bottle 示例程序

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

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

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

%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
```


## 主程序

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..."
```


## 路由

Bottle中,添加一条路由规则很简单,只需要在对应该路由规则的处理函数上面加上一个`@route`装饰器即可,如:
```
@route('/sayhi')
def hello_world():
    return 'Hello World!'
```
表示,所有请求`/sayhi`的都是函数hello_world来处理。并且,Bottle支持动态路由,下面的示例展示了在Bottle中使用动态路由:
```
@route('/hello/:name')
def hello(name = 'World')
    return 'Hello {}!'.format(name)
```
当请求为`/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
```
其中,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])
```
如果是简单路由规则,将该规则添加到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))
```

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'])
```

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方法。

“您的支持是我持续分享的动力”

微信收款码
微信
支付宝收款码
支付宝

目录