7

I have packaged my Flask web application into an executable Python zipped archive (zipapp). I am having problems with the loading of templates. Flask/Jinja2 is unable to find the templates.

To load templates, I used jinja2.FunctionLoader with a loading function that should have been able to read bundled files (in this case, Jinja templates) from inside the executable zip archive (reference: python: can executable zip files include data files?). However, the loading function is unable to find the template (see: (1) in the code), even though the template is readable from outside the loading function (see: (2) in the code).

Here's the directory structure:

└── src
    ├── __main__.py
    └── templates
        ├── index.html
        └── __init__.py  # Empty file.

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)

To produce the executable archive, I installed all dependencies into src/ (using pip3 install wheel flask --target src/), then I ran python3 -m zipapp src/ -o myapp to produce the executable archive itself. I then ran the executable archive using python3 myapp. Unfortunately, trying to access the the index page through a web browser results in an error:

# ...
  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'

The error is caused by (1) in the code. As part of my debugging effort, I added (2) to check whether or not the template could be found in the global scope of the file. Surprisingly, it successfully finds and reads the template file.

What accounts for the difference in behavior between (1) and (2)? More importantly, how can I make Flask find Jinja templates that are bundled together with a Flask app inside an executable Python zip archive?

(Python version: 3.6.8 on linux; Flask version: 1.1.1)

4

2 回答 2

0

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

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

pkgutil.get_data('templates', name)不返回 unicode 字符串,而是返回字节对象。要解决此问题,您应该使用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') 

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

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

于 2020-02-25T03:19:45.477 回答
0

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

当我尝试您的原始代码时,它失败了ValueError: too many values to unpack (expected 3),因为它是由app.jinja_loader = jinja2.FunctionLoader(load_template) .

在我看来,最好的方法是render_template像这样自定义你自己的:

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)

此外,以下是建议:

  1. 制定新的工作方向,并将我的演示代码复制为__main__.py并重做命令以创建新的 zipapp。

  2. print(sys.modules["templates"].__file__)之前不要尝试pkgutil.get_data('templates', name),模块不是由 util 加载pkgutil.get_dataimportlib.util.find_spec

  3. 之后pkgutil.get_data('templates', name),您可能会调试并获得 sys.modules["templates"].__file__ == "myapp/templates/__init__.py"

  4. 在 zipapp 中,myapp/templates/不是目录,因此如果您尝试设置它将不起作用app.template_folder = os.path.dirname(sys.modules["templates"].__file__)

于 2020-03-02T21:35:32.463 回答