web.py源码分析: 模板(3)

 阅读约 26 分钟

前两篇文章主要说明了web.py的模板系统将模板文件处理后得到的结果:__template__()函数。本文主要讲述模板文件是如何变成__template__()函数的。

Render和frender

一般来说,更常用的是Render类,该类会处理整个目录下的模板,还支持缓存和嵌套模板。不过这些其实都和模板本身的解析基本没关系,以后再说明这个类的实现和用途。这里我们使用frender()函数:

def frender(path, **keywords):
    """Creates a template from the given file path.
    """
    return Template(open(path).read(), filename=path, **keywords)

这个函数相当简单,只作了一键事情,就是读取模板文件内容,然后交给Template类处理,并且返回一个Template类实例。从这里也可以看出,整个模板的解析,只和Template类有关,frender是来打杂的。

Template类

Template实例的效果

当我们根据一个模板内容创建一个Template类实例t后,我们可以调用该实例,这相当于调用模板对应的__template__()函数,得到的结果是一个TemplateResult实例。

In [7]: t = web.template.frender("templates/hello.html")
# coding: utf-8
def __template__ (name):
    __lineoffset__ = -4
    loop = ForLoop()
    self = TemplateResult(); extend_ = self.extend
    extend_([u'hello, ', escape_(name, True), u'\n'])

    return self

In [8]: print t("xxxx")
hello, xxxx


In [9]: print type(t("xxxx"))

<class 'web.template.TemplateResult'>

Template实例化过程

Template实例化过程是把模板转换成HTML内容的实质性步骤,不过这个过程比较复杂。但是,概括的来讲,这个过程和Template的__init__()函数中的步骤差不都差不多。

# Template类的__init__()函数
def __init__(self, text, filename='<template>', filter=None, globals=None, builtins=None, extensions=None):
    self.extensions = extensions or []
    text = Template.normalize_text(text)
    code = self.compile_template(text, filename)

    _, ext = os.path.splitext(filename)
    filter = filter or self.FILTERS.get(ext, None)
    self.content_type = self.CONTENT_TYPES.get(ext, None)

    if globals is None:
        globals = self.globals
    if builtins is None:
        builtins = TEMPLATE_BUILTINS

    BaseTemplate.__init__(self, code=code, filename=filename, filter=filter, globals=globals, builtins=builtins)

首先把,参数里除了text以外的参数忽略掉,然后来看下对text的处理过程(text就是模板的内容)。整个过程概括的说有如下步骤:

  • 对text作归一化处理:text = Template.normalize_text(text), 主要换行符统一成\n,删除BOM字符串,将\$替换成$$,这个就是简单的字符串处理。

  • 编译模板得到编译后的Python字节码codecode = self.compile_template(text, filename),code就是之前已经提到过的__template__()函数。

  • 调用父类(BaseTemplate)的__init__()函数:创建__template__()函数的执行环境,并且实现可调用功能。

其他没有说明的代码,暂时都可以忽略,不会影响你理解Template的实例化过程。从上面的步骤可以看出,Template实例化的过程主要有两个:生成__template__()函数的代码并编译,以及创建__template__()函数的执行环境。

生成__template__()函数的代码

这个是模板生成过程中最长最复杂的一段,会应用到Python的token分析功能以及动态编译功能。还记得第一篇里,我们搭建实验环境的时候,修改了web.py源码,在一个地方插入了一行打印语句么?没错,就是这个compile_template()函数,我们现在来看看它是如何生成__template__()函数的代码的。

def compile_template(self, template_string, filename):
    code = Template.generate_code(template_string, filename, parser=self.create_parser())

    def get_source_line(filename, lineno):
        try:
            lines = open(filename).read().splitlines()
            return lines[lineno]
        except:
            return None

    print code  # 上次增加的打印语句
    ...
    

很明显,第一行的调用就生成了__template__()函数的代码。我们继续看:

