Как заставить Flask/Jinja2 загружать связанные шаблоны в исполняемый zip-архив?
Я упаковал свое веб-приложение Flask в исполняемый zip-архив Python ( zipapp). У меня проблемы с загрузкой шаблонов. Flask/Jinja2 не может найти шаблоны.
Для загрузки шаблонов я использовал jinja2.FunctionLoader
с функцией загрузки, которая должна была иметь возможность читать связанные файлы (в данном случае шаблоны Jinja) из исполняемого zip-архива (ссылка: python: могут ли исполняемые zip-файлы включать файлы данных?). Однако функция загрузки не может найти шаблон (см.:(1)
в коде), даже если шаблон доступен для чтения извне функции загрузки (см.: (2)
в коде).
Вот структура каталогов:
└── 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)
Для создания исполняемого архива я установил все зависимости в 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'
Ошибка вызвана (1)
в коде. В рамках моих усилий по отладке я добавил(2)
чтобы проверить, можно ли найти шаблон в глобальной области действия файла. Удивительно, но он успешно находит и читает файл шаблона.
Чем объясняется разница в поведении между (1)
а также (2)
? Что еще более важно, как я могу заставить Flask находить шаблоны Jinja, которые связаны вместе с приложением Flask внутри исполняемого zip-архива Python?
(Версия Python: 3.6.8 в Linux; версия Flask: 1.1.1)
2 ответа
jinja2.FunctionLoader(load_template)
ищет функцию для возврата полного index.html
шаблон в виде строки Юникода. Согласно документации jinja2:
Загрузчик, которому передана функция, выполняющая загрузку. Функция получает имя шаблона и должна возвращать либо строку Unicode с источником шаблона, либо кортеж в форме (источник, имя файла, 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.FunctionLoader
который ожидает строку. Часть (1) не удалась из-заValueError
для меня.
Я подозреваю, что поскольку ваше сообщение об ошибке FileNotFoundError
и взывает myapp
как файл, эта часть вашего сообщения не совсем соответствует вашему приложению. Я точно воспроизвел инструкции как для Windows 10, так и для Ubuntu Server 18.04, а также для Python 3.6 и 3.7, и у меня не было проблем, кроме необходимости использования.decode
. Я иногда сталкивалсяPermissionErrors
на Ubuntu, что потребовало от меня запуска sudo python3 myapp
.
После обновления кода вам следует обновить 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)
Кроме того, вот предложения:
создать новое направление работы и скопировать мой демонстрационный код как
__main__.py
и повторите команды для создания нового zipapp.Не пытайся
print(sys.modules["templates"].__file__)
передpkgutil.get_data('templates', name)
, модуль не загружается утилитойpkgutil.get_data
отimportlib.util.find_spec
После
pkgutil.get_data('templates', name)
, вы можете отладить и получитьsys.modules["templates"].__file__ == "myapp/templates/__init__.py"
В zipapp,
myapp/templates/
не является каталогом, поэтому он не сработает, если вы попытаетесь установитьapp.template_folder = os.path.dirname(sys.modules["templates"].__file__)