I have an application which is bundled with pyinstaller. Now a new feature request is, that parts are compiled with cyphon to c libraries.
After the compilation inside the activated virtual environment (poetry) the app runs as expected.
BUT, when I bundle it with pyinstaller the executable afterwards can't find packages which are not imported in the main.py file.
With my understanding, this is totally fine, because the Analysis stage of the pyinstaller can't read the conntent of the compiled c code ( In the following example modules/test/test.py
which is available for the pyinstaller as modules/test/test.cpython-311-x86_64-linux-gnu.so
).
├── compile_with_cython.py
├── main.py
├── main.spec
├── main_window.py
├── poetry.lock
└── pyproject.toml
import sys
from PySide6.QtWidgets import QApplication
from main_window import MainWindow
if __name__ == '__main__':
app = QApplication(sys.argv)
mainWin = MainWindow()
mainWin.show()
sys.exit(app.exec_())
MVP PySide6 Application which uses tomllib to load some toml file
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QDialog, QVBoxLayout, QTextEdit
from PySide6.QtCore import Slot
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
...
./main
Traceback (most recent call last):
File "main.py", line 12, in <module>
File "modules/test/test.py", line 3, in init modules.test.test
ModuleNotFoundError: No module named 'tomllib'
[174092] Failed to execute script 'main' due to unhandled exception!
The main problem pyinstaller faces is that it can't follow imports of files/modules compiled by cython. Therefore, it can only resolve and package files & libraries named in main.py
, but not in main_window.py
. To make it work, we need to specify all imports that are hidden from pyinstaller.
I have found two suitable solutions for using pyinstaller with cython compiled binaries.
Add any import needed by any script to the main python file, e.g:
# imports needed by the main.py file
import argparse
import logging
import sys
import time
# dummy imports (needed by the main_window.py file)
import tomllib
import pydantic
This will work, but is only suitable for small projects. Moreover the stated imports will be deleted by various linters because the imports are not really used by this file...
I found the following in the pyinstaller documentation, to get it to work I changed my `.spec' file as follows:
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=['tomllib', 'pydantic'],
Since the code above was clearly just an example, and I had a project with hundreds of Python files and libraries, I came up with the following code to automatically generate the contents of the `hiddenimports' variable each time the pipeline builds the package:
def find_all_hidden_imports(directory_path: Path) -> set:
imports_set = set()
for file_path in directory_path.rglob('*.py'):
if ".venv" not in str(file_path):
imports_set.update(get_imports_of_file(file_path))
return imports_set
def get_imports_of_file(file_path: Path) -> set:
imports_set = set()
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
try:
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for name in node.names:
imports_set.add(name.name)
elif isinstance(node, ast.ImportFrom):
if node.module is not None:
imports_set.add(node.module)
except SyntaxError:
print(f"Syntax error in file: {file_path}")
return imports_set
This set is then converted to the correct list format string and this string is then inserted into the current .spec
file...