Search code examples
pythonpython-3.xpyqt5pyinstallerqprocess

Convert multiple .py file to .exe


I am trying to convert my python program to an executable (.exe) file using PyInstaller, I have 2 python fils: dummy_script.py and GUI.py.

Basically, GUI.py consists of a button that executes dummy_script.py.

dummy_script.py:

import sys
import time


def flush_then_wait():
    sys.stdout.flush()
    sys.stderr.flush()
    time.sleep(0.5)


sys.stdout.write("Script stdout 1\n")
sys.stdout.write("Script stdout 2\n")
sys.stdout.write("Script stdout 3\n")
sys.stderr.write("Total time: 00:05:00\n")
sys.stderr.write("Total complete: 10%\n")
flush_then_wait()

sys.stdout.write("name=Martin\n")
sys.stdout.write("Script stdout 4\n")
sys.stdout.write("Script stdout 5\n")
sys.stderr.write("Total complete: 30%\n")
flush_then_wait()

sys.stderr.write("Elapsed time: 00:00:10\n")
sys.stderr.write("Elapsed time: 00:00:50\n")
sys.stderr.write("Total complete: 50%\n")
sys.stdout.write("country=Nederland\n")
flush_then_wait()

sys.stderr.write("Elapsed time: 00:01:10\n")
sys.stderr.write("Total complete: 100%\n")
sys.stdout.write("Script stdout 6\n")
sys.stdout.write("Script stdout 7\n")
sys.stdout.write("website=www.mfitzp.com\n")
flush_then_wait()

GUI.py:

from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QPlainTextEdit,
                                QVBoxLayout, QWidget, QProgressBar)
from PyQt5.QtCore import QProcess
import sys
import re

# A regular expression, to extract the % complete.
progress_re = re.compile("Total complete: (\d+)%")

def simple_percent_parser(output):
    """
    Matches lines using the progress_re regex,
    returning a single integer for the % progress.
    """
    m = progress_re.search(output)
    if m:
        pc_complete = m.group(1)
        return int(pc_complete)


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.p = None

        self.btn = QPushButton("Execute")
        self.btn.pressed.connect(self.start_process)
        self.text = QPlainTextEdit()
        self.text.setReadOnly(True)

        self.progress = QProgressBar()
        self.progress.setRange(0, 100)

        l = QVBoxLayout()
        l.addWidget(self.btn)
        l.addWidget(self.progress)
        l.addWidget(self.text)

        w = QWidget()
        w.setLayout(l)

        self.setCentralWidget(w)

    def message(self, s):
        self.text.appendPlainText(s)

    def start_process(self):
        if self.p is None:  # No process running.
            self.message("Executing process")
            self.p = QProcess()  # Keep a reference to the QProcess (e.g. on self) while it's running.
            self.p.readyReadStandardOutput.connect(self.handle_stdout)
            self.p.readyReadStandardError.connect(self.handle_stderr)
            self.p.stateChanged.connect(self.handle_state)
            self.p.finished.connect(self.process_finished)  # Clean up once complete.
            self.p.start("python",["dummy_script.py"])

    def handle_stderr(self):
        data = self.p.readAllStandardError()
        stderr = bytes(data).decode("utf8")
        # Extract progress if it is in the data.
        progress = simple_percent_parser(stderr)
        if progress:
            self.progress.setValue(progress)
        self.message(stderr)

    def handle_stdout(self):
        data = self.p.readAllStandardOutput()
        stdout = bytes(data).decode("utf8")
        self.message(stdout)

    def handle_state(self, state):
        states = {
            QProcess.NotRunning: 'Not running',
            QProcess.Starting: 'Starting',
            QProcess.Running: 'Running',
        }
        state_name = states[state]
        self.message(f"State changed: {state_name}")

    def process_finished(self):
        self.message("Process finished.")
        self.p = None


app = QApplication(sys.argv)

w = MainWindow()
w.show()

app.exec_()

The concept used to execute dummy_script.py in GUI.py is:

p = QProcess()
p.start("python3", ['dummy_script.py'])

Now when I pack the whole program in an executable using PyInstaller, dummy_script.py is missing.

How can I make sure dummy_script.py gets included in the PyInstaller bundle?

Reference: https://www.pythonguis.com/tutorials/qprocess-external-programs/


Solution

  • The concept used to execute (dummy_script.py) in (GUI.py) is,

    p = QProcess()
    p.start("python3", ['dummy_script.py'])
    

    PyInstaller is pretty good at finding and bundling dependencies, but it won't be able to figure out that GUI.py needs dummy_script.py if you run it this way.

    A better approach would be to import the code you need and use it directly, e.g something like

    from dummy_script import some_function
    
    
    some_function()
    

    Other options

    If you simply modify GUI.py like this, PyInstaller should find dummy_script.py on its own.

    If that is not practical for some reason, you should be able to declare it as a hidden import using a spec file. You may have a spec file from an earlier build, but if you need to create a new one you can do that with something like this:

    pyi-makespec GUI.py
    

    Then edit the spec file to add dummy_script to the list of hidden imports:

    a = Analysis(['GUI.py'],
         # ...
         hiddenimports=['dummy_script'],
         # ...,
    )
    

    Then build again from the modified spec file:

    pyinstaller foo.spec
    

    That may not work either, since you still aren't importing the other module. In that case you may need to declare it as a data file instead.