Python Forum
tkinter help please
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
tkinter help please
#11
What have you tried to solve the problem?
Reply
#12
I was thinking about the test runner and realized the runner should not contain any test scripts. The test scripts should be independent of the runner, only using the runner's print() and request() methods.

I removed the test scripts from the runner code and made a directory structure like this:
top folder
Output:
| runner.py # Test runner program |_tests # Folder of test scripts. |_test_countdown.py |_test_voltage.py |_test_whatever.py
When you write a new test script, give it a name that starts with "test_" and save it i the tests folder. Test scripts need to follow some guidelines. Each test script must:
1. Assign a name, wihich will appear in a list of tests in the test runner window.
2. Assign a description, a list of strings that are printed in the test results window
when the test is selected.
3. Contain a function named "script" that is executed to run the test.

This is a simple test that does a countdown then displays a message in a tkinter message box.
from tkinter import messagebox

name = "Countdown"
description = ["Test example.\n", "Performs a countdown.\n"]

def script(x):
    """Counts down from 10 then displays message."""
    try:
        for i in range(10, 0, -1):
            x.print(i, end=" ")
            x.wait(0.5)
        x.request(messagebox.showinfo, title="Countdown", message="Blastoff!")
    except SystemExit:
        pass

    x.print(*description, clear=True)
The test runner program imports all the test modules and passes them to the test runner window. The test names get displayed in an option menu. To run a test you select the test in the option menu and press the "Run Test" menu. A Stop Test menu stops a running test.
test_folder = Path(__file__).parent / "tests"
sys.path.append(str(test_folder))
tests = [import_module(file.stem) for file in test_folder.glob("test_*.py")]
All the code:
import sys
import time
import threading
import tkinter as tk
from tkinter.scrolledtext import ScrolledText
from functools import partial
from pathlib import Path
from importlib import import_module


class MainWindow(tk.Tk):
    """A test runner"""

    def __init__(self, tests):
        """Initialize test runner.

        tests: list of test modules.
        """
        super().__init__()
        self.title("Test Runner")

        test_names = sorted([test.name for test in tests])
        self.tests = dict(zip(test_names, tests))
        self.test = tk.StringVar(self, "")
        self.test.trace("w", self.select_test)

        frame = tk.Frame(self)
        frame.pack(side=tk.TOP, fill=tk.X)
        self.test_menu = tk.OptionMenu(frame, self.test, *test_names)
        self.test_menu.pack(side=tk.LEFT, padx=5, pady=5, expand=True, fill=tk.X)
        self.run_button = tk.Button(
            frame, text="Run Test", width=20, command=self.run_test
        )
        self.run_button.pack(side=tk.LEFT)
        self.stop_button = tk.Button(
            frame, text="Stop Test", width=20, command=self.stop_test, state=tk.DISABLED
        )
        self.stop_button.pack(side=tk.LEFT, padx=5, pady=4)

        self.text = ScrolledText(self, wrap=tk.WORD, width=80, height=20)
        self.text.pack(side=tk.TOP, padx=5, pady=(0, 5), expand=True, fill=tk.BOTH)

        self.request_action = None
        self.request_reply = None
        self.exit_test = False
        self.test.set(test_names[0])

    def select_test(self, *args):
        """Display description of selected test"""
        module = self.tests[self.test.get()]
        self.print(*module.description, clear=True)

    def run_test(self):
        """Run selected test"""
        module = self.tests[self.test.get()]
        self.thread = threading.Thread(target=partial(module.script, self))
        self.thread.start()
        self.test_menu["state"] = tk.DISABLED
        self.run_button["state"] = tk.DISABLED
        self.stop_button["state"] = tk.NORMAL
        self.monitor_test()

    def stop_test(self):
        """Exit test next time it calls print, wait or request"""
        self.exit_test = True

    def monitor_test(self):
        """Monitor test execution. Executes requests from the test."""
        if self.thread and self.thread.is_alive():
            # Test is still running
            if self.request_action is not None:
                # execute the request
                self.request_reply = self.request_action()
                self.request_action = None
            self.after(100, self.monitor_test)
        else:
            # Test is complete
            self.thread = None
            self.test_menu["state"] = tk.NORMAL
            self.run_button["state"] = tk.NORMAL
            self.stop_button["state"] = tk.DISABLED

    def print(self, *lines, end="\n", clear=False):
        """Print lines in scrolled text area.

        lines: Lines to print.
        end: Printed at the end of each line.  Defaults to "\n".
        clear: Clear textbox before printing
        """
        if self.exit_test:
            self.exit_test = False
            exit()
        if clear:
            self.text.delete(1.0, tk.END)
        for line in lines:
            self.text.insert(tk.END, f"{line}{end}")
        self.text.see(tk.END)

    def request(self, func, *args, **kwargs):
        """Test script uses request to execute a command in the GUI context.
        Use to draw a message box or dialog.  Could even make a new window.

        func : function to execute.
        *args, **kwargs : arguments passed to action.
        """
        if self.exit_test:
            self.exit_test = False
            exit()
        self.request_action = partial(func, *args, **kwargs)
        while self.request_action is not None:
            time.sleep(0.1)
        return self.request_reply

    def wait(self, seconds=0):
        count = max(int(seconds * 10), 1)
        for _ in range(count):
            if self.exit_test:
                self.exit_test = False
                exit()
            time.sleep(0.1)


# Test scripts are python files in ./tests folder and start with "test_".
# Collect tests in a dictionary and pass to the test runner __init__.
test_folder = Path(__file__).parent / "tests"
sys.path.append(str(test_folder))
tests = [import_module(file.stem) for file in test_folder.glob("test_*.py")]
if tests:
    window = MainWindow(tests)
    window.mainloop()
else:
    print("There are no tests in the test folder.")
Recently added a way to stop a running test. Added a "Stop" button to the test runner. Pressing the button sets an "exit_test" flag. If the exit_test flag is set when test script calls the runner (print, request, wait), a SystemExit exception is raised, ending the test script.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020