"""
-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
import decorator
import textwrap
import getpass
+import os
import wizard
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
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)
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.")
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']):]
@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):
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.
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 """
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 """
message and the preceding backtrace.
""" % self.exitcode
+class UnsupportedTerminal(Error):
+ """It doesn't look like we support this terminal. Internal error."""
+ pass