Flask Templates源码学习

Flask中的模版是基于jinja2.所以首先学习一下jinja2当中的基本概念.

Part 1 Basics of Jinja2


暂时略

Part 2 demo.py


from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    name = 'Saltriver'
    return render_template('hello.html', name=name)

Part 3


在初始化Flask实例过程中:

    def __init__(self, import_name, static_path=None, static_url_path=None,
                 static_folder='static', template_folder='templates',
                 instance_path=None, instance_relative_config=False,
                 root_path=None):
        _PackageBoundObject.__init__(self, import_name,
                                     template_folder=template_folder,
                                     root_path=root_path)

首先会初始化_PackageBoundObject.从名字上面可以看出,这个对象是与某个包绑定了的.下面来看下:

    def __init__(self, import_name, template_folder=None, root_path=None):
        #: The name of the package or module.  Do not change this once
        #: it was set by the constructor.
        self.import_name = import_name

        #: location of the templates.  ``None`` if templates should not be
        #: exposed.
        self.template_folder = template_folder

        if root_path is None:
            root_path = get_root_path(self.import_name)

        #: Where is the app root located?
        self.root_path = root_path

        self._static_folder = None
        self._static_url_path = None

_PackageBoundObject中的init函数还是比较简单的.设置了import_name, root_path. 其中template_folder, _static_folder, _static_url_path为None.
在Flask的init函数中,设置了static_folder.

下面看render_template函数:

def render_template(template_name_or_list, **context):
    
    ctx = _app_ctx_stack.top
    ctx.app.update_template_context(context)   # <-----
    return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list),
                   context, ctx.app)
    def update_template_context(self, context):
       
        # funcs = _default_template_ctx_processor
        funcs = self.template_context_processors[None]
        reqctx = _request_ctx_stack.top
        ...
        orig_ctx = context.copy()
        for func in funcs:
            # func()的效果就是_default_template_ctx_processor().
            # 将g, request, session变量加入到context中.
            context.update(func())
        # 意思是,如果render_template()中的参数有类似于g=x,request=y,session=z
        # 之类的值的话,就优先使用参数中的g.也就是说context['g'] 为参数中的值
        context.update(orig_ctx)

此时context中就包括以下4个key-value:
{'session': <NullSession {}>, 'request': <Request 'http://127.0.0.1:5000/' [GET]>, 'name': 'Saltriver', 'g': <flask.g of 'test_template'>}

接下来是render_template中的_render函数.

    return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list),
                   context, ctx.app)
def _render(template, context, app):

    before_render_template.send(app, template=template, context=context)
    rv = template.render(context)
    template_rendered.send(app, template=template, context=context)
    return rv

首先看template参数是如何获得的:

ctx.app.jinja_env.get_or_select_template(template_name_or_list)

jinja_env是一个locked_cached_property

    @locked_cached_property
    def jinja_env(self):
        return self.create_jinja_environment()

那么,什么是locked_cached_property?

class locked_cached_property(object):
    
    def __init__(self, func, name=None, doc=None):
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__
        self.func = func
        self.lock = RLock()

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        with self.lock:
            value = obj.__dict__.get(self.__name__, _missing)
            if value is _missing:
                value = self.func(obj)
                obj.__dict__[self.__name__] = value
            return value

也就是说jinja_env = locked_cached_property(jinja_env),jinja_env现在是个描述符.
但我们访问jinja_env属性的时候,会调用get方法,obj设置为ctx.app. obj.dict中并没有self.name也就是'jinja_env'属性(type(obj).dict才有这个属性).于是value is _missing. 然后执行原始函数,value = ctx.app.create_jinja_environment() 并设置该属性到ctx.app中. 换句话说,在第一次创建了jinja environment后,app实例中就有了这个属性,以后就不用每次调用一次create_jinja_environment. 加锁是保证多线程安全.下面看create_jinja_environment函数:

    def create_jinja_environment(self):
        
        ...
        # 创建环境.
        rv = self.jinja_environment(self, **options)  # <-----代码段1
        rv.globals.update(
            url_for=url_for,
            get_flashed_messages=get_flashed_messages,
            config=self.config,
            # request, session and g are normally added with the
            # context processor for efficiency reasons but for imported
            # templates we also want the proxies in there.
            request=request,
            session=session,
            g=g
        )
        rv.filters['tojson'] = json.tojson_filter
        return rv

下面看代码段1, jinja_environment就是Enviroment:

class Environment(BaseEnvironment):
    
    def __init__(self, app, **options):
        if 'loader' not in options:
            # 创建loader.
            options['loader'] = app.create_global_jinja_loader()   # <-----
        BaseEnvironment.__init__(self, **options)   # <----
        self.app = app

loader就是DispatchingJinjaLoader,继承自BaseLoader:

class DispatchingJinjaLoader(BaseLoader):
    def __init__(self, app):
        self.app = app
    ...

于是,rv=Environment也创建完毕了.最后更新rv.globals中的值,可以看到环境中的全局变量有url_for, get_flashed_messages, config, request, session, g.这些全局变量可以在模版中直接使用.
rv.filters['tojson']是什么意思呢?
====================================================

