7

I'm often tasked with asking users for input. I've always just written my prompts "as-needed" in my main execution scripts. This is kind of ugly, and because I often ask for the same types of input across multiple scripts, a ton of my code is just copy/pasted prompt loops. Here's what I've done in the past:

while True:
    username = input("Enter New Username: ")
    if ldap.search(username):
        print "   [!] Username already taken."
    if not validator.validate_username(username):
        print "   [!] Invalid Username."
    else:
        break

I'd like to create something that can be called like:

username = prompt(prompt="Enter New Username: ",
                  default=None,
                  rules=["user_does_not_exist",
                         "valid_username"])

Then the prompt function looks like:

def prompt(prompt, default, rules):
    while True:
        retval = input(prompt)
        if default and retval == "":
            break
            return default
        if not rule_match(retval, rules):
            continue
        break
        return retval

def rule_match(value, rules):
    if "user_does_not_exist" in rules:
        if not user.user_exists(value):
            return False

    if "valid_username" in rules:
        if not validator.username(value):
            return False

    if "y_n_or_yes_no" in rules:
        if "ignore_case" in rules:
            if value.lower() not in ["y", "yes", "n", "no"]:
                return False
        else:
            if value not in ["y", "yes", "n", "no"]:
                return False

    return True

An alternative I'm considering is to make a Prompt class, which would allow for more flexibility with the results. For example, if I want to convert the "y" or "n" to True or False, the above doesn't really work.

create_another = Prompt(prompt="Create another user? (y/n): ,"
                       default=False,
                       rules=["y_n_or_yes_no",
                              "ignore_case"]).prompt().convert_to_bool()

The other alternative I'm considering is just making individualized prompts and naming them, with each one written similar to my very original code. This doesn't actually change anything. It just serves to get these loops out of my main execution code which makes the main execution code easier to browse:

username = prompt("get_new_username")

def prompt(prompt_name):
    if prompt_name == "get_new_username":
        while True:
            username = input("Enter New Username: ")
            if ldap.search(username):
                print "   [!] Username already taken."
            if not validator.validate_username(username):
                print "   [!] Invalid Username."
            else:
                break
                return username

    if prompt_name == "y_n_yes_no_ignore_case":
        # do prompt
    if prompt_name == "y_n_yes_no":
        # do prompt
    if prompt_name == "y_n":
        # do prompt
    if prompt_name == "y_n_ignore_case":
        # do prompt
    if prompt_name == "yes_no":
        # do prompt 
    if prompt_name == "yes_no_ignore_case":
        # do prompt 

I realize that it's probably just a good idea to settle on one accepted "y/n" format for all of my programs, and I will. This is just for the sake of showing that, in cases where I would need a very similar but slightly different prompt, it would result in a lot of copy/pasted code (no flexibility with this method at all).

What is a good approach to writing clean, flexible, and easy-to-maintain user prompts?

(I've seen this: Asking the user for input until they give a valid response and some other responses. My question is not about how to get input and validate it, it's about how to make a flexible input system that can be reused with across multiple programs).

Community
  • 1
  • 1
CptSupermrkt
  • 6,844
  • 12
  • 56
  • 87
  • 3
    I've always found that my use cases have needed to be customized enough to make writing a general purpose solution difficult. I'm excited to see if anyone comes up with a practical solution though, good question. – Jared Goguen Feb 11 '16 at 23:43
  • Designing a flexible, reusable prompt system is a bit beyond the scope of Stack Overflow. – chepner Feb 12 '16 at 00:11
  • @chepner Well, that is kind of my question! Is it something that has to be really "designed" in-depth (in which case I agree, we can't post entire programs in SO answers), or, and this is my question, is there some solution or way of going about this that is clean and simple that I'm just not thinking of? From what I'm gathering, the answer is that this is indeed a question which requires a complex solution. Even in the case of the former, the question still serves merit I think, to show other people with similar questions that indeed it isn't simple enough to answer in one post. – CptSupermrkt Feb 12 '16 at 15:05

3 Answers3

0

I once wrote a function for something similar. The explanation is in the doc-string:

def xory(question = "", setx = ["yes"], sety = ["no"], setz = [], strict = False):
    """xory([question][, setx][, sety][, setz][, strict]) -> string

    Asks question.  If the answer is equal to one of the elements in setx,
    returns True.  If the answer is equal to one of the elements in sety,
    returns False.  If the answer is equal to one of the elements in setz,
    returns the element in setz that answer is equal to.  If the answer is
    not in any of the sets, reasks the question.  Strict controls whether
    the answer is case-sensitive.  If show is True, an indication of the
    acceptable answers will be displayed next to the prompt."""

    if isinstance(setx, str):
        setx = [setx]
    if isinstance(sety, str):
        sety = [sety]
    if isinstance(setz, str):
        setz = [setz]
    if (setx[0])[0] != (sety[0])[0]:
        setx = [(setx[0])[0]] + setx
        sety = [(sety[0])[0]] + sety
    question = question.strip(" ") + " "
    while True:
        if show:
            shows = "[%s/%s] " % (setx[0], sety[0])
        else:
            shows = ""
        user_input = raw_input(question + shows)
        for y in [setx, sety, setz]:
            for x in y:
                if (user_input == x) or ((not strict) and (user_input.lower() == x.lower())):
                    if y is setx:
                        return True
                    elif y is sety:
                        return False
                    else:
                        return x
        question = ""
        show = True

Examples:

>>> response = xory("1 or 0?", ["1", "one", "uno"], ["0", "zero", "null"], ["quit", "exit"])
1 or 0? x
[1/0] eante
[1/0] uno
>>> print(response)
True
>>> response = xory("Is that so?")
Is that so? Who knows?
[y/n] no
>>> print(response)
False
>>> response = xory("Will you do it?", setz=["quit", "exit", "restart"])
Will you do it? hm
[y/n] quit
>>> print(response)
quit
zondo
  • 19,901
  • 8
  • 44
  • 83
0

I'd advise to write a library that contains a number of very clearly defined building blocks, each one as small and light-weight as possible, without too many assumptions baked in about how you're going to put the pieces together.

That is, I'd include one function that does the loop, but instead of passing in a set of rules, I'd allow for exactly one function to be passed in that either returns a value (if a valid input was given and after converting it in any way necessary) or raises a ValueError if the input wasn't usable. Other building blocks would implement certain checks or transformations (like resolution of 'y' and 'n' into boolean values).

This way, you would leave it completely up to the user to assemble the stuff in a way suitable for the use case.

# library:

def prompt(prompt, default, postprocess):
    value = input('{} ({}): '.format(prompt, default)) or default
    try:
        return postprocess(value)
    except ValueError:
        continue

def check_lower(value):
    if not value.islower():
        raise ValueError()

def to_bool(value):
    return value in 'yes'

# using the library:

def postprocess(value):
    check_lower(value)
    return to_bool(value)

prompt('Really?', 'n', postprocess)
Thomas Lotze
  • 5,153
  • 1
  • 16
  • 16
0

I would create a prompt function as such:

def prompt(prompt, default=None, rules=[]):
    while True:
        response = input(prompt)
        if response:
            valid = [rule(response) for rule in rules]
            if not(False in valid):
                return response
            else:
                print('Invalid input')
        else:
            return default

You could then create different validation functions such as

def filterValidEmail(string):
    if '@' in string:
        if '.' in string.split('@')[1]:
            return True
        else:
            return False
    else:
        return False

And call these functions like so:

prompt('What is your email? ', rules=[filterValidEmail])

You could also tweak this so that you can tell the user what verification they failed or disallow blank inputs.

hhaefliger
  • 521
  • 3
  • 18