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

Flu*_*lux 6 python jinja2 flask zipapp

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

为了加载模板,我使用jinja2.FunctionLoader了一个加载函数,该函数应该能够从可执行 zip 存档内部读取捆绑文件(在本例中为 Jinja 模板)(参考:python:可执行 zip 文件可以包含数据文件吗?)。然而,加载函数无法找到模板(参见:(1)代码中),即使模板可以从加载函数外部读取(参见:(2)代码中)。

这是目录结构:

??? src
    ??? __main__.py
    ??? templates
        ??? index.html
        ??? __init__.py  # Empty file.
Run Code Online (Sandbox Code Playgroud)

src/__main__.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)
Run Code Online (Sandbox Code Playgroud)

为了生成可执行存档,我将所有依赖项安装到src/(使用pip3 install wheel flask --target src/)中,然后运行python3 -m zipapp src/ -o myapp生成可执行存档本身。然后我使用python3 myapp. 不幸的是,尝试通过网络浏览器访问索引页面会导致错误:

# ...
  File "myapp/__main__.py", line 10, in load_template
  File "/usr/lib/python3.6/pkgutil.py", line 634, in get_data
    return loader.get_data(resource_name)
FileNotFoundError: [Errno 2] No such file or directory: 'myapp'
Run Code Online (Sandbox Code Playgroud)

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

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

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

Joe*_*Joe 0

jinja2.FunctionLoader(load_template)正在寻找一个函数来将完整index.html模板作为 unicode 字符串返回。根据jinja2 文档

一个加载器,传递一个执行加载的函数。该函数接收模板的名称,并且必须返回带有模板源的 unicode 字符串、形式为 (source、filename、uptodatefunc) 的元组,如果模板不存在,则返回 None。

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

def load_template(name):
    """
    Loads file from the templates folder and returns file contents as a string.
    See jinja2.FunctionLoader docs.
    """
    return pkgutil.get_data('templates', name).decode('utf-8') 
Run Code Online (Sandbox Code Playgroud)

这意味着第 (2) 部分将正常工作,因为代码作为index.html字节对象打印。Print 可以处理字节对象,它在控制台上看起来几乎与字符串相同。但是,第 (1) 部分中的代码将失败,因为它被馈送到jinja2.FunctionLoader需要字符串的位置。第(1)部分对ValueError我来说失败了。

我怀疑由于您的错误消息是 aFileNotFoundError并作为文件调用myapp,因此您帖子的该部分与您的应用程序不完全匹配。我在 Windows 10 和 Ubuntu Server 18.04 以及 Python 3.6 和 3.7 上准确复制了这些说明,除了需要使用decode. 我偶尔会遇到PermissionErrors需要我运行sudo python3 myapp.