General purpose input validation GUI

Posted on

Problem

I decided to build a GUI that would seemlessly create user interfaces for validation functions, functions that check if a string satisfies given rules.

An example ‘hello, world!’ usage is:

from gui_validation import gui_validation

def is_valid_salutation(salutation):
    """
    Salutations to be valid must start with one of:
         ['hello', 'hi', 'howdy'] + ',' [COMMA]
    and must end with '!' [EXCLAMATION MARK]

    >>> is_valid_salutation('howdy, moon!')
    True
    >>> is_valid_salutation('random phrase')
    False
    """
    return any(salutation.startswith(i+',') for i in ['hello', 'hi', 'howdy']) 
           and salutation.endswith('!')

if __name__ == "__main__":
    gui_validation(is_valid_salutation)

As you can see the only argument required is the function itself.

gui_validation.py

"""
General purpose user input validation GUI.
"""
import tkinter
from tkinter.constants import X
from tkinter import messagebox

def gui_validation(validator, title=None, text=None, button_text="Validate"):
    """
    Provides a general purpose gui to validate user input.
    The user will be prompted to enter a string and will be
    given feedback by the `validator` function.

    This interface avoids verbosity and assumes the title to be
    the `validator` name and the text to be the `validator` doc if not +
    told explicitly.
    """
    if title is None:
        title = validator.__name__.replace('_',' ').capitalize()
    if text is None:
        text = validator.__doc__

    def validate():
        if validator(user_input.get()):
            messagebox.showinfo("Input valid.",
                                "Congratulations, you entered valid input.")
        else:
            messagebox.showerror("Input NOT valid.",
                                 "Please try again and enter a valid input.")

    root = tkinter.Tk()
    root.wm_title(title)

    title_label = tkinter.Label(root, text=title, font='25px')
    title_label.pack(fill=X, expand=1)

    text_label = tkinter.Label(root, text=text, font='20px')
    text_label.pack(fill=X, expand=1)

    user_input = tkinter.Entry(root)
    user_input.pack()

    button = tkinter.Button(root, text=button_text, command=validate)
    button.pack()

    root.mainloop()

I was wondering:

  • Would a class make the code clearer or just more verbose?
  • Am I asking too little, should I force the user to give more details?
  • Is it OK to use big fonts? The small fonts are much less legible to me.
  • Any other improvement?

Solution

If the goal is “general purpose” I would be inclined to start from a much simpler premise, allowing users (i.e. developers) to build GUIs with the features that they need. Using object-oriented techniques you could subclass the tkinter widgets and extend them as needed. For example:

import tkinter as tk
from tkinter import messagebox


class ValidEntry(tk.Entry):

    def __init__(self, *args, validator=lambda text: True, **kwargs):
        super().__init__(*args, **kwargs)
        self.validator = validator

    def get(self):
        text = super().get()
        if not self.validator(text):
            raise ValueError('Invalid input')
        return text

This is now a reusable component; it can be dropped in wherever a regular tk.Entry is required (indeed the default validator allows it to be a direct replacement if validation isn’t actually needed). The next step up is then:

class ValidatorFrame(tk.Frame):

    INSTRUCTION_FONT = '20px'

    def __init__(self, root, *args, button_text='Validate', instructions=None,
                 validator=lambda text: True, **kwargs):
        super().__init__(root, *args, **kwargs)
        if instructions is None:
            instructions = validator.__doc__
        self.instructions = tk.Label(
            root,
            font=INSTRUCTION_FONT,
            justify=tk.LEFT,
            text=instructions
        )
        self.instructions.pack(expand=1, fill=tk.X)
        self.user_input = ValidEntry(root, validator=validator)
        self.user_input.pack(expand=1, fill=tk.X)
        self.validate = tk.Button(root, command=self.validate, text=button_text)
        self.validate.pack()

    def validate(self):
        try:
            text = self.user_input.get()
        except ValueError:
            messagebox.showerror(
                "Input NOT valid.",
                "Please try again and enter a valid input."
            )
        else:
            messagebox.showinfo(
                "Input valid.",
                "Congratulations, you entered valid input."
            )

Again, this is a component that can be easily used elsewhere. One final step outward:

class TestGui(tk.Tk):

    TITLE_FONT = '25px'

    def __init__(self, *args, title=None, validator=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.title_label = tk.Label(self, font=self.TITLE_FONT, text=title)
        self.title_label.pack(expand=1, fill=tk.X)
        self.frame = ValidatorFrame(self, validator=validator)
        self.frame.pack(expand=1, fill=tk.X)
        if title is None:
            title = validator.__name__.replace('_', ' ').capitalize()
        self.wm_title(title)

By building up from basic components, you give yourself and your users a lot more flexibility. You can now launch this (using the validator you’ve already written) as simply as:

if __name__ == '__main__':
    app = TestGui(validator=is_valid_salutation)
    app.mainloop()

It’s slightly more code, but a lot more flexible. One thing I haven’t put much thought into is where the text would go once valid…


A few other things to note:

  • import tkinter as tk saves you repeating a few extra characters all over the place;
  • You tend to end up with a lot of keyword arguments to widgets, so I keep them in alphabetical order; and
  • Factoring out the styling information makes it easier to reuse, too.

Leave a Reply

Your email address will not be published. Required fields are marked *