Hugo Martins

PyInstaller + PyFiglet = Trouble

This essay is a slightly extended version of a comment I made on PyInstaller’s official repository, regarding PyInstaller and PyFiglet integration, as well as some known issues.

PyFiglet allows you to create really large letters out of ordinary text, as per figlet’s own description, which essentially results in interesting displays commonly seen in command line interfaces. One example of this would be:

# From wwww.figlet.org.
 _ _ _          _   _     _     
| (_) | _____  | |_| |__ (_)___ 
| | | |/ / _ \ | __| '_ \| / __|
| | |   <  __/ | |_| | | | \__ \
|_|_|_|\_\___|  \__|_| |_|_|___/

Whenever you want to freeze a Python package with PyInstaller, PyInstaller will run a series of built-in or custom hooks which define what a specific package needs in order to be freezable. PyFiglet doesn’t have a built-in hook in PyInstaller so someone suggested that a hook should be created so that everyone could use PyInstaller to freeze applications that use PyFiglet. This is where things started to become troublesome.

As it appears, PyFiglet uses pkg_resourcesas a way to find out which fonts you would like to use for the displayed text. A runtime hook in PyInstaller - these are different from the pre-runtime hooks mentioned above - is registering pkg_resources.NullProvider which means that everything that uses pkg_resources will forcibly use the NullProvider, triggering the error: NotImplementedError: Can't perform this operation for unregistered loader type. In essence, this means that the registered provider for pkg_resources is incompatible with importing some packages for freezing.

Since, we cannot register a new provider, as the NullProvider will always be registered first, and we also can’t easily change the core of PyInstaller, in order to register a different provider, I’ve come upon a workaround that builds on top of the suggestions in the mentioned discussion.

We start by adding a custom hook to be run by PyInstaller, suggested in the initial issue:

# hook-pyfiglet.py

from PyInstaller.utils.hooks import collect_all
datas, binaries, hiddenimports = collect_all("pyfiglet")

This will force PyInstaller to import all known PyFiglet modules, as well as data. Following that, we need to force registering a new provider, somewhere inside the script that will be frozen, before any use of pyfiglet occurs. An example of this will be:

import pkg_resources
import sys

from pyimod03_importers import FrozenImporter

if getattr(sys, 'frozen', False):
   pkg_resources.register_loader_type(
       FrozenImporter, pkg_resources.DefaultProvider
   )

As I said in my comment, in essence, instead of registering NullProvider, we now register FrozenImporter with DefaultProvider, which inherits from NullProvider anyway - but doesn’t error apparently. I couldn’t figure out any side effects from this but I would advise caution, when using this workaround, as well as thorough testing, to guarantee that there are no side effects that affect the script that is going to be frozen.