def generate_code(text, filename, parser=None):
    # parse the text
    parser = parser or Parser()
    rootnode = parser.parse(text, filename)

    # generate python code from the parse tree
    code = rootnode.emit(indent="").strip()
    return safestr(code)

generate_code = staticmethod(generate_code)

def create_parser(self):
    p = Parser()
    for ext in self.extensions:
        p = ext(p)
    return p

这两个函数配合起来使用,意思就是:创建一个没有扩展的Parser实例,用该实例解析模板内容得到rootnode,调用rootnodeemit()函数得到最终的代码(__template__()函数)。所以,问题的关键由转到了Parser类上。

Parser类初探

Parser类的parse函数解析模板内容,然后返回一些节点结构,这种节点结构的emit方法会产生实际的Python代码。

class Parser:
    """Parser Base.
    """
    def __init__(self):
        self.statement_nodes = STATEMENT_NODES
        self.keywords = KEYWORDS

    def parse(self, text, name="<template>"):
        self.text = text
        self.name = name

        defwith, text = self.read_defwith(text)
        suite = self.read_suite(text)
        return DefwithNode(defwith, suite)
    
    ...

上面的代码是Template类中用的Parser类的两个方法,初始化函数没啥好说的,parse函数则是说明了模板解析的最顶层逻辑:

  1. 先解析def with这行。

  2. 然后解析剩余部分。

  3. 最后返回一个DefwithNode实例。

这里不打算细说整个解析过程(后面专门说),反正知道了Parser类的parse函数可以返回一个DefwithNode实例,且调用其emit()方法后能生成最终的代码即可。

编译__template__()函数的代码

compile_template方法得到模板对应的代码后,就要对这个代码进行编译,生成Python的字节码:

def compile_template(self, template_string, filename):
    code = Template.generate_code(template_string, filename, parser=self.create_parser())

    ...
    print code
    try:
        # compile the code first to report the errors, if any, with the filename
        compiled_code = compile(code, filename, 'exec')
    except SyntaxError, e:
        # display template line that caused the error along with the traceback.
    ...
    
    return compiled_code
    

到此就完成了Template类实例化过程的第二个步骤:得到了编译后的模板函数代码。

BaseTemplate

接下来就是要调用父类,也就是BaseTemplate类,的初始化方法了:

BaseTemplate.__init__(self, code=code, filename=filename, filter=filter, globals=globals, builtins=builtins)

该方法接受的参数中,code就是上面编译出来的字节码,globalsbuiltins是用来构建__template__()函数的运行环境的,记得在web.py的文档中有说可以修改这两个的地方么?忘记的话,传送门filter函数是过滤函数,用来处理生成的HTML内容。

globals和builtins

如果你的代码中不指定,默认情况下,globals是空的,builtins则包含如下内容:

TEMPLATE_BUILTIN_NAMES = [
    "dict", "enumerate", "float", "int", "bool", "list", "long", "reversed",
    "set", "slice", "tuple", "xrange",
    "abs", "all", "any", "callable", "chr", "cmp", "divmod", "filter", "hex",
    "id", "isinstance", "iter", "len", "max", "min", "oct", "ord", "pow", "range",
    "True", "False",
    "None",
    "__import__", # some c-libraries like datetime requires __import__ to present in the namespace
]

这些就是Python的全局内置函数的一个子集,你在模板中可以直接使用。

BaseTemplate的初始化

这个初始化过程,我们直接看代码(只列出相关函数):

class BaseTemplate:
    def __init__(self, code, filename, filter, globals, builtins):
        self.filename = filename
        self.filter = filter
        self._globals = globals
        self._builtins = builtins
        if code:
            self.t = self._compile(code)
        else:
            self.t = lambda: ''

    def _compile(self, code):
        env = self.make_env(self._globals or {}, self._builtins)
        exec(code, env)
        return env['__template__']

    def make_env(self, globals, builtins):
        return dict(globals,
            __builtins__=builtins,
            ForLoop=ForLoop,
            TemplateResult=TemplateResult,
            escape_=self._escape,
            join_=self._join
        )

