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:
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()
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.