pythonpycharmpyinstaller

Can´t create a working .exe file in python with pyinstaller


I'm noob at programming in python, and tried to make a script to help my kids to improve their reading and math skills.

I have this code:

import tkinter as tk
from tkinter import messagebox
import random
from num2words import num2words
import inflect
import inspect
import time
print(inspect)


# Inicializamos la biblioteca inflect
p = inflect.engine()

# Velocidad predeterminada para mostrar palabras
velocidad_predeterminada = 1  # en segundos
palabras = [ "Abrir", "Amor", "Aprender", "Amigo", "Año", "Azul"]  

# Lista de palabras en español
generando_palabras = False  # Variable global para controlar la generación de palabras


# Código de la clase MatQuizApp aquí (no modificado)
class MathQuizApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Math Quiz")
        self.root.geometry("500x400")
        self.score = 0

        # Widgets for setting the number of digits
        self.digit_label = tk.Label(root, text="Número de dígitos:")
        self.digit_label.pack()
        self.digit_entry = tk.Entry(root)
        self.digit_entry.pack()
        self.start_button = tk.Button(root, text="Iniciar", command=self.start_quiz)
        self.start_button.pack()

        # Frame for displaying the operation
        self.operation_frame = tk.Frame(root)
        self.operation_frame.pack(pady=20)

        self.symbol_label = tk.Label(self.operation_frame, font=("Helvetica", 24))
        self.symbol_label.grid(row=0, column=0, rowspan=2)
        self.num1_labels = []
        self.num2_labels = []
        self.answer_entries = []
        self.num1_carry = []
        self.num2_carry = []

        for i in range(5):
            carry_num1_entry = tk.Entry(self.operation_frame, font=("Helvetica", 10), width=2)
            carry_num1_entry.grid(row=0, column=i + 1, sticky='W')
            carry_num1_entry.bind('<KeyRelease>', self.on_carry_num1_change)
            self.num1_carry.append(carry_num1_entry)

            num1_label = tk.Label(self.operation_frame, font=("Helvetica", 24))
            num1_label.grid(row=0, column=i + 1, sticky='E')
            self.num1_labels.append(num1_label)

            carry_num2_entry = tk.Entry(self.operation_frame, font=("Helvetica", 10), width=2)
            carry_num2_entry.grid(row=1, column=i + 1, sticky='W')
            carry_num2_entry.bind('<KeyRelease>', self.on_carry_num2_change)
            self.num2_carry.append(carry_num2_entry)

            num2_label = tk.Label(self.operation_frame, font=("Helvetica", 24))
            num2_label.grid(row=1, column=i + 1, sticky='E')
            self.num2_labels.append(num2_label)

            answer_entry = tk.Entry(self.operation_frame, font=("Helvetica", 24), width=2)
            answer_entry.grid(row=2, column=i + 1)
            self.answer_entries.append(answer_entry)

        # Buttons for quiz actions
        self.validate_button = tk.Button(root, text="Validar", command=self.validate_answer)
        self.retry_button = tk.Button(root, text="Reintentar", command=self.retry)
        self.continue_button = tk.Button(root, text="Continuar", command=self.continue_quiz)
        self.exit_button = tk.Button(root, text="Salir", command=root.quit)

        # Score label
        self.score_label = tk.Label(root, text=f"Score: {self.score}", font=("Helvetica", 14))
        self.score_label.pack(anchor='ne', padx=10, pady=5)

        # Initially hide quiz widgets
        self.hide_quiz_widgets()

    def hide_quiz_widgets(self):
        self.operation_frame.pack_forget()
        self.validate_button.pack_forget()
        self.retry_button.pack_forget()
        self.continue_button.pack_forget()
        self.exit_button.pack_forget()

    def show_quiz_widgets(self):
        self.operation_frame.pack(pady=20)
        self.validate_button.pack()
        self.retry_button.pack()
        self.continue_button.pack()
        self.exit_button.pack()

    def start_quiz(self):
        try:
            self.num_digits = int(self.digit_entry.get())
            if self.num_digits < 1 or self.num_digits > 5:
                raise ValueError("Número de dígitos debe ser entre 1 y 5.")
        except ValueError as e:
            messagebox.showerror("Error", str(e))
            return

        self.digit_entry.pack_forget()
        self.digit_label.pack_forget()
        self.start_button.pack_forget()

        self.show_quiz_widgets()
        self.generate_operation()

    def generate_operation(self):
        max_value = 10 ** self.num_digits
        self.num1 = random.randint(1, max_value - 1)
        self.num2 = random.randint(1, max_value - 1)
        self.operation = random.choice(['+', '-'])

        if self.operation == '-':
            # Ensure the result is non-negative
            if self.num1 < self.num2:
                self.num1, self.num2 = self.num2, self.num1
            self.show_carry_entries()
        else:
            self.hide_carry_entries()

        self.symbol_label.config(text=f"{self.operation}")
        self.update_labels(self.num1, self.num1_labels)
        self.update_labels(self.num2, self.num2_labels)
        self.clear_entries()

    def update_labels(self, number, labels):
        num_str = str(number).zfill(5)
        for i, digit in enumerate(num_str):
            labels[i].config(text=digit)

    def clear_entries(self):
        for entry in self.answer_entries:
            entry.delete(0, tk.END)
        for entry in self.num1_carry + self.num2_carry:
            entry.delete(0, tk.END)

    def hide_carry_entries(self):
        for entry in self.num2_carry:
            entry.grid_remove()

    def show_carry_entries(self):
        for entry in self.num2_carry:
            entry.grid()

    def get_user_answer(self):
        answer_str = ''.join(entry.get().zfill(1) for entry in self.answer_entries)
        try:
            return int(answer_str)
        except ValueError:
            return None

    def validate_answer(self):
        user_answer = self.get_user_answer()
        if user_answer is None:
            messagebox.showerror("Error", "Por favor, ingresa un número válido.")
            return

        correct_answer = self.num1 + self.num2 if self.operation == '+' else self.num1 - self.num2

        if user_answer == correct_answer:
            messagebox.showinfo("Correcto", "¡FELICIDADES la respuesta es CORRECTA!")
            self.score += 1
            self.score_label.config(text=f"Score: {self.score}")
            self.generate_operation()
        else:
            response = messagebox.askquestion("Incorrecto", "La respuesta es incorrecta. ¿Quieres reintentar?",
                                              icon='warning')
            if response == 'yes':
                self.retry()
            else:
                self.continue_quiz()

    def retry(self):
        self.clear_entries()

    def continue_quiz(self):
        self.generate_operation()

    def on_carry_num1_change(self, event):
        entry = event.widget
        if self.operation == '+':
            self.add_plus_symbol(entry)

    def on_carry_num2_change(self, event):
        entry = event.widget
        if self.operation == '-':
            self.add_plus_symbol(entry)

    def add_plus_symbol(self, entry):
        content = entry.get()
        if content and not content.endswith('+'):
            entry.delete(0, tk.END)
            entry.insert(0, f"{content}+")