初始化的时候,主要的工作是调用self.t = self._compile(code)_compile()方法先使用make_env()方法构造一个函数运行环境env,里面包含了__template__()函数会用到的对象,包括默认的内置函数,以及TemplateResult等。然后,调用exec(code, env)env环境下执行__template__()函数的定义,最后返回的env['__template']就是在env环境下执行的__template__()函数。

注意:*在python2中,exec是一个语句:

The first expression should evaluate to either a Unicode string, a Latin-1 encoded string, an open file object, a code object, or a tuple.

exec还有下面这种形式:

exec code in  globals, locals

如果以元组作为参数,则有如下两个形式:

exec (code, globals)
exec (code, globals, locals)

globals字典中可以插入__builtins__ 对象来设置内置对象的引用,比如上面的make_env()函数。

Parser类

现在可以来看Parser类是如何解析模板,并生成特定的节点结构的。还是从parse函数开始:

def parse(self, text, name="<template>"):
    self.text = text
    self.name = name

    defwith, text = self.read_defwith(text)
    suite = self.read_suite(text)
    return DefwithNode(defwith, suite)

首先调用read_defwith()方法,将$def with (name)这行分离出来,剩余的都交给read_suite()方法去解析,最后再实例化一个DefwithNode作为解析结果。整个解析的主要工作是由read_suite()方法完成的。

Parser类的实现惯例

解析函数

Parser类里面实现了如下这些方法:

def read_assignment : function
def read_block_section : function
def read_expr : function
def read_indented_block : function
def read_keyword : function
def read_node : function
def read_section : function
def read_statement : function
def read_suite : function
def read_text : function
def read_var : function
def readline : function

这些方法的参数和返回值都遵循同一个模式,先知道一下有助于阅读代码。

  • 每个方法都负责解析特定的内容(方法名可以体现),有的方法内部会调用其他的方法。

  • 参数都是一个text,表示还未解析的模板内容。

  • 返回值都是含有两个元素的元组,第一个元素是该函数解析的结果(字符串或者某个类实例),第二个元素是该函数处理过后还剩余的模板内容(给下个函数去解析)。

从这个惯例可以看出,整个模板解析的思想是:从模板内容头部开始,一点一点的读取,一旦所读取的内容可以解析就进行解析,剩余的内容再继续这么处理

另外,这些解析函数还有包含关系,有一些是处理整块内容的函数,有一些则是处理一行,还有的是处理一个单词,这也就形成了一个调用关系:粗粒度的函数调用细粒度的解析函数。如下图所示:

解析函数调用关系图

解析节点

上一小节提到了,解析函数返回值中的第一个一般都是解析好的节点类实例。那么什么是解析节点呢?解析节点其实就是一些类,这些类都实现了emit()函数,当调用emit()时,就会产生对应的代码字符串。先来看下有哪些解析节点吧:

--
TextNode
ExpressoinNode
LineNode
VarNode
StatementNode
AssignmentNode
BlockNode
IfNode, ElseNode, ElifNode
ForNode
CodeNode
DefNode
SuiteNode
DefwithNode

这些节点的功能基本上从类名称就可以看出来,有些节点只是其他节点的包装(比如LineNode),有一些则需要处理比较复杂的情况(比如ForNode)。但是这些节点作的事情都是可以概括为:处理初始化参数以便在调用emit方法的时候能产生正确的代码。来看两个例子:

AssignmentNode

模板内容

$def with (name)
$ b = name

函数内容

def __template__ (name):
    __lineoffset__ = -4
    loop = ForLoop()
    self = TemplateResult(); extend_ = self.extend
    b = name

    return self

我们知道当解析到$ b = name这行时,会生成一个AssignmentNode,调用read_assignment()的代码在Parser类的read_section()方法里,我们可以模拟一下:

In [16]: node, _ = p.read_assignment(" b = name\n")

In [18]: print node
<assignment: 'b = name'>

In [20]: print node.emit("    ")
    b = name

