1# -*- test-case-name: twisted.test.test_formmethod -*- 2# Copyright (c) Twisted Matrix Laboratories. 3# See LICENSE for details. 4 5 6""" 7Form-based method objects. 8 9This module contains support for descriptive method signatures that can be used 10to format methods. 11""" 12 13import calendar 14 15class FormException(Exception): 16 """An error occurred calling the form method. 17 """ 18 def __init__(self, *args, **kwargs): 19 Exception.__init__(self, *args) 20 self.descriptions = kwargs 21 22 23class InputError(FormException): 24 """ 25 An error occurred with some input. 26 """ 27 28 29class Argument: 30 """Base class for form arguments.""" 31 32 # default value for argument, if no other default is given 33 defaultDefault = None 34 35 def __init__(self, name, default=None, shortDesc=None, 36 longDesc=None, hints=None, allowNone=1): 37 self.name = name 38 self.allowNone = allowNone 39 if default is None: 40 default = self.defaultDefault 41 self.default = default 42 self.shortDesc = shortDesc 43 self.longDesc = longDesc 44 if not hints: 45 hints = {} 46 self.hints = hints 47 48 def addHints(self, **kwargs): 49 self.hints.update(kwargs) 50 51 def getHint(self, name, default=None): 52 return self.hints.get(name, default) 53 54 def getShortDescription(self): 55 return self.shortDesc or self.name.capitalize() 56 57 def getLongDescription(self): 58 return self.longDesc or '' #self.shortDesc or "The %s." % self.name 59 60 def coerce(self, val): 61 """Convert the value to the correct format.""" 62 raise NotImplementedError("implement in subclass") 63 64 65class String(Argument): 66 """A single string. 67 """ 68 defaultDefault = '' 69 min = 0 70 max = None 71 72 def __init__(self, name, default=None, shortDesc=None, 73 longDesc=None, hints=None, allowNone=1, min=0, max=None): 74 Argument.__init__(self, name, default=default, shortDesc=shortDesc, 75 longDesc=longDesc, hints=hints, allowNone=allowNone) 76 self.min = min 77 self.max = max 78 79 def coerce(self, val): 80 s = str(val) 81 if len(s) < self.min: 82 raise InputError("Value must be at least %s characters long" % self.min) 83 if self.max != None and len(s) > self.max: 84 raise InputError("Value must be at most %s characters long" % self.max) 85 return str(val) 86 87 88class Text(String): 89 """A long string. 90 """ 91 92 93class Password(String): 94 """A string which should be obscured when input. 95 """ 96 97 98class VerifiedPassword(String): 99 """A string that should be obscured when input and needs verification.""" 100 101 def coerce(self, vals): 102 if len(vals) != 2 or vals[0] != vals[1]: 103 raise InputError("Please enter the same password twice.") 104 s = str(vals[0]) 105 if len(s) < self.min: 106 raise InputError("Value must be at least %s characters long" % self.min) 107 if self.max != None and len(s) > self.max: 108 raise InputError("Value must be at most %s characters long" % self.max) 109 return s 110 111 112class Hidden(String): 113 """A string which is not displayed. 114 115 The passed default is used as the value. 116 """ 117 118 119class Integer(Argument): 120 """A single integer. 121 """ 122 defaultDefault = None 123 124 def __init__(self, name, allowNone=1, default=None, shortDesc=None, 125 longDesc=None, hints=None): 126 #although Argument now has allowNone, that was recently added, and 127 #putting it at the end kept things which relied on argument order 128 #from breaking. However, allowNone originally was in here, so 129 #I have to keep the same order, to prevent breaking code that 130 #depends on argument order only 131 Argument.__init__(self, name, default, shortDesc, longDesc, hints, 132 allowNone) 133 134 def coerce(self, val): 135 if not val.strip() and self.allowNone: 136 return None 137 try: 138 return int(val) 139 except ValueError: 140 raise InputError("%s is not valid, please enter a whole number, e.g. 10" % val) 141 142 143class IntegerRange(Integer): 144 145 def __init__(self, name, min, max, allowNone=1, default=None, shortDesc=None, 146 longDesc=None, hints=None): 147 self.min = min 148 self.max = max 149 Integer.__init__(self, name, allowNone=allowNone, default=default, shortDesc=shortDesc, 150 longDesc=longDesc, hints=hints) 151 152 def coerce(self, val): 153 result = Integer.coerce(self, val) 154 if self.allowNone and result == None: 155 return result 156 if result < self.min: 157 raise InputError("Value %s is too small, it should be at least %s" % (result, self.min)) 158 if result > self.max: 159 raise InputError("Value %s is too large, it should be at most %s" % (result, self.max)) 160 return result 161 162 163class Float(Argument): 164 165 defaultDefault = None 166 167 def __init__(self, name, allowNone=1, default=None, shortDesc=None, 168 longDesc=None, hints=None): 169 #although Argument now has allowNone, that was recently added, and 170 #putting it at the end kept things which relied on argument order 171 #from breaking. However, allowNone originally was in here, so 172 #I have to keep the same order, to prevent breaking code that 173 #depends on argument order only 174 Argument.__init__(self, name, default, shortDesc, longDesc, hints, 175 allowNone) 176 177 178 def coerce(self, val): 179 if not val.strip() and self.allowNone: 180 return None 181 try: 182 return float(val) 183 except ValueError: 184 raise InputError("Invalid float: %s" % val) 185 186 187class Choice(Argument): 188 """ 189 The result of a choice between enumerated types. The choices should 190 be a list of tuples of tag, value, and description. The tag will be 191 the value returned if the user hits "Submit", and the description 192 is the bale for the enumerated type. default is a list of all the 193 values (seconds element in choices). If no defaults are specified, 194 initially the first item will be selected. Only one item can (should) 195 be selected at once. 196 """ 197 def __init__(self, name, choices=[], default=[], shortDesc=None, 198 longDesc=None, hints=None, allowNone=1): 199 self.choices = choices 200 if choices and not default: 201 default.append(choices[0][1]) 202 Argument.__init__(self, name, default, shortDesc, longDesc, hints, allowNone=allowNone) 203 204 def coerce(self, inIdent): 205 for ident, val, desc in self.choices: 206 if ident == inIdent: 207 return val 208 else: 209 raise InputError("Invalid Choice: %s" % inIdent) 210 211 212class Flags(Argument): 213 """ 214 The result of a checkbox group or multi-menu. The flags should be a 215 list of tuples of tag, value, and description. The tag will be 216 the value returned if the user hits "Submit", and the description 217 is the bale for the enumerated type. default is a list of all the 218 values (second elements in flags). If no defaults are specified, 219 initially nothing will be selected. Several items may be selected at 220 once. 221 """ 222 def __init__(self, name, flags=(), default=(), shortDesc=None, 223 longDesc=None, hints=None, allowNone=1): 224 self.flags = flags 225 Argument.__init__(self, name, default, shortDesc, longDesc, hints, allowNone=allowNone) 226 227 def coerce(self, inFlagKeys): 228 if not inFlagKeys: 229 return [] 230 outFlags = [] 231 for inFlagKey in inFlagKeys: 232 for flagKey, flagVal, flagDesc in self.flags: 233 if inFlagKey == flagKey: 234 outFlags.append(flagVal) 235 break 236 else: 237 raise InputError("Invalid Flag: %s" % inFlagKey) 238 return outFlags 239 240 241class CheckGroup(Flags): 242 pass 243 244 245class RadioGroup(Choice): 246 pass 247 248 249class Boolean(Argument): 250 def coerce(self, inVal): 251 if not inVal: 252 return 0 253 lInVal = str(inVal).lower() 254 if lInVal in ('no', 'n', 'f', 'false', '0'): 255 return 0 256 return 1 257 258class File(Argument): 259 def __init__(self, name, allowNone=1, shortDesc=None, longDesc=None, 260 hints=None): 261 Argument.__init__(self, name, None, shortDesc, longDesc, hints, 262 allowNone=allowNone) 263 264 def coerce(self, file): 265 if not file and self.allowNone: 266 return None 267 elif file: 268 return file 269 else: 270 raise InputError("Invalid File") 271 272def positiveInt(x): 273 x = int(x) 274 if x <= 0: raise ValueError 275 return x 276 277class Date(Argument): 278 """A date -- (year, month, day) tuple.""" 279 280 defaultDefault = None 281 282 def __init__(self, name, allowNone=1, default=None, shortDesc=None, 283 longDesc=None, hints=None): 284 Argument.__init__(self, name, default, shortDesc, longDesc, hints) 285 self.allowNone = allowNone 286 if not allowNone: 287 self.defaultDefault = (1970, 1, 1) 288 289 def coerce(self, args): 290 """Return tuple of ints (year, month, day).""" 291 if tuple(args) == ("", "", "") and self.allowNone: 292 return None 293 294 try: 295 year, month, day = map(positiveInt, args) 296 except ValueError: 297 raise InputError("Invalid date") 298 if (month, day) == (2, 29): 299 if not calendar.isleap(year): 300 raise InputError("%d was not a leap year" % year) 301 else: 302 return year, month, day 303 try: 304 mdays = calendar.mdays[month] 305 except IndexError: 306 raise InputError("Invalid date") 307 if day > mdays: 308 raise InputError("Invalid date") 309 return year, month, day 310 311 312class Submit(Choice): 313 """Submit button or a reasonable facsimile thereof.""" 314 315 def __init__(self, name, choices=[("Submit", "submit", "Submit form")], 316 reset=0, shortDesc=None, longDesc=None, allowNone=0, hints=None): 317 Choice.__init__(self, name, choices=choices, shortDesc=shortDesc, 318 longDesc=longDesc, hints=hints) 319 self.allowNone = allowNone 320 self.reset = reset 321 322 def coerce(self, value): 323 if self.allowNone and not value: 324 return None 325 else: 326 return Choice.coerce(self, value) 327 328 329class PresentationHint: 330 """ 331 A hint to a particular system. 332 """ 333 334 335class MethodSignature: 336 337 def __init__(self, *sigList): 338 """ 339 """ 340 self.methodSignature = sigList 341 342 def getArgument(self, name): 343 for a in self.methodSignature: 344 if a.name == name: 345 return a 346 347 def method(self, callable, takesRequest=False): 348 return FormMethod(self, callable, takesRequest) 349 350 351class FormMethod: 352 """A callable object with a signature.""" 353 354 def __init__(self, signature, callable, takesRequest=False): 355 self.signature = signature 356 self.callable = callable 357 self.takesRequest = takesRequest 358 359 def getArgs(self): 360 return tuple(self.signature.methodSignature) 361 362 def call(self,*args,**kw): 363 return self.callable(*args,**kw) 364