# Código de la clase MathGame aquí (no modificado)
class MathGame:
    def __init__(self, root):
        self.root = root
        self.root.title("Juego de Tablas Matemáticas")
        self.root.geometry("800x400")  # Tamaño de la ventana aumentado
        self.score = 0
        self.errors = 0


        self.label = tk.Label(root, text="Elige una o más tablas para practicar:", font=("Arial", 14))
        self.label.pack(pady=10)

        # Checkbox variables for selecting multiple tables
        self.table_vars = {}
        self.checkboxes_frame = tk.Frame(root)
        self.checkboxes_frame.pack(pady=10)

        for i in range(1, 11):
            var = tk.IntVar()
            checkbox = tk.Checkbutton(self.checkboxes_frame, text=f"Tabla del {i}", variable=var, font=("Arial", 14))
            checkbox.grid(row=(i - 1) // 5, column=(i - 1) % 5, padx=5, pady=5)
            self.table_vars[i] = var

        self.start_button = tk.Button(root, text="Empezar", font=("Arial", 14), command=self.start_game)
        self.start_button.pack(pady=10)

        self.question_frame = tk.Frame(root)
        self.score_label = tk.Label(root, text=f"Puntaje: {self.score}  |  Errores: {self.errors}", font=("Arial", 14))

    def start_game(self):
        self.selected_tables = [table for table, var in self.table_vars.items() if var.get() == 1]
        if not self.selected_tables:
            self.label.config(text="Por favor, elige al menos una tabla.")
            return

        self.label.pack_forget()
        self.checkboxes_frame.pack_forget()
        self.start_button.pack_forget()

        self.question_frame.pack(pady=10)
        self.score_label.pack(pady=10)
        self.generate_question()

    def generate_question(self):
        self.table = random.choice(self.selected_tables)
        self.x = random.randint(1, 10)
        correct_answer = self.table * self.x
        options = [correct_answer, random.randint(1, 100), random.randint(1, 100)]
        random.shuffle(options)

        self.question_label = tk.Label(self.question_frame, text=f"{self.table} x {self.x} =", font=("Arial", 14))
        self.question_label.grid(row=0, column=0, padx=10, pady=10)

        self.answer_var = tk.StringVar()
        self.answer_var.set(None)  # Opciones sin seleccionar al inicio

        self.option_buttons = []
        for i, option in enumerate(options):
            button = tk.Radiobutton(self.question_frame, text=option, variable=self.answer_var, value=option,
                                    font=("Arial", 14))
            button.grid(row=i + 1, column=0, padx=10, pady=10)
            self.option_buttons.append(button)

        self.submit_button = tk.Button(self.question_frame, text="Enviar", font=("Arial", 14),
                                       command=self.check_answer)
        self.submit_button.grid(row=4, column=0, padx=10, pady=10)

    def check_answer(self):
        selected = self.answer_var.get()
        if not selected:
            self.question_label.config(text=f"Por favor selecciona una respuesta.")
            return

        selected = int(selected)
        correct_answer = self.table * self.x
        if selected == correct_answer:
            self.score += 1
            messagebox.showinfo("Correcto", "¡Respuesta correcta!")
        else:
            self.errors += 1
            messagebox.showerror("Incorrecto", f"La respuesta correcta era {correct_answer}.")

        self.score_label.config(text=f"Puntaje: {self.score}  |  Errores: {self.errors}")
        self.generate_question()



# Función para practicar Números en Español
def practicar_numeros_espanol():
    def generar_numero():
        try:
            digitos = int(entry_digitos.get())
            if digitos <= 0:
                raise ValueError("El número de dígitos debe ser mayor que 0.")
            numero = random.randint(10**(digitos-1), (10**digitos)-1)
            numero_palabras = num2words(numero, lang='es')  # Convertir a palabras en español
            numero_mostrado.set(numero_palabras.capitalize())
            numero_generado.set(numero)
        except ValueError:
            messagebox.showerror("Error", "Por favor, ingresa una cantidad válida de dígitos.")

    def verificar_respuesta():
        try:
            respuesta = int(entry_respuesta.get())
            if respuesta == numero_generado.get():
                correcto.set(correcto.get() + 1)
                messagebox.showinfo("Resultado", "¡Correcto!")
            else:
                incorrecto.set(incorrecto.get() + 1)
                messagebox.showinfo("Resultado", "Incorrecto")
            generar_numero()  # Generar otro número después de verificar
            entry_respuesta.delete(0, tk.END)  # Limpiar el campo de texto
        except ValueError:
            messagebox.showerror("Error", "Por favor, ingresa un número válido.")

    # Configuración de la ventana
    ventana_numeros = tk.Toplevel(ventana)
    ventana_numeros.title("Juego de Números en Palabras en Español")

    # Variables
    numero_mostrado = tk.StringVar()
    numero_generado = tk.IntVar()
    correcto = tk.IntVar(value=0)
    incorrecto = tk.IntVar(value=0)

    # Interfaz gráfica
    tk.Label(ventana_numeros, text="Ingresa la cantidad de dígitos:").pack(pady=5)

    entry_digitos = tk.Entry(ventana_numeros)
    entry_digitos.pack(pady=5)

    tk.Button(ventana_numeros, text="Generar número", command=generar_numero).pack(pady=5)

    tk.Label(ventana_numeros, text="Número mostrado (en palabras):").pack(pady=5)
    tk.Label(ventana_numeros, textvariable=numero_mostrado, font=("Helvetica", 16)).pack(pady=5)

    tk.Label(ventana_numeros, text="Escribe el número:").pack(pady=5)
    entry_respuesta = tk.Entry(ventana_numeros)
    entry_respuesta.pack(pady=5)

    tk.Button(ventana_numeros, text="Verificar respuesta", command=verificar_respuesta).pack(pady=5)

    tk.Label(ventana_numeros, text="Correcto:").pack(pady=5)
    tk.Label(ventana_numeros, textvariable=correcto).pack()
    tk.Label(ventana_numeros, text="Incorrecto:").pack(pady=5)
    tk.Label(ventana_numeros, textvariable=incorrecto).pack()

# Función para practicar Números en Inglés
def practicar_numeros_ingles():
    def generar_numero():
        try:
            digitos = int(entry_digitos.get())
            if digitos <= 0:
                raise ValueError  # Evitar que se ingresen valores no válidos
            numero = random.randint(10**(digitos-1), (10**digitos)-1)
            numero_palabras = p.number_to_words(numero)
            numero_mostrado.set(numero_palabras.capitalize())
            numero_generado.set(numero)
        except ValueError:
            messagebox.showerror("Error", "Por favor, ingresa una cantidad válida de dígitos")

    def verificar_respuesta():
        try:
            respuesta = int(entry_respuesta.get())
            if respuesta == numero_generado.get():
                correcto.set(correcto.get() + 1)
                messagebox.showinfo("Resultado", "¡Correcto!")
            else:
                incorrecto.set(incorrecto.get() + 1)
                messagebox.showinfo("Resultado", "Incorrecto")
            generar_numero()  # Generar otro número después de verificar
            entry_respuesta.delete(0, tk.END)  # Limpiar el campo de texto
        except ValueError:
            messagebox.showerror("Error", "Por favor, ingresa un número válido")

    # Configuración de la ventana
    ventana_numeros = tk.Toplevel(ventana)
    ventana_numeros.title("Juego de Números en Palabras en Inglés")

    # Variables
    numero_mostrado = tk.StringVar()
    numero_generado = tk.IntVar()
    correcto = tk.IntVar(value=0)
    incorrecto = tk.IntVar(value=0)

    # Interfaz gráfica
    tk.Label(ventana_numeros, text="Ingresa la cantidad de dígitos:").pack(pady=5)

    entry_digitos = tk.Entry(ventana_numeros)
    entry_digitos.pack(pady=5)

    tk.Button(ventana_numeros, text="Generar número", command=generar_numero).pack(pady=5)

    tk.Label(ventana_numeros, text="Número mostrado (en palabras):").pack(pady=5)
    tk.Label(ventana_numeros, textvariable=numero_mostrado, font=("Helvetica", 16)).pack(pady=5)

    tk.Label(ventana_numeros, text="Escribe el número:").pack(pady=5)
    entry_respuesta = tk.Entry(ventana_numeros)
    entry_respuesta.pack(pady=5)

    tk.Button(ventana_numeros, text="Verificar respuesta", command=verificar_respuesta).pack(pady=5)

    tk.Label(ventana_numeros, text="Correcto:").pack(pady=5)
    tk.Label(ventana_numeros, textvariable=correcto).pack()
    tk.Label(ventana_numeros, text="Incorrecto:").pack(pady=5)
    tk.Label(ventana_numeros, textvariable=incorrecto).pack()

# Función para practicar lectura
def practicar_lectura():
    def mostrar_palabra_aleatoria():
        global generando_palabras

        if generando_palabras:
            cantidad_palabras = int(entrada_cantidad_palabras.get())
            palabras_aleatorias = random.choices(palabras, k=cantidad_palabras)
            etiqueta.config(text=" ".join(palabras_aleatorias))

            try:
                velocidad = float(entrada_velocidad.get())
                if velocidad > 0:
                    pausa = max(0.01, velocidad)  # Establecer una pausa mínima de 0.01 segundos
                    ventana.after(int(pausa * 1000), mostrar_palabra_aleatoria)
                else:
                    ventana.after(velocidad_predeterminada * 1000, mostrar_palabra_aleatoria)
            except ValueError:
                ventana.after(velocidad_predeterminada * 1000, mostrar_palabra_aleatoria)

    def iniciar_generacion():
        global generando_palabras
        generando_palabras = True
        cantidad_palabras = int(entrada_cantidad_palabras.get())
        if cantidad_palabras > 0:
            mostrar_palabra_aleatoria()

    def detener_generacion():
        global generando_palabras
        generando_palabras = False

    # Configuración de la ventana
    ventana_lectura = tk.Toplevel(ventana)
    ventana_lectura.title("Palabras al Azar")
    ventana_lectura.geometry("1200x400")

    etiqueta = tk.Label(ventana_lectura, font=("Arial", 50))
    etiqueta.pack(pady=50)

    etiqueta_velocidad = tk.Label(ventana_lectura, text="¿Segundos entre Palabras?")
    etiqueta_velocidad.pack()

    entrada_velocidad = tk.Entry(ventana_lectura, width=10)
    entrada_velocidad.pack(pady=10)
    entrada_velocidad.insert(0, velocidad_predeterminada)

    etiqueta_cantidad_palabras = tk.Label(ventana_lectura, text="Cantidad de palabras para mostrar")
    etiqueta_cantidad_palabras.pack()

    entrada_cantidad_palabras = tk.Entry(ventana_lectura, width=10)
    entrada_cantidad_palabras.pack(pady=10)
    entrada_cantidad_palabras.insert(0, "1")

    boton_iniciar = tk.Button(ventana_lectura, text="Iniciar", command=iniciar_generacion)
    boton_iniciar.pack(pady=10)

    boton_detener = tk.Button(ventana_lectura, text="Detener", command=detener_generacion)
    boton_detener.pack(pady=10)

# Función para abrir las diferentes prácticas
def abrir_practica(practica):
    if practica == "numeros_espanol":
        practicar_numeros_espanol()
    elif practica == "numeros_ingles":
        practicar_numeros_ingles()
    elif practica == "lectura":
        practicar_lectura()
    elif practica == "tablas":
        MathGame(tk.Toplevel(ventana))  # Abre el juego de tablas en una nueva ventana
    elif practica == "SumasyRestas":
        MathQuizApp(tk.Toplevel(ventana))  # Abre el juego de tablas en una nueva ventana

# Configuración de la ventana principal
ventana = tk.Tk()
ventana.title("¿Que vamos a practicar hoy?")
ventana.geometry("400x400")



# Botones para seleccionar la práctica
tk.Button(ventana, text="Practicar Tablas", command=lambda: abrir_practica("tablas")).pack(pady=10)  # Nuevo botón para practicar tablas
tk.Button(ventana, text="Practicar Sumas Y Restas", command=lambda: abrir_practica("SumasyRestas")).pack(pady=10)  # Nuevo botón para practicar tablas
tk.Button(ventana, text="Practicar Lectura", command=lambda: abrir_practica("lectura")).pack(pady=10)
tk.Button(ventana, text="Practicar Números en Español", command=lambda: abrir_practica("numeros_espanol")).pack(pady=10)
tk.Button(ventana, text="Practicar Números en Inglés", command=lambda: abrir_practica("numeros_ingles")).pack(pady=10)

# Iniciar el bucle principal
ventana.mainloop()

and i created the .exe file using: pyinstaller --onefile -w main.py. but when i try to run the main.exe file in the dist folder, i get this error:

"Failed to execute script 'main' due to unhandled exception: could not get source code"

Traceback (most recent call last):
  File "main.py", line 5, in <module>
  File "<frozen importlib._bootstrap>", line 1178, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1149, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
  File "PyInstaller\loader\pyimod02_importers.py", line 378, in exec_module
  File "inflect\__init__.py", line 2071, in <module>
  File "inflect\__init__.py", line 2092, in engine
  File "typeguard\_decorators.py", line 223, in typechecked
  File "typeguard\_decorators.py", line 71, in instrument
  File "inspect.py", line 1262, in getsource
  File "inspect.py", line 1244, in getsourcelines
  File "inspect.py", line 1081, in findsource
OSError: could not get source code

The Script is running with no issues within pycharm when i click the play button, but i just cant have the .exe file working.

I already look for answer withing google and chatgpt and none of the solutions suggested worked.

thanks in advance for your support.


Solution

  • Doing some digging on this, I ended up here: https://github.com/pyinstaller/pyinstaller/issues/8013

    The module inflect uses a dataset and the traceback points to line 5 which is the import for inflect.

    This means that you'll need to apply the solution from the github issue to your project.

    If you run pyinstaller --onefile -w main.py it creates a main.spec file which you'll need to edit. You need to add this:

        module_collection_mode={
            'inflect': 'pyz+py',
        }
    

    to the section a = Analysis.

    For me I ended up with the following main.spec file:

    # -*- mode: python ; coding: utf-8 -*-
    
    
    a = Analysis(
        ['main.py'],
        pathex=[],
        binaries=[],
        datas=[],
        hiddenimports=[],
        hookspath=[],
        hooksconfig={},
        runtime_hooks=[],
        excludes=[],
        noarchive=False,
        optimize=0,
        module_collection_mode={
            'inflect': 'pyz+py',
        }
    )
    pyz = PYZ(a.pure)
    
    exe = EXE(
        pyz,
        a.scripts,
        a.binaries,
        a.datas,
        [],
        name='main',
        debug=False,
        bootloader_ignore_signals=False,
        strip=False,
        upx=True,
        upx_exclude=[],
        runtime_tmpdir=None,
        console=False,
        disable_windowed_traceback=False,
        argv_emulation=False,
        target_arch=None,
        codesign_identity=None,
        entitlements_file=None,
    )
    

    Finally, you need to build the .EXE with this command:

    pyinstaller main.spec