Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unable to Include Jinja2 Template to Pyinstaller Distribution

I have a python script that uses Jinja2 template, and I'm trying to create a one-folder distribution using Pyinstaller.

In Jinja, I'm letting the program understand the location of the templates by using a PackageLoader class. The code below shows that it's pointing to my templates folder under pycorr Python package.

env = Environment(loader=PackageLoader('pycorr', 'templates'))
template = env.get_template('child_template.html')

And here's what my folder structure looks like:

pycorr
| |
| + templates
|    |
|    + base.html
|    + child.html

When I compile the package into a single folder using Pyinstaller, I don't see any warning/error related to Jinja2, and I'm able to start the .exe file. However when the program start to look for Jinja2 template, it fails with this error message displayed on the console window:

Traceback (most recent call last):
  ...
  File "C:\Users\ ... \out00-PYZ.pyz\pycorr.WriterToHTML", line 96, in htmlout_table
  File "C:\Users\ ... \out00-PYZ.pyz\pycorr.WriterToHTML", line 13, in __init__
  File "C:\Users\ ... \out00-PYZ.pyz\pycorr.WriterToHTML", line 48, in __set_template
  File "C:\Users\ ... \out00-PYZ.pyz\jinja2.environment", line 791, in get_template
  File "C:\Users\ ... \out00-PYZ.pyz\jinja2.environment", line 765, in _load_template
  File "C:\Users\ ... \out00-PYZ.pyz\jinja2.loaders", line 113, in load
  File "C:\Users\ ... \out00-PYZ.pyz\jinja2.loaders", line 224, in get_source
  File "C:\Users\ ... \dist\OCA_CO~1\eggs\setuptools-14.3-py2.7.egg\pkg_resources\__init__.py", line 1572, in has_resource 
    return self._has(self._fn(self.module_path, resource_name))
  File "C:\Users\ ... \dist\OCA_CO~1\eggs\setuptools-14.3-py2.7.egg\pkg_resources\__init__.py", line 1627, in _has
    "Can't perform this operation for unregistered loader type"
  NotImplementedError: Can't perform this operation for unregistered loader type

I don't really understand the error message, but my guess is that Pyinstaller need to find the templates folder. So I added these lines in the Pyinstaller .spec file:

a.datas += [('BASE', './pycorr/templates/base.html', 'DATA')]
a.datas += [('TABLE', './pycorr/templates/table_child.html', 'DATA')]
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=None,
               upx=False,
               name='OCA_correlation')

But it doesn't seems to solve the issue.

Can anyone help? I read up the Pyinstaller manual several times but I just can't figure it out.

like image 563
cahoy Avatar asked Jul 07 '15 04:07

cahoy


Video Answer


2 Answers

I encountered a similar Jinja2 error when trying to render a Pandas DataFrame to html from within a PyInstaller distribution using the code,

html = df.style.render()

I resolved the issue by modifying the package loader instruction.

In the Pandas style file: site-packages\pandas\io\formats\style.py

I replaced,

loader = PackageLoader("pandas", "io/formats/templates")   

With,

if getattr(sys, 'frozen', False):
    # we are running in a bundle
    bundle_dir = sys._MEIPASS
    loader = FileSystemLoader(bundle_dir)
else:
    loader = PackageLoader("pandas", "io/formats/templates")

And a corresponding import at the top of the file,

import sys

Now, if the program is 'frozen' then the loader will look for the template in the bundle directory. In which case, the final step was to add the template to the bundle. To do this I ran PyInstaller from the command line with the --add-data command. For example, a command similar to that below adds the default template html.tpl,

pyinstaller --add-data PATH1\site-packages\pandas\io\formats\templates\html.tpl;.  PATH2\Main.py
like image 50
ChrisL Avatar answered Oct 05 '22 07:10

ChrisL


I ran into this problem when building a GUI use pyinstaller. I used Jinja2 to render a report and the templates did not load, instead I received "unregistered loader type" error as well. Reading and testing many solutions online I finally had a fix: FileSystemLoader has to be used instead of PackageLoader. Also a file path needs to be provided for the FileSystemLoader. My final solution is a combination of information from here and here.

The following provide a complete example for this solution. My code is under testjinjia2 with templates in sub-directory templates:

testjinja2
| |
| + templates
|    |
|    + base.html
|    + report.html
testreport.py
testreport.spec

In testreport.spec:

# -*- mode: python -*-

block_cipher = None


a = Analysis(['E:\\testjinja2\\testreport.py'],
             pathex=['E:\\testjinja2'],
             binaries=[],
             datas=[('E:\\testjinja2\\templates\\base.html', '.'),
                    ('E:\\testjinja2\\templates\\report.css', '.'),
                    ('E:\\testjinja2\\templates\\report.html', '.')],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name='testreport',
          debug=False,
          strip=False,
          upx=True,
      console=True )

In testreport.py,

import os
import sys
from jinja2 import Environment, PackageLoader, FileSystemLoader


def resource_path(relative_path, file_name):
    """ Get absolute path to resource, works for both in IDE and for PyInstaller """
    # PyInstaller creates a temp folder and stores path in sys._MEIPASS
    # In IDE, the path is os.path.join(base_path, relative_path, file_name)
    # Search in Dev path first, then MEIPASS
    base_path = os.path.abspath(".")
    dev_file_path = os.path.join(base_path, relative_path, file_name)
    if os.path.exists(dev_file_path):
        return dev_file_path
    else:
        base_path = sys._MEIPASS
        file_path = os.path.join(base_path, file_name)
        if not os.path.exists(file_path):
            msg = "\nError finding resource in either {} or {}".format(dev_file_path, file_path)
            print(msg)
            return None
        return file_path

class Report:

    def main(self, output_html_file):
        # template_loader = PackageLoader("report", "templates")
        # --- PackageLoader returns unregistered loader problem,  use FileSystemLoader instead
        template_file_name = 'report.html'
        template_file_path = resource_path('templates', template_file_name)
        template_file_directory = os.path.dirname(template_file_path)
        template_loader = FileSystemLoader(searchpath=template_file_directory)
        env = Environment(loader=template_loader)  # Jinja2 template environment
        template = env.get_template(template_file_name)
        report_content_placeholder = "This is my report content placeholder"
        html = template.render(report_content= report_content_placeholder)
        with open(output_html_file, 'w') as f:
            f.write(html)

if __name__ == "__main__":
    my_report = Report()
    my_report.main("output.html")

A method resource_path is needed because the file path to jinja templates files are different in my IDE and the extracted files from the exe file.

Also just some simple template files for you to try on.
base.html

<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>

    <style>
        .centered {
            text-align: center;
        }
        .centeredbr {
            text-align: center;
            page-break-before:always;
        }

        .underlined {
            text-decoration: underline;
        }

    </style>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>

Report html

<!DOCTYPE html>
{% extends "base.html" %}

{% block body %}
<h1 class="centered underlined">Report Title</h1>

<h2 class="centeredbr">Chaper I</h2>

<p>{{ report_content }}</p>

{% endblock %}

I am using pyinstaller 3.2.1 and Python 3.5.1 Anaconda Custom (64-bit)

like image 44
Uynix Avatar answered Oct 05 '22 08:10

Uynix