web.py的模板系统就是这样针对每个节点调用emit()方法来生产最终的代码的。

ForNode

我们再来看复杂一点的ForNode节点。

模板内容

$def with (name_list)
$for name in name_list:
    $name

函数内容

def __template__ (name_list):
    __lineoffset__ = -4
    loop = ForLoop()
    self = TemplateResult(); extend_ = self.extend
    for name in loop.setup(name_list):
        extend_([escape_(name, True), u'\n'])

    return self

web.py中调用创建ForNode实例的方法是read_block_section(),我们可以这么模拟:

In [27]: node = web.template.ForNode("for name in name_list\n", "    $name\n")

In [28]: print node
<block: 'for name in name_list\n', [<line: [t'    ', $name, t'\n']>]>

In [31]: print node.emit("    ")
    for name in loop.setup(name_list):
        extend_([u'    ', escape_(name, True), u'\n'])

创建ForNode节点实例时,是把第一行的循环控制语句和其他的循环内部代码分别作为两个参数传递给ForNode的初始化函数。ForNode的主要工作是解析第一个参数,转换为loop.setup()这样的代码(为了能够在模板中支持loop关键字)。然后,调用父类BlockNode的初始化函数,主要作用是调用Parser类的read_suite()方法解析循环内部的代码,因为我们在循环内部也会使用模板语法(就像上面例子中的$name\n被转换成extend_([u' ', escape_(name, True), u'\n']))。

class BlockNode:
    def __init__(self, stmt, block, begin_indent=''):
        self.stmt = stmt
        self.suite = Parser().read_suite(block)
        self.begin_indent = begin_indent

    def emit(self, indent, text_indent=''):
        text_indent = self.begin_indent + text_indent
        out = indent + self.stmt + self.suite.emit(indent + INDENT, text_indent)
        return out

    def __repr__(self):
        return "<block: %s, %s>" % (repr(self.stmt), repr(self.suite))

class ForNode(BlockNode):
    def __init__(self, stmt, block, begin_indent=''):
        self.original_stmt = stmt
        tok = PythonTokenizer(stmt)
        tok.consume_till('in')
        a = stmt[:tok.index] # for i in
        b = stmt[tok.index:-1] # rest of for stmt excluding :
        stmt = a + ' loop.setup(' + b.strip() + '):'
        BlockNode.__init__(self, stmt, block, begin_indent)

    def __repr__(self):
        return "<block: %s, %s>" % (repr(self.original_stmt), repr(self.suite))

SuiteNode

SuiteNode内部就是一个列表,保存了所有子节点,调用SuiteNode的emit()方法时,就是依次调用子节点的emit()方法,然后连接成一个字符串:

def emit(self, indent, text_indent=''):
    return "\n" + "".join([s.emit(indent, text_indent) for s in self.sections])

DefwithNode

DefwithNode作为根节点,做了两个事情:

  • 生成__template__()函数的框架。

  • 调用suite.emit()生成其余的代码,并且拼接得到一个完整的函数。

Parser类小结

本章讲解了Parser类的大部分实现,但是没有讲解Parser类如何分析模板内容来确定生成哪个节点。这部分的内容都是具体的实现细节,采用的分析技术主要有两种:

  • 字符串分析,看看字符串是否以某些特定模式开始。

  • token分析,利用了Python的tokenize模块的功能来分析。

总结

本文主要是分析了从模板文件到Template类实例的生成过程,大概是如下几个步骤:

  • 调用frender()函数读取模板文件内容,作为参数传递给Template类的初始化函数。

  • Template类调用Parser类将模板内容解析成__template__()函数的定义代码。

  • Template类调用Python的compile函数将生成的__template__()定义代码进行编译。

  • Template类调用父类BaseTemplate类的初始化函数构建__template__()函数的执行环境,得到在指定环境下执行的__template__()函数。

阅读 3k更新于 2015-11-06
推荐阅读
Coffee, Coke and Code
用户专栏

79 人关注
30 篇文章
专栏主页
目录