X-Git-Url: https://scripts.mit.edu/gitweb/wizard.git/blobdiff_plain/8cda195cdbf9cc5f17c195a7f98167e3e9c4657b..6554c6378a6e801b4fe47c50688cdae1d627bc18:/wizard/prompt.py diff --git a/wizard/prompt.py b/wizard/prompt.py index ce6a5f5..852dc44 100644 --- a/wizard/prompt.py +++ b/wizard/prompt.py @@ -1,10 +1,14 @@ """ -Interface compatible with :class:`dialog.Dialog` for doing +Interface compatible with :class:`PromptInterface` for doing non-ncurses interaction. By convention, the last line of a text parameter should be a short value with a trailing colon so that we can prompt a user for a value immediately after it. + +.. testsetup:: * + + from wizard.prompt import * """ import sys @@ -12,6 +16,7 @@ import readline import decorator import textwrap import getpass +import os import wizard @@ -22,9 +27,17 @@ except ImportError: has_dialog = False def fill(text, width=60, **kwargs): + """ + Convenience wrapper for :func:`textwrap.fill` that preserves + paragraphs. + """ return "\n\n".join(textwrap.fill(p, width=width, **kwargs) for p in text.split("\n\n")) def guess_dimensions(text, width=60): + """ + Guesses the dimensions that any given piece of text will + need to display on terminal, given some width. + """ # +1 for the fact that there's no trailing newline from fill # +2 for the borders # +1 as a buffer in case we underestimate @@ -32,24 +45,79 @@ def guess_dimensions(text, width=60): return width, fill(text, width-2).count("\n") + 1 + 2 + 1 def make(prompt, non_interactive): + """ + Makes a :class:`dialog.Dialog` compatible class based on + configuration. + """ if non_interactive: return FailPrompt() - if prompt or not has_dialog: + if prompt or os.getenv('TERM') == 'dumb' or not has_dialog: return Prompt() try: return Dialog() - except dialog.ExecutableNotFound: + except (dialog.ExecutableNotFound, UnsupportedTerminal): return Prompt() def join_or(items): + """ + Joins a list of disjunctions into a human readable sentence. + + >>> join_or(['foo']) + 'foo' + >>> join_or(['foo', 'bar', 'baz']) + 'foo, bar or baz' + """ if len(items) == 0: raise ValueError elif len(items) == 1: return items[0] return ', '.join(items[:-1]) + ' or ' + items[-1] +class PromptInterface(object): + def inputbox(self, text, init='', **kwargs): + """ + Request a free-form, single line of text from the user. + Prompt the user using ``text``; and ``init`` is the + initial value filling the field; not all implementations + support editing ``init``. Returns the typed string. + """ + raise NotImplementedError + def menu(self, text, choices=[], **kwargs): + """ + Request a selection from a number of choices from the user. + Prompt the user using ``text``; ``choices`` is a list + of tuples of form ``(value to return, description)``, where + ``value to return`` is the value that this function will + return. + """ + raise NotImplementedError + def passwordbox(self, text, **kwargs): + """ + Securely requests a password from the user. Prompts the user + using ``text``; return value is the password. + """ + raise NotImplementedError + def msgbox(self, text, **kwargs): + """ + Gives the user a message that they must dismiss before proceeding. + """ + raise NotImplementedError + def infobox(self, text, **kwargs): + """ + Gives the user a non-blocking message; useful if you are about + to do an operation that will take some time. + """ + raise NotImplementedError + @decorator.decorator def dialog_wrap(f, self, text, *args, **kwargs): + """ + Convenience decorator that automatically: + + 1. Removes already handled keyword arguments, + 2. Configures the dimensions of the dialog box, and + 3. Handles the different ext possibilities of dialog. + """ if 'cmdopt' in kwargs: del kwargs['cmdopt'] if 'width' not in kwargs and 'height' not in kwargs: kwargs["width"], kwargs["height"] = guess_dimensions(text) @@ -68,11 +136,14 @@ def dialog_wrap(f, self, text, *args, **kwargs): raise DialogError(exit) return value -class Dialog(object): - interactive = True +class Dialog(PromptInterface): """Ncurses interface using dialog.""" + interactive = True def __init__(self): self.dialog = dialog.Dialog() + exit = self.dialog.infobox("Setting up...") + if exit != 0: + raise UnsupportedTerminal @dialog_wrap def inputbox(self, *args, **kwargs): kwargs.setdefault('initerror', "You cannot edit initial value; please type characters after it.") @@ -80,12 +151,12 @@ class Dialog(object): del kwargs['initerror'] kwargs['height'] += 5 # for the text box exit, result = self.dialog.inputbox(*args, **kwargs) - if exit == self.dialog.DIALOG_OK: + if exit == self.dialog.DIALOG_OK: # pylint: disable-msg=E1101 # do some funny munging kwargs.setdefault('init', '') if result[0:len(kwargs['init'])] != kwargs['init']: self.msgbox(initerror, height=10, width=50) - exit = self.dialog.DIALOG_OK + exit = self.dialog.DIALOG_OK # pylint: disable-msg=E1101 result = self.inputbox(*args, initerror=initerror, **kwargs) else: result = result[len(kwargs['init']):] @@ -108,12 +179,14 @@ class Dialog(object): @decorator.decorator def prompt_wrap(f, self, *args, **kwargs): + """Convenience decorator that handles end-of-document and interrupts.""" try: return f(self, *args, **kwargs) except (EOFError, KeyboardInterrupt): raise UserCancel -class Prompt(object): +class Prompt(PromptInterface): + """Simple stdin/stdout prompt object.""" interactive = True @prompt_wrap def inputbox(self, text, init='', **kwargs): @@ -146,7 +219,7 @@ class Prompt(object): print "" print fill(text.strip()) -class FailPrompt(object): +class FailPrompt(PromptInterface): """ Prompt that doesn't actually ask the user; just fails with an error message. @@ -169,11 +242,13 @@ class FailPrompt(object): print fill(text.strip(), break_long_words=False) class Error(wizard.Error): + """Base error class.""" pass class MissingRequiredParam(Error): """Non-interactive, but we needed more info.""" def __init__(self, cmdopt): + """``cmdopt`` is the command line option that should be specified.""" self.cmdopt = cmdopt def __str__(self): return """ @@ -188,6 +263,7 @@ class UserCancel(Error): class DialogError(Error): """Dialog returned a mysterious error.""" def __init__(self, exit): + """``exit`` is the mysterious exit code.""" self.exitcode = exit def __str__(self): return """ @@ -197,3 +273,6 @@ send mail to scripts@mit.edu with the contents of this error message and the preceding backtrace. """ % self.exitcode +class UnsupportedTerminal(Error): + """It doesn't look like we support this terminal. Internal error.""" + pass