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

前兩篇文章主要說明了web.py的模板系統將模板文件處理後獲得的結果:__template__()函數。本文主要講述模板文件是如何變成__template__()函數的。html

Render和frender

通常來講,更經常使用的是Render類,該類會處理整個目錄下的模板,還支持緩存和嵌套模板。不過這些其實都和模板自己的解析基本不要緊,之後再說明這個類的實現和用途。這裏咱們使用frender()函數:node

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

這個函數至關簡單,只做了一鍵事情,就是讀取模板文件內容,而後交給Template類處理,而且返回一個Template類實例。從這裏也能夠看出,整個模板的解析,只和Template類有關,frender是來打雜的。python

Template類

Template實例的效果

當咱們根據一個模板內容建立一個Template類實例t後,咱們能夠調用該實例,這至關於調用模板對應的__template__()函數,獲得的結果是一個TemplateResult實例。web

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__()函數中的步驟差不都差很少。express

# 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__()函數的執行環境,而且實現可調用功能。oop

其餘沒有說明的代碼,暫時均可以忽略,不會影響你理解Template的實例化過程。從上面的步驟能夠看出,Template實例化的過程主要有兩個:生成__template__()函數的代碼並編譯,以及建立__template__()函數的執行環境。ui

生成__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__()函數。

相關文章
相關標籤/搜索