Search code examples
pythontkinterpyinstaller

GUI wrapping speedtest_cli does not work when packaged with pyinstaller


I tried to compile a TKinter script into .exe file with pyinstall, but when I open the .exe file it shows an error. My TKinter script has threading. Here's a stack trace showing unhandled exception in speedtest.py

Traceback (most recent call last):
  File "speedtest.py", line 156, in <module>
ModuleNotFoundError: No module named '__builtin__'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "Speedtest.py", line 3, in <module>
    from speedtest_cli import Speedtest
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "PyInstaller\loader\pyimod02_importers.py", line 450, in exec_module
  File "speedtest_cli.py", line 30, in <module>
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "PyInstaller\loader\pyimod02_importers.py", line 450, in exec_module
  File "speedtest.py", line 179, in <module>
  File "speedtest.py", line 166, in __init__
AttributeError: 'NoneType' object has no attribute 'fileno'

Also here's the code I used for compilation:

process = subprocess.Popen(
    r'pyinstaller.exe --onefile --noconsole --distpath D:\PythonProjects\Speedtest Speedtest.py',
    stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE,
    shell=True)

output, error = process.communicate()

if error:
    print(error.decode())

if output:
    print(output.decode())

I tried:

  • Changing --noconsole to --windowed
  • Deleting stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE

Deleting --noconsole is not an option, I don't need a console while in .exe file

Also here is speedtest.py

from tkinter import *
from tkinter import ttk
from speedtest_cli import Speedtest
import threading

root = Tk()
root["bg"] = "#fafafa"
root.geometry("300x400")
root.title("Speedtest")
root.iconbitmap("icon.ico")
root.resizable(False, False)
style = ttk.Style()
style.configure("TButton", font=("Comic Sans MS", 20))


def speedtest():
    downloadText.config(text="Download Speed:\n-")
    uploadText.config(text="Upload Speed:\n-")
    pingText.config(text="Ping:\n-")
    st = Speedtest()
    errorText.place_forget()
    analyzingText.place(anchor=CENTER, relx=0.5, rely=0.92)
    downloadSpeed = round(st.download() / (10 ** 6), 2)
    uploadSpeed = round(st.upload() / (10 ** 6), 2)
    pingSpeed = st.results.ping
    downloadText.config(text="Download Speed:\n" + str(downloadSpeed) + " Mbps")
    uploadText.config(text="Upload Speed:\n" + str(uploadSpeed) + " Mbps")
    pingText.config(text="Ping:\n" + str(pingSpeed) + " Ms")
    analyzingText.place_forget()

def speedtestChecked():
    try:
        speedtest()
    except Exception:
        analyzingText.place_forget()
        errorText.place(anchor=CENTER, relx=0.5, rely=0.92)

def startSpeedtestThread():
    speedtestThread = threading.Thread(target=speedtestChecked)
    speedtestThread.start()


speedtestButton = ttk.Button(root, text="Start Speedtest", style="TButton", command=startSpeedtestThread)
speedtestButton.pack(side=BOTTOM, pady=60)

analyzingText = Label(text="Analyzing...", bg="#fafafa", font=("Comic Sans MS", 17))
errorText = Label(text="Error occurred!", bg="#fafafa", font=("Comic Sans MS", 17), fg="#a83636")
downloadText = Label(text="Download Speed:\n-", bg="#fafafa", font=("Comic Sans MS", 17))
uploadText = Label(text="Upload Speed:\n-", bg="#fafafa", font=("Comic Sans MS", 17))
pingText = Label(text="Ping\n-", bg="#fafafa", font=("Comic Sans MS", 17))
downloadText.place(anchor=CENTER, relx=0.5, rely=0.13)
uploadText.place(anchor=CENTER, relx=0.5, rely=0.35)
pingText.place(anchor=CENTER, relx=0.5, rely=0.57)

root.mainloop()

Solution

  • Threading works fine -- your problem has nothing whatsoever to do with threading.

    The problem is that the speedtest_cli library tries to wrap stdout and stderr for UTF-8 compatibility (needed because it tries to be compatible with ancient Python 2.x releases where Unicode strings weren't default), and the code it uses for that purpose doesn't work in a GUI when one doesn't have a real terminal handle: the NullWriter object set up by pyinstaller with --noconsole doesn't provide the fileno() method that a file object wrapping a real file descriptor offers.

    Specifically, the code in question:

    class _Py3Utf8Output(TextIOWrapper):
        """UTF-8 encoded wrapper around stdout for py3, to override
        ASCII stdout
        """
        def __init__(self, f, **kwargs):
            buf = FileIO(f.fileno(), 'w')
            super(_Py3Utf8Output, self).__init__(
                buf,
                encoding='utf8',
                errors='strict'
            )
    
    
        def write(self, s):
            super(_Py3Utf8Output, self).write(s)
            self.flush()
    
    

    The code applying that patch (in lines shortly below those quoted) aborts if an OSError is encountered, but it doesn't have the same fallback logic for an AttributeError.


    If there's no non-CLI-specific alternative library, I'd patch it to rip that functionality out entirely; you don't need it, and it's providing no value. You could also just change the exception handling to handle AttributeError or Exception as a whole.