]> scripts.mit.edu Git - wizard.git/blob - wizard/prompt.py
Set admin e-mail address properly on MediaWiki >= 1.18.0
[wizard.git] / wizard / prompt.py
1 """
2 Interface compatible with :class:`PromptInterface` for doing
3 non-ncurses interaction.
4
5 By convention, the last line of a text parameter should be
6 a short value with a trailing colon so that we can prompt a user
7 for a value immediately after it.
8
9 .. testsetup:: *
10
11     from wizard.prompt import *
12 """
13
14 import sys
15 import readline
16 import decorator
17 import textwrap
18 import getpass
19 import os
20
21 import wizard
22
23 try:
24     import dialog
25     has_dialog = True
26 except ImportError:
27     has_dialog = False
28
29 def fill(text, width=60, **kwargs):
30     """
31     Convenience wrapper for :func:`textwrap.fill` that preserves
32     paragraphs.
33     """
34     return "\n\n".join(textwrap.fill(p, width=width, **kwargs) for p in text.split("\n\n"))
35
36 def guess_dimensions(text, width=60):
37     """
38     Guesses the dimensions that any given piece of text will
39     need to display on terminal, given some width.
40     """
41     # +1 for the fact that there's no trailing newline from fill
42     # +2 for the borders
43     # +1 as a buffer in case we underestimate
44     print fill(text)
45     return width, fill(text, width-2).count("\n") + 1 + 2 + 1
46
47 def make(prompt, non_interactive):
48     """
49     Makes a :class:`dialog.Dialog` compatible class based on
50     configuration.
51     """
52     if non_interactive:
53         return FailPrompt()
54     if prompt or os.getenv('TERM') == 'dumb' or not has_dialog:
55         return Prompt()
56     try:
57         return Dialog()
58     except (dialog.ExecutableNotFound, UnsupportedTerminal):
59         return Prompt()
60
61 def join_or(items):
62     """
63     Joins a list of disjunctions into a human readable sentence.
64
65     >>> join_or(['foo'])
66     'foo'
67     >>> join_or(['foo', 'bar', 'baz'])
68     'foo, bar or baz'
69     """
70     if len(items) == 0:
71         raise ValueError
72     elif len(items) == 1:
73         return items[0]
74     return ', '.join(items[:-1]) + ' or ' + items[-1]
75
76 class PromptInterface(object):
77     def inputbox(self, text, init='', **kwargs):
78         """
79         Request a free-form, single line of text from the user.
80         Prompt the user using ``text``; and ``init`` is the
81         initial value filling the field; not all implementations
82         support editing ``init``.  Returns the typed string.
83         """
84         raise NotImplementedError
85     def menu(self, text, choices=[], **kwargs):
86         """
87         Request a selection from a number of choices from the user.
88         Prompt the user using ``text``; ``choices`` is a list
89         of tuples of form ``(value to return, description)``, where
90         ``value to return`` is the value that this function will
91         return.
92         """
93         raise NotImplementedError
94     def passwordbox(self, text, **kwargs):
95         """
96         Securely requests a password from the user.  Prompts the user
97         using ``text``; return value is the password.
98         """
99         raise NotImplementedError
100     def msgbox(self, text, **kwargs):
101         """
102         Gives the user a message that they must dismiss before proceeding.
103         """
104         raise NotImplementedError
105     def infobox(self, text, **kwargs):
106         """
107         Gives the user a non-blocking message; useful if you are about
108         to do an operation that will take some time.
109         """
110         raise NotImplementedError
111
112 @decorator.decorator
113 def dialog_wrap(f, self, text, *args, **kwargs):
114     """
115     Convenience decorator that automatically:
116
117         1. Removes already handled keyword arguments,
118         2. Configures the dimensions of the dialog box, and
119         3. Handles the different ext possibilities of dialog.
120     """
121     if 'cmdopt' in kwargs: del kwargs['cmdopt']
122     if 'width' not in kwargs and 'height' not in kwargs:
123         kwargs["width"], kwargs["height"] = guess_dimensions(text)
124     elif 'width' in kwargs and 'height' not in kwargs:
125         kwargs["width"], kwargs["height"] = guess_dimensions(text, width=kwargs["width"])
126     result = f(self, text, *args, **kwargs)
127     if not isinstance(result, tuple):
128         exit = result
129         value = None
130     else:
131         exit, value = result
132     if exit == self.dialog.DIALOG_CANCEL or exit == self.dialog.DIALOG_ESC:
133         raise UserCancel
134     elif exit != self.dialog.DIALOG_OK:
135         # XXX: We don't support stuff like DIALOG_EXTRA or DIALOG_HELP
136         raise DialogError(exit)
137     return value
138
139 class Dialog(PromptInterface):
140     """Ncurses interface using dialog."""
141     interactive = True
142     def __init__(self):
143         self.dialog = dialog.Dialog()
144         exit = self.dialog.infobox("Setting up...")
145         if exit != 0:
146             raise UnsupportedTerminal
147     @dialog_wrap
148     def inputbox(self, *args, **kwargs):
149         kwargs.setdefault('initerror', "You cannot edit initial value; please type characters after it.")
150         initerror = kwargs['initerror']
151         del kwargs['initerror']
152         kwargs['height'] += 5 # for the text box
153         exit, result = self.dialog.inputbox(*args, **kwargs)
154         if exit == self.dialog.DIALOG_OK: # pylint: disable-msg=E1101
155             # do some funny munging
156             kwargs.setdefault('init', '')
157             if result[0:len(kwargs['init'])] != kwargs['init']:
158                 self.msgbox(initerror, height=10, width=50)
159                 exit = self.dialog.DIALOG_OK # pylint: disable-msg=E1101
160                 result = self.inputbox(*args, initerror=initerror, **kwargs)
161             else:
162                 result = result[len(kwargs['init']):]
163         return (exit, result)
164     @dialog_wrap
165     def menu(self, *args, **kwargs):
166         kwargs['height'] += 6 + len(kwargs['choices']) # for the border and menu entries
167         return self.dialog.menu(*args, **kwargs)
168     @dialog_wrap
169     def msgbox(self, *args, **kwargs):
170         kwargs['height'] += 3
171         return self.dialog.msgbox(*args, **kwargs)
172     @dialog_wrap
173     def passwordbox(self, *args, **kwargs):
174         kwargs['height'] += 6
175         return self.dialog.passwordbox(*args, **kwargs)
176     @dialog_wrap
177     def infobox(self, text, **kwargs):
178         return self.dialog.infobox(text, **kwargs)
179
180 @decorator.decorator
181 def prompt_wrap(f, self, *args, **kwargs):
182     """Convenience decorator that handles end-of-document and interrupts."""
183     try:
184         return f(self, *args, **kwargs)
185     except (EOFError, KeyboardInterrupt):
186         raise UserCancel
187
188 class Prompt(PromptInterface):
189     """Simple stdin/stdout prompt object."""
190     interactive = True
191     @prompt_wrap
192     def inputbox(self, text, init='', **kwargs):
193         print ""
194         return raw_input(fill(text.strip()) + " " + init)
195     @prompt_wrap
196     def menu(self, text, choices=[], **kwargs):
197         print ""
198         print fill(text.strip())
199         values = list(choice[0] for choice in choices)
200         for choice in choices:
201             print "% 4s  %s" % (choice[0], choice[1])
202         while 1:
203             out = raw_input("Please enter %s: " % join_or(values))
204             if out not in values:
205                 print "'%s' is not a valid value" % out
206                 continue
207             return out
208     @prompt_wrap
209     def passwordbox(self, text, **kwargs):
210         print ""
211         return getpass.getpass(text + " ")
212     @prompt_wrap
213     def msgbox(self, text, **kwargs):
214         print ""
215         print fill(text.strip())
216         print "Press <Enter> to continue..."
217     @prompt_wrap
218     def infobox(self, text, **kwargs):
219         print ""
220         print fill(text.strip())
221
222 class FailPrompt(PromptInterface):
223     """
224     Prompt that doesn't actually ask the user; just fails with
225     an error message.
226     """
227     interactive = False
228     def inputbox(self, *args, **kwargs):
229         kwargs.setdefault('cmdopt', '(unknown)')
230         raise MissingRequiredParam(kwargs['cmdopt'])
231     def passwordbox(self, *args, **kwargs):
232         kwargs.setdefault('cmdopt', '(unknown)')
233         raise MissingRequiredParam(kwargs['cmdopt'])
234     def menu(self, *args, **kwargs):
235         kwargs.setdefault('cmdopt', '(unknown)')
236         raise MissingRequiredParam(kwargs['cmdopt'])
237     def msgbox(self, text, **kwargs):
238         print ""
239         print fill(text.strip(), break_long_words=False)
240     def infobox(self, text, **kwargs):
241         print ""
242         print fill(text.strip(), break_long_words=False)
243
244 class Error(wizard.Error):
245     """Base error class."""
246     pass
247
248 class MissingRequiredParam(Error):
249     """Non-interactive, but we needed more info."""
250     def __init__(self, cmdopt):
251         """``cmdopt`` is the command line option that should be specified."""
252         self.cmdopt = cmdopt
253     def __str__(self):
254         return """
255
256 ERROR: Missing required parameter, try specifying %s""" % self.cmdopt
257
258 class UserCancel(Error):
259     """User canceled the input process."""
260     def __str__(self):
261         return "\nAborting installation process; no changes were made"
262
263 class DialogError(Error):
264     """Dialog returned a mysterious error."""
265     def __init__(self, exit):
266         """``exit`` is the mysterious exit code."""
267         self.exitcode = exit
268     def __str__(self):
269         return """
270
271 ERROR:  Dialog returned a mysterious exit code %d.""" % self.exitcode
272
273 class UnsupportedTerminal(Error):
274     """It doesn't look like we support this terminal.  Internal error."""
275     pass