Package qanda :: Module session
[hide private]
[frames] | no frames]

Source Code for Module qanda.session

  1  """ 
  2  A round of prompting the users for, and validating, answers. 
  3   
  4  These provide a simple, consistent and robust way of formatting prompts for 
  5  gathering information from a commandline user and validating their answers. 
  6  Users are prompted with a question and optionally explanatory help text and 
  7  hints of possible answers. 
  8   
  9  A question is usually formatted as follows:: 
 10   
 11          helptext ... (multiple lines if need be) ... helptext 
 12          question (hints) [default]: 
 13   
 14  Multiple choice questions are formatted as:: 
 15   
 16          helptext ... (multiple lines if need be) ... helptext 
 17          1. choice 
 18          2. choice 
 19          ... 
 20          N. choice 
 21          question (hints) [default]: 
 22   
 23  """ 
 24  # TODO: add readline support for better editting? 
 25   
 26  __docformat__ = "restructuredtext en" 
 27   
 28   
 29  ### IMPORTS 
 30   
 31  import types 
 32   
 33  import defs 
 34  import validators 
 35   
 36  __all__ = [ 
 37          'Session', 
 38          'prompt', 
 39  ] 
 40   
 41   
 42  ### CONSTANTS & DEFINES 
 43   
 44  ### IMPLEMENTATION ### 
 45   
