Writing clean, flexible, and easy to maintain user input prompts
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
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: continuedef 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)
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.