ctx.app.jinja_env.get_or_select_template(template_name_or_list)

这段代码的前段部分ctx.app.jinja_env是用来创建Environment.
我们可以查看Environment中的属性.

>>>with app.test_request_context('/'):
...     print env.loader
...     for key in env.globals:
...         print key, env.globals[key], '\n'
>>>...

env.loader为flask.templating.DispatchingJinjaLoader.
我们可以看到env.globals中还有其他有趣的变量: lipsum, dict, cycler, joiner.
Environment创建好之后,我们看看接下来是怎么加载(loader)模版的.
加载模版就是get_or_select_template()函数.我们查看该函数的doc:

Does a typecheck and dispatches to select_template() if 
an iterable of template names is given, otherwise to get_template().

对于‘hello.html’这个模版,该函数会委托给get_template()函数.再看get_template的doc:

Load a template from the loader. If a loader is configuered this
method ask the loader for the template and returns a Template.

换句话说,get_template函数将会使用env.loader来加载模版.
下面我们简单的深入到env.get_template函数中看看:

    def get_template(self, name, parent=None, globals=None):
        ...
        return self._load_template(name, self.make_globals(globals))

其中self.make_globals(globals)就是加入了DEFAULT_NAMESPACE:
DEFAULT_NAMESPACE = {
'range': range_type,
'dict': dict,
'lipsum': generate_lorem_ipsum,
'cycler': Cycler,
'joiner': Joiner
}

下面继续看_load_template函数:

    def _load_template(self, name, globals):
        ...
        # 开始使用self.loader加载模版.
        template = self.loader.load(self, name, globals)   # <-----
        if self.cache is not None:
            self.cache[cache_key] = template
        return template

self.loader也就是DispatchingJinjaLoader,继承自BaseLoader.
下面为了不陷入Jinja2的细节之中而不能自拔.这里我们只是熟悉一下Jinja2.Loader的API.
官方文档已经写得很好了,这里我也只是对照官方文档对重点进行了翻译.
----------Jinja2.Loader API-----------
Loaders负责从一个资源所在地,比如文件系统(file system),加载模版.
Environment会保存编译后的modules保存到内存中,原理类似Python的sys.modules.
不同于sys.modules, Environment中的缓存(cache),会有大小的限制.所有的loaders
都继承BaseLoader.如果你想创建你自己的loader, 请继承BaseLoader然后重写get_source函数.

class jinja2.BaseLoader:
Environment提供了一个get_template方法, # <-----Env调用load与loader调用load的区别.
这个方法会调用loader的load方法来获取Template对象.
不用overide BaseLoader的load方法.load方法会调用get_source方法.
官方文档给出了一个class MyLoader(BaseLoader)的示例.
get_source函数会返回一个元组,元组里面都有什么东西呢?
(source, filename, uptodate)
其中source应该为unicode string 或者 ASCII bytestring.
filename应该为name of file on the filesystem.否则为None.
uptodate是用来被调用,以确定template是否发生变化了.通常是个
lambda函数.


                BaseLoader
                /  /|\
               /  / | \
         MyLoader/  |  \
                /   |  DictLoader 
        FileSystemLoader 
                    |    
                PackageLoader    

在DispatchingJinjaLoader的get_source中,
如果没有设置'EXPLAIN_TEMPLATE_LOADING'参数为True.
就启用_get_source_fast(所谓快速(默认)获取?):

    def _get_source_fast(self, environment, template):
        # loader为FileSystemLoader.
        for srcobj, loader in self._iter_loaders(template):  # 代码段1
            try:
                # 调用FileSystemLoader的get_source函数.
                # get_source函数返回一个元组.
                return loader.get_source(environment, template)  
            except TemplateNotFound:
                continue
        raise TemplateNotFound(template)

在代码段1中:

    def _iter_loaders(self, template):
        loader = self.app.jinja_loader  
        if loader is not None:
            yield self.app, loader

        # 暂时忽略,暂时不涉及到蓝本.
        for blueprint in self.app.iter_blueprints():
            loader = blueprint.jinja_loader
            if loader is not None:
                yield blueprint, loader

_iter_loaders是个生成器.可以看到与蓝本有关的loader就在这里获取.
jinja_loader在_PackageBoundObject中定义,这里就不贴出代码,
self.app.jinja_loader返回FileSystemLoader.

于是DispatchingJinjaLoader执行完毕.
最后_render函数中,发送相应信号,对模版进行渲染.并返回.

--------- QUESTION ----------
Q1:app.create_jinja_environment()函数中rv.globals.update()
中有request, session, g. 同时render_template()函数中
对ctx.app.update_template_context(context)也进行了request,
session, g的更新.这是为什么呢?

A1: 注释已经说了:
# inside the app.create_jinja_environment():
# request, session and g are normally added with the
# context processor for efficiency reasons but for imported
# templates we also want the proxies in there.


Part 4


整个执行框架已经有了,我们再看看其中的一些细节.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容