Как заставить 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)

Кроме того, вот предложения:

  1. создать новое направление работы и скопировать мой демонстрационный код как __main__.py и повторите команды для создания нового zipapp.

  2. Не пытайся print(sys.modules["templates"].__file__) перед pkgutil.get_data('templates', name), модуль не загружается утилитой pkgutil.get_data от importlib.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__)

Другие вопросы по тегам