PyInstaller, как включить файлы данных из внешнего пакета, который был установлен pip?

Проблема

Я пытаюсь использовать PyInstaller для создания приложения для внутреннего использования внутри моей компании. script отлично работает с рабочей средой python, но теряет что-то при переводе в пакет.

Я знаю, как включать и ссылаться на файлы данных, которые мне нужны в моем пакете, но у меня возникают проблемы с включением или ссылкой на файлы, которые должны появляться при импорте.

Я использую пакет, устанавливаемый в пике tk-tools, который включает в себя несколько приятных изображений для панельных дисплеев (выглядит как светодиоды). Проблема в том, что когда я создаю pyinstaller script, в любое время, когда обращается одно из этих изображений, я получаю сообщение об ошибке:

DEBUG:aspen_comm.display:COM23 19200
INFO:aspen_comm.display:adding pump 1 to the pump list: [1]
DEBUG:aspen_comm.display:updating interrogation list: [1]
Exception in Tkinter callback
Traceback (most recent call last):
  File "tkinter\__init__.py", line 1550, in __call__
  File "aspen_comm\display.py", line 206, in add
  File "aspen_comm\display.py", line 121, in add
  File "aspen_comm\display.py", line 271, in __init__
  File "aspen_comm\display.py", line 311, in __init__
  File "lib\site-packages\tk_tools\visual.py", line 277, in __init__
  File "lib\site-packages\tk_tools\visual.py", line 289, in to_grey
  File "lib\site-packages\tk_tools\visual.py", line 284, in _load_new
  File "tkinter\__init__.py", line 3394, in __init__
  File "tkinter\__init__.py", line 3350, in __init__
_tkinter.TclError: couldn't open "C:\_code\tools\python\aspen_comm\dist\aspen_comm\tk_tools\img/led-grey.png": no such file or directory

Я просмотрел этот каталог в последней строке, где находится мой дистрибутив, и обнаружил, что нет каталога tk_tools.

Вопрос

Как мне получить pyinstaller для сбора файлов данных импортированных пакетов?

Файл спецификации

В настоящее время мой datas пуст. Файл спецификации, созданный с помощью pyinstaller -n aspen_comm aspen_comm/__main__.py:

# -*- mode: python -*-

block_cipher = None


a = Analysis(['aspen_comm\\__main__.py'],
             pathex=['C:\\_code\\tools\\python\\aspen_comm'],
             binaries=[],
             datas=[],
             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,
          exclude_binaries=True,
          name='aspen_comm',
          debug=False,
          strip=False,
          upx=True,
          console=True )

coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               name='aspen_comm')

Когда я просматриваю /build/aspen_comm/out00-Analysis.toc и /build/aspen_comm/out00-PYZ.toc, я нахожу запись, которая выглядит так, как будто она найдена в пакете tk_tools. Кроме того, есть функции пакета tk_tools, которые отлично работают, прежде чем перейти к поиску файлов данных, поэтому я знаю, что он импортируется где-то, я просто не знаю, где. Когда я выполняю поиск tk_tools, я не могу найти ссылки на него в структуре файла.

Я также попробовал вариант --hidden-imports с теми же результатами.

Частичное решение

Если я "вручную" добавлю путь к spec файлу с помощью datas = [('C:\\_virtualenv\\aspen\\Lib\\site-packages\\tk_tools\\img\\', 'tk_tools\\img\\')] и datas=datas в Analysis, тогда все будет работать так, как ожидалось. Это будет работать, но я предпочел бы, чтобы PyInstaller нашел данные пакета, так как он четко установлен. Я продолжу искать решение, но на данный момент я, вероятно, воспользуюсь этим неидеальным обходным решением.

Если у вас есть контроль над пакетом...

Затем вы можете использовать stringify в субпакете, но это работает только в том случае, если это ваш собственный пакет.

Ответ 1

Отредактировано для добавления

Чтобы решить эту проблему более надолго, я создал пакет, устанавливаемый в pip stringify, который возьмет файл или каталог и преобразует его в строку python, чтобы такие пакеты, как pyinstaller, распознавали их как родные файлы python.

Просмотрите страницу проекта , обратная связь приветствуется!


Оригинальный ответ

Ответ немного окольный и касается способа компоновки tk_tools, а не pyinstaller.

Кто-то недавно сообщил мне о технике, в которой двоичные данные, такие как данные изображения, могут храниться как строка base64:

with open(img_path, 'rb') as f:
    encoded_string = base64.encode(f.read())

Закодированная строка фактически хранит данные. Если исходный пакет просто хранит файлы пакета как строки вместо файлов изображений и создает файл python с этими данными, доступными как строковые переменные, тогда можно просто включить двоичные данные в пакет в форме, которая pyinstaller найдет и обнаружит без вмешательства.

Рассмотрим приведенные ниже функции:

def create_image_string(img_path):
    """
    creates the base64 encoded string from the image path 
    and returns the (filename, data) as a tuple
    """

    with open(img_path, 'rb') as f:
        encoded_string = base64.b64encode(f.read())

    file_name = os.path.basename(img_path).split('.')[0]
    file_name = file_name.replace('-', '_')

    return file_name, encoded_string


def archive_image_files():
    """
    Reads all files in 'images' directory and saves them as
    encoded strings accessible as python variables.  The image
    at images/my_image.png can now be found in tk_tools/images.py
    with a variable name of my_image
    """

    destination_path = "tk_tools"
    py_file = ''

    for root, dirs, files in os.walk("images"):
        for name in files:
            img_path = os.path.join(root, name)
            file_name, file_string = create_image_string(img_path)

            py_file += '{} = {}\n'.format(file_name, file_string)

    py_file += '\n'

    with open(os.path.join(destination_path, 'images.py'), 'w') as f:
        f.write(py_file)

Если archive_image_files() помещается в установочный файл, тогда <package_name>/images.py автоматически создается при запуске установки script (при создании и установке колес).

Я могу улучшить эту технику в ближайшем будущем. Спасибо всем за вашу помощь,

J

Ответ 2

Следующий код поместит все PNG файлы в ваш каталог в папку на верхнем уровне подключенного приложения под названием imgs:

datas=[("C:\\_code\\tools\\python\\aspen_comm\\dist\\aspen_comm\\tk_tools\\img\\*.png", "imgs")],

Затем вы можете ссылаться на них с помощью os.path.join("imgs", "your_image.png") в своем коде.

Ответ 3

Я решил это, воспользовавшись тем, что spec файл - это код Python, который выполняется. Вы можете получить корень пакета динамически во время фазы сборки PyInstaller и использовать это значение в списке datas. В моем случае у меня есть что-то вроде этого в файле .spec:

import os
import importlib

package_imports = [['package_name', ['file0', 'file1']]

datas = []
for package, files in package_imports:
    proot = os.path.dirname(importlib.import_module(package).__file__)
    datas.extend((os.path.join(proot, f), package) for f in files)

И используйте полученный datas список в качестве параметров Analysis.