Python 如何使Flask/Jinja2在可执行的zip存档中加载捆绑模板?

Python 如何使Flask/Jinja2在可执行的zip存档中加载捆绑模板?,python,flask,jinja2,zipapp,Python,Flask,Jinja2,Zipapp,我已经将我的Flask web应用程序打包到一个可执行的Python压缩存档()中。我在加载模板时遇到问题。Flask/Jinja2无法找到模板 为了加载模板,我使用了jinja2.FunctionLoader和加载函数,该函数应该能够从可执行zip存档(参考:)中读取捆绑文件(在本例中为Jinja模板)。但是,加载函数无法找到模板(请参阅代码中的:(1)),即使可以从加载函数外部读取模板(请参阅代码中的:(2)) 以下是目录结构: └── src ├── __main__.py

我已经将我的Flask web应用程序打包到一个可执行的Python压缩存档()中。我在加载模板时遇到问题。Flask/Jinja2无法找到模板

为了加载模板,我使用了
jinja2.FunctionLoader
和加载函数,该函数应该能够从可执行zip存档(参考:)中读取捆绑文件(在本例中为Jinja模板)。但是,加载函数无法找到模板(请参阅代码中的:
(1)
),即使可以从加载函数外部读取模板(请参阅代码中的:
(2)

以下是目录结构:

└── src
    ├── __main__.py
    └── templates
        ├── index.html
        └── __init__.py  # Empty file.
src/\uuuuuu main\uuuuuuuuu.py

import pkgutil
import jinja2
from flask import Flask, render_template


def load_template(name):
    # (1) ATTENTION: this produces an error. Why?
    # Error message:
    #   FileNotFoundError: [Errno 2] No such file or directory: 'myapp'
    data = pkgutil.get_data('templates', name)
    return data

# (2) ATTENTION: Unlike (1), this successfully found and read the template file. Why?
data = pkgutil.get_data('templates', 'index.html')
print(data)
# This also works:
data = load_template('index.html')
print(data)
# Why?

app = Flask(__name__)
app.config['SECRET_KEY'] = 'my-secret-key'
app.jinja_loader = jinja2.FunctionLoader(load_template)  # <-


@app.route('/')
def index():
    return render_template('index.html')


app.run(host='127.0.0.1', port=3000)
错误是由代码中的
(1)
引起的。作为调试工作的一部分,我添加了
(2)
,以检查是否可以在文件的全局范围内找到模板。令人惊讶的是,它成功地找到并读取了模板文件

是什么解释了
(1)
(2)
之间的行为差异?更重要的是,我如何让Flask在可执行的Python zip存档中找到与Flask应用程序捆绑在一起的Jinja模板


(linux上的Python版本:3.6.8;Flask版本:1.1.1)

jinja2.FunctionLoader(load_template)
正在寻找一个函数以unicode字符串形式返回完整的
index.html
模板。根据:

一种加载程序,它被传递一个进行加载的函数。函数接收模板的名称,并且必须返回带有模板源的unicode字符串、格式为(源、文件名、uptodatefunc)的元组,或者如果模板不存在,则返回None

pkgutil.get_data('templates',name)
不返回unicode字符串,而是返回bytes对象。要解决这个问题,您应该使用
pkgutil.get_data('templates',name.).decode('utf-8')

def load_模板(名称):
"""
从templates文件夹加载文件并以字符串形式返回文件内容。
请参阅jinja2.FunctionLoader文档。
"""
返回pkgutil.get_数据('templates',name)。解码('utf-8')
这意味着第(2)部分可以正常工作,因为代码正在将
index.html
打印为字节对象。Print可以处理bytes对象,它看起来几乎与控制台上的字符串相同。但是,第(1)部分中的代码将失败,因为它被馈送到需要字符串的
jinja2.FunctionLoader
。第(1)部分失败,我的
ValueError


我怀疑,由于您的错误消息是一个
FileNotFoundError
,并将
myapp
作为文件调用,因此您文章的这一部分与您的应用程序不完全匹配。我在Windows 10和Ubuntu Server 18.04以及Python 3.6和3.7上完全复制了这些说明,除了需要使用
解码
之外,没有其他问题。我确实偶尔在Ubuntu上遇到
权限错误
,这要求我运行
sudo python3 myapp

一旦你更新了你的代码,你应该更新你的
myapp
。 然后你会发现
pkgutil.get_data('templates',name)
始终返回上下文数据

当我尝试您的原始代码时,它失败了,因为
ValueError:太多值无法解压缩(预期为3)
,这是由
app.jinja\u loader=jinja2.FunctionLoader(加载模板)生成的

在我看来,最好的方法是自定义自己的
渲染模板
,如下所示:

import pkgutil
import jinja2
from flask import Flask

app = Flask(__name__)
app.config['SECRET_KEY'] = 'my-secret-key'
template = jinja2.Environment()


def load_template(name):
    data = pkgutil.get_data('templates', name)
    return data


def render_template(name, **kwargs):
    data = load_template(name).decode()
    tpl = template.from_string(data)
    return tpl.render(**kwargs)


@app.route('/')
def index():
    return render_template('index.html')


app.run(host='127.0.0.1', port=3000)

此外,以下是建议:

  • 创建一个新的工作方向,将我的演示代码复制为
    \uuuu main\uuuuuuu.py
    ,然后重新执行命令以创建新的Zipap

  • pkgutil.get\u data('templates',name)
    之前,不要尝试
    print(sys.modules[“templates”]。\uuuuu file\uuuu)
    打印(sys.modules[“templates”]。
    importlib.util.find\u spec

  • pkgutil.get_data('templates',name)
    之后,您可能会调试并获取
    sys.modules[“templates”]。\uuuuuu文件\uuuuuuuuu==“myapp/templates/\uuuuuu init\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu

  • 在zipapp中,
    myapp/templates/
    不是目录,因此如果您试图设置
    app.template\u folder=os.path.dirname(sys.modules[“templates”]。\u文件\u)


  • 非常感谢。你知道问题代码中所示的
    (1)
    (2)
    之间的区别是什么吗?我做了一些测试,意识到我对pkgutil的理解是错误的。它应该与Zipap兼容。我还测试了第(1)部分和第(2)部分,并在添加了
    解码('utf-8')
    后成功运行了这两个部分。没有
    解码
    ,我收到了(1)的错误消息,但它与您的不同。有趣。在我的例子中,
    (1)
    失败,出现
    FileNotFoundError:[Errno 2]没有这样的文件或目录:'myapp'
    ,而
    (2)
    成功运行。您使用的是哪个操作系统、Python版本、Flask版本?Python 3.6、Flask 1.1.1、Windows 10。我在没有使用decode的情况下得到了一个
    ValueError
    。如果我试图呈现的不是
    index.html
    ,我只会得到一个
    FileNotFoundError
    。在您的代码中,是否有
    render_模板('index.html')
    verbatim?或者您是否可以将
    myapp
    输入到
    render\u template
    ?我尝试了您的代码,但我的原始错误仍然出现:
    FileNotFoundError:[Errno 2]没有这样的文件或目录:“myapp”
    。回溯表明它是由
    data=pkgutil.get_data('templates',name)
    引起的。您是否重新运行了
    python3-m zipapp src/-o myapp
    ?是的
    rm myapp
    python3-m zipapp src/-o myapp
    。您是否保留了“templates/\uu init\uuuuuu.py”是的
    templates/_init__.py
    templates/index.html
    都存在。
    import pkgutil
    import jinja2
    from flask import Flask
    
    app = Flask(__name__)
    app.config['SECRET_KEY'] = 'my-secret-key'
    template = jinja2.Environment()
    
    
    def load_template(name):
        data = pkgutil.get_data('templates', name)
        return data
    
    
    def render_template(name, **kwargs):
        data = load_template(name).decode()
        tpl = template.from_string(data)
        return tpl.render(**kwargs)
    
    
    @app.route('/')
    def index():
        return render_template('index.html')
    
    
    app.run(host='127.0.0.1', port=3000)