46 -class Session (object):
47 # XXX: in future, this may include initialization of readline etc. 48 "" 49
50 - def __init__ (self):
51 self.choice_delim = '/'
52 53 ## Questions:
54 - def string (self, question, converters=[], help=None, hints=None, 55 default=None, convert_default=True, 56 strip_flanking_space=False):
57 """ 58 Ask for and return text from the user. 59 60 The simplest public question function and the basis of many of the others, 61 this is a thin wrapper around the core `_ask` method that 62 63 """ 64 return self._ask (question, 65 converters=converters, 66 help=help, 67 hints=hints, 68 default=default, 69 strip_flanking_space=strip_flanking_space, 70 multiline=False, 71 )
72
73 - def text (self, question, converters=[], help=None, hints=None, default=None, 74 strip_flanking_space=False):
75 """ 76 Ask for and return text from the user. 77 78 The simplest public question function and the basis of many of the others, 79 this is a thin wrapper around the core `_ask` method that allows for 80 multi-line responses. 81 82 """ 83 return self._ask (question, 84 converters=[], 85 help=help, 86 hints=hints, 87 default=default, 88 strip_flanking_space=strip_flanking_space, 89 multiline=True, 90 )
91
92 - def integer (self, question, converters=[], help=None, hints=None, 93 default=None, convert_default=True, min=None, max=None):
94 return self.string (question, 95 converters=[validators.ToInt(), validators.Range (min, max)] + converters, 96 help=help, 97 hints=hints, 98 default=default, 99 convert_default=convert_default, 100 strip_flanking_space=True, 101 )
102 103
104 - def short_choice (self, question, choice_str, converters=[], help=None, default=None):
105 """ 106 Ask the user to make a choice using single letters. 107 """ 108 ## Preconditions: 109 choice_str = choice_str.strip().lower() 110 assert choice_str, "need choices for question" 111 if default: 112 default = default.lower() 113 assert (len(default) == 1), \ 114 "ask_short_choice uses only single letters, not '%s'" % default 115 ## Main: 116 hints = choice_str 117 ## Postconditions & return: 118 return self._ask (question, 119 converters= converters or [validators.Vocab(list(choice_str))], 120 help=help, hints=hints, default=default)
121 122
123 - def yesno (self, question, help=None, default=None):
124 choice_str = 'yn' 125 return self.short_choice (question, choice_str, 126 converters=[ 127 lambda s: s.strip().lower(), 128 validators.Synonyms(YESNO_SYNONYMS), 129 Vocab(list(choice_str)), 130 ], 131 help=help, 132 default=default, 133 )
134
135 - def ask_long_choice (self, question, choices, help=None, default=None):
136 """ 137 Ask the user to make a choice from a list. 138 139 """ 140 ## Preconditions: 141 assert choices, "need choices for question" 142 if default: 143 default = default.lower() 144 ## Main: 145 # build choices list 146 synonyms = {} 147 vocab = [] 148 menu = [] 149 for i, c in enumerate (choices): 150 if isinstance (c, basestring): 151 val = c 152 desc = c 153 syns = [] 154 elif instance_of (c, Choice): 155 val = c.value 156 desc = c.desc or value 157 syns = c.syns 158 else: 159 assert false, "shouldn't get here" 160 assert val not in vocab, "duplicate choice value '%s'" % val 161 vocab.append (val) 162 menu_index = str(i + 1) 163 syns.append(menu_index) 164 for s in syns: 165 assert not synonyms.has_key(s), "duplicate choice synonym '%s'" % s 166 synonyms[s] = val 167 menu.append (" %s. %s" % (menu_index, desc)) 168 help = '\n'.join([help]+ menu).strip() 169 170 ## Postconditions & return: 171 return self._ask (question, 172 converters=[ 173 Synonyms(synonyms), 174 Vocab(vocab) 175 ], 176 help=help, 177 hints='1-%s' % len(choices), 178 default=default 179 )
180 181 ## Internals
182 - def _ask (self, question, converters=[], help=None, choices=[], hints=None, 183 default=None, convert_default=True, multiline=False, 184 strip_flanking_space=True):
185 """ 186 Ask for and return an answer from the user. 187 188 :Parameters: 189 question 190 The text of the question asked. 191 converters 192 An array of conversion and validation functions to be called in 193 sucession with the results of the previous. If any throws an error, 194 it will be caught and the question asked again. 195 help 196 Introductory text to be shown before the question. 197 hints 198 Short reminder text of possible answers. 199 default 200 The value the answer will be set to before processing if a blank 201 answer (i.e. just hitting return) is entered. 202 convert_default 203 If the default value is used, it will be processed through the 204 converters. Otherwise it will be dircetly returned. 205 strip_flanking_space 206 If true, flanking space will be stripped from the answer before it is 207 processed. 208 209 This is the underlying function for getting information from the user. It 210 prints the help text (if any), any menu of choices, prints the question 211 and hints and then waits for input from the user. All answers are fed from 212 the converters. If conversion fails, the question is re-asked. 213 214 The following sequence is used in processing user answers: 215 216 1. The raw input is read 217 2. If the options is set, flanking space is stripped 218 3. If the input is an empty string and a default answer is given: 219 220 1. if convert_default is set, the input is set to that value (i.e. 221 the default answer must be a valid input value) 222 223 2. else return default value immediately (bypass conversion) 224 225 4. The input is feed through each converter in turn, with the the result 226 of one feeding into the next. 227 5. If the conversion raises an error, the question is asked again 228 6. Otherwise the processed answer is returned 229 230 """ 231 # XXX: the convert_default and default handling is a little tricksy: 232 # - you can't return a default value of None (without some fancy 233 # converting), because None is intepreted as no default 234 # - It makes sense to process/convert the default value, as this ensures 235 # that the default value is valid (converts correctly) and the printed 236 # value can be different to the returned value. 237 # - However this makes some queries difficult, like "ask for an integer 238 # or return False", where the default value is of a different type. Thus 239 # the (occasional) need for `convert_default=False`. 240 241 ## Preconditions: 242 assert (question), "'ask' requires a question" 243 244 ## Main: 245 # show leadin 246 if help: 247 print self._clean_text (help) 248 for c in choices: 249 print " %s" % c.lstrip() 250 251 # build actual question line 252 question_str = self._clean_text ("%s%s: " % ( 253 question, self._format_hints_text (hints, default))) 254 255 # ask question until you get a valid answer 256 while True: 257 if multiline: 258 raw_answer = self.read_input_multiline (question_str) 259 else: 260 raw_answer = self.read_input_line (question_str) 261 if strip_flanking_space: 262 raw_answer = raw_answer.strip() 263 # if the answer is blank and a default has been supplied 264 # NOTE: makes it impossible to have a default value of None 265 if (raw_answer == '') and (default is not None): 266 if convert_default: 267 # feed default through converters 268 raw_answer = default 269 else: 270 # return default value immmediately 271 return default 272 try: 273 for conv in converters: 274 raw_answer = conv.__call__ (raw_answer) 275 except StandardError, err: 276 print "A problem: %s. Try again ..." % err 277 except: 278 print "A problem: unknown error. Try again ..." 279 else: 280 return raw_answer
281
282 - def _clean_text (self, text):
283 """ 284 Trim, un-wrap and rewrap text to be presented to the user. 285 """ 286 # XXX: okay so we don't wrap it. Should we? And is there text we don't 287 # want to wrap? 288 return defs.SPACE_RE.sub (' ', text.strip())
289 290
291 - def _format_hints_text (self, hints=None, default=None):
292 """ 293 Consistently format hints and default values for inclusion in questions. 294 295 The hints section of the questions is formatted as:: 296 297 (hint text) [default value text] 298 299 If hints or the default value are not supplied (i.e. they are set to None) 300 that section does not appear. If neither is supplied, an empty string is 301 returned. 302 303 Some heuristics are used in presentation. If the hints is a list (e.g. of 304 possible choices), these are formatted as a comma delimited list. If the 305 default value is a blank string, '' is given to make this explicit. 306 307 Note this does not check if the default is a valid value. 308 309 For example:: 310 311 >>> print prompt._format_hints_text() 312 <BLANKLINE> 313 >>> print prompt._format_hints_text([1, 2, 3], 'foo') 314 (1,2,3) [foo] 315 >>> print prompt._format_hints_text('1-3', '') 316 (1-3) [''] 317 >>> print prompt._format_hints_text('an integer') 318 (an integer) 319 320 """ 321 hints_str = '' 322 if hints is not None: 323 # if hints is an array, join the contents 324 if type(hints) in [types.ListType, types.TupleType]: 325 hints = self.choice_delim.join (['%s' % x for x in hints]) 326 hints_str = ' (%s)' % hints 327 if default is not None: 328 # quote empty default strings for clarity 329 if default is '': 330 default = "''" 331 hints_str += ' [%s]' % default 332 ## Postconditions % return: 333 return hints_str
334
335 - def read_input_line (self, prompt):
336 """ 337 Read and return a single line of user input. 338 339 Input is terminated by return or enter (which is stripped). 340 """ 341 # raw_input uses readline if available 342 return raw_input(prompt)
343
344 - def read_input_multiline (self, prompt):
345 """ 346 Read and return multiple lines of user input. 347 348 Input is terminated by two blank lines. Input is returned as a multiline 349 string, with newlines at the linebreaks. 350 """ 351 # TODO: be nice to make end condition flexible. 352 # NOTE: because raw_input can use readline, the readline up-arrow can 353 # "paste-in" multiple lines of text in one go. So a bit of post-parsing 354 # is required. 355 # NOTE: we don't even attempt to cope with anything but unix yet 356 line_arr = [self.read_input_line (prompt)] 357 if line_arr[0] == '': 358 line_arr = [] 359 else: 360 while True: 361 line_arr.append(self.read_input_line ('... ')) 362 if line_arr[-2:] == ['', '']: 363 line_arr = line_arr[:-2] 364 break 365 clean_arr = [] 366 for line in line_arr: 367 clean_arr.extend (line.split('\n')) 368 return '\n'.join (clean_arr)
369 370 371 # An always available session 372 prompt = Session() 373 374 375 376 ## DEBUG & TEST ### 377 378 if __name__ == "__main__": 379 import doctest 380 doctest.testmod() 381 382 383 ### END ####################################################################### 384