Как заставить Flask / Jinja2 загружать связанные шаблоны в исполняемый zip-архив? - PullRequest
5 голосов
/ 23 февраля 2020

Я упаковал мое Flask веб-приложение в исполняемый 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 в исполняемом Python zip-архиве?

(Python версия: 3.6.8 для linux; Flask версия: 1.1.1)

Ответы [ 2 ]

0 голосов
/ 03 марта 2020

Как только вы обновите свой код, вы должны обновить myapp. И тогда вы обнаружите, что pkgutil.get_data('templates', name) всегда возвращает данные контекста.

Пока я пробовал ваш оригинальный код, он не удался из-за ValueError: too many values to unpack (expected 3), который был сгенерирован app.jinja_loader = jinja2.FunctionLoader(load_template) .

* 1008. * На мой взгляд, лучший способ - настроить свой собственный 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. make новое направление работы, скопируйте мой демонстрационный код как __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__)

0 голосов
/ 25 февраля 2020

jinja2.FunctionLoader(load_template) ищет функцию, которая возвращает полный шаблон index.html в виде строки Unicode. Согласно jinja2 документам :

Загрузчику, которому передана функция, выполняющая загрузку. Функция получает имя шаблона и должна возвращать либо строку Юникода с источником шаблона, кортеж в форме (источник, имя файла, uptodatefun c) или None, если шаблон не существует.

pkgutil.get_data('templates', name) не возвращает строку в юникоде, вместо этого он возвращает объект в байтах. Чтобы исправить это, вы должны использовать 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') 

Это означает, что part (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.

...