Package csb :: Package apps
[frames] | no frames]

Source Code for Package csb.apps

  1  """ 
  2  Root package for all executable CSB client programs. 
  3   
  4  Introduction 
  5  ============ 
  6   
  7  There are roughly three types of CSB apps: 
  8       
  9      1. protocols: client applications, which make use of the core library 
 10         to perform some action     
 11      2. wrappers: these provide python bindings for external programs 
 12      3. mixtures of (1) and (2). 
 13   
 14  The main design goal of this framework is to provide a way for writing 
 15  executable code with minimal effort, without the hassle of repeating yourself 
 16  over and over again. Creating a professional-grade CLI, validating and 
 17  consuming the command line arguments is therefore really straightforward. 
 18  On the other hand, one frequently feels the need to reuse some apps or their 
 19  components in other apps. For such reasons, a CSB L{Application} is just a 
 20  regular, importable python object, which never communicates directly with the 
 21  command line interface or calls sys.exit(). The app's associated L{AppRunner} 
 22  will take care of those things.       
 23   
 24  Getting Started 
 25  =============== 
 26   
 27  Follow these simple steps to write a new CSB app: 
 28   
 29      1. Create the app module in the C{csb.apps} package. 
 30       
 31      2. Create a main class and derive it from L{csb.apps.Application}. You need 
 32      to implement the L{csb.apps.Application.main()} abstract method - this is 
 33      the app's entry point. You have the L{csb.apps.Application.args} object at 
 34      your disposal. 
 35       
 36      3. Create an AppRunner class, derived from csb.apps.AppRunner. You need to 
 37      implement the following methods and properties: 
 38       
 39          - property L{csb.apps.AppRunner.target} -- just return YourApp's class 
 40          - method L{csb.apps.AppRunner.command_line()} -- make an instance of 
 41          L{csb.apps.ArgHandler}, define your command line parameters on that 
 42          instance and return it 
 43          - optionally, override L{csb.apps.AppRunner.initapp(args)} if you need 
 44          to customize the instantiation of the main app class, or to perform 
 45          additional checks on the parsed application C{args} and eventually call 
 46          C{YourApp.exit()}. Return an instance of your app at the end  
 47       
 48      4. Make it executable:: 
 49          if __name__ == '__main__': 
 50              MyAppRunner().run() 
 51       
 52  See L{csb.apps.helloworld} for a sample implementation.     
 53  """ 
 54   
 55  import os 
 56  import re 
 57  import sys 
 58  import argparse 
 59  import traceback 
 60   
 61  from abc import ABCMeta, abstractmethod, abstractproperty 
62 63 64 -class ExitCodes(object):
65 """ 66 Exit code constants. 67 """ 68 CLEAN = 0 69 USAGE_ERROR = 1 70 CRASH = 99
71
72 -class AppExit(Exception):
73 """ 74 Used to signal an immediate application exit condition (e.g. a fatal error), 75 that propagates down to the client, instead of forcing the interpreter to 76 close via C{sys.exit()}. 77 78 @param message: exit message 79 @type message: str 80 @param code: exit code (see L{ExitCodes} for common constants) 81 @type code: int 82 @param usage: ask the app runner to print also the app's usage line 83 @type usage: bool 84 """ 85
86 - def __init__(self, message='', code=0, usage=False):
87 88 self.message = message 89 self.code = code 90 self.usage = usage
91
92 -class Application(object):
93 """ 94 Base CSB application class. 95 96 @param args: an object containing the application arguments 97 @type args: argparse.Namespace 98 """ 99 __metaclass__ = ABCMeta 100 101 USAGE = '' 102 HELP = '' 103
104 - def __init__(self, args, log=sys.stdout):
105 106 self.__args = None 107 self.__log = log 108 109 self.args = args
110 111 @property
112 - def args(self):
113 """ 114 The object containing application's arguments, as returned by the 115 command line parser. 116 """ 117 return self.__args
118 @args.setter
119 - def args(self, args):
120 self.__args = args
121 122 @abstractmethod
123 - def main(self):
124 """ 125 The main application hook. 126 """ 127 pass
128
129 - def log(self, message, ending='\n'):
130 """ 131 Write C{message} to the logging stream and flush it. 132 133 @param message: message 134 @type message: str 135 """ 136 137 self.__log.write(message) 138 self.__log.write(ending) 139 self.__log.flush()
140 141 @staticmethod
142 - def exit(message, code=0, usage=False):
143 """ 144 Notify the app runner about an application exit. 145 146 @param message: exit message 147 @type message: str 148 @param code: exit code (see L{ExitCodes} for common constants) 149 @type code: int 150 @param usage: advise the client to show the usage line 151 @type usage: bool 152 153 @note: you re not supposed to use C{sys.exit()} for the same purpose. 154 It is L{AppRunner}'s responsibility to handle the real system 155 exit, if the application has been started as an executable. 156 Think about your app being executed by some Python client as a 157 regular Python class, imported from a module -- in that case you 158 only want to ask the client to terminate the app, not to kill 159 the whole interpreter. 160 """ 161 raise AppExit(message, code, usage)
162
163 -class AppRunner(object):
164 """ 165 A base abstract class for all application runners. Concrete sub-classes 166 must define their corresponding L{Application} using the L{self.target} 167 property and must customize the L{Application}'s command line parser using 168 L{self.command_line()}. 169 170 @param argv: the list of command line arguments passed to the program. By 171 default this is C{sys.argv}. 172 @type argv: tuple of str 173 """ 174 __metaclass__ = ABCMeta 175
176 - def __init__(self, argv=sys.argv):
177 178 self._module = argv[0] 179 self._program = os.path.basename(self.module) 180 self._args = argv[1:]
181 182 @property
183 - def module(self):
184 return self._module
185 186 @property
187 - def program(self):
188 return self._program
189 190 @property
191 - def args(self):
192 return self._args
193 194 @abstractproperty
195 - def target(self):
196 """ 197 Reference to the concrete L{Application} class to run. This is 198 an abstract property that couples the current C{AppRunner} to its 199 corresponding L{Application}. 200 201 @rtype: type (class reference) 202 """ 203 return Application
204 205 @abstractmethod
206 - def command_line(self):
207 """ 208 Command line factory: build a command line parser suitable for the 209 application. 210 This is a hook method that each concrete AppRunner must implement. 211 212 @return: a command line parser object which knows how to handle 213 C{sys.argv} in the context of the concrete application. See the 214 documentation of L{ArgHandler} for more info on how to define command 215 line arguments. 216 217 @rtype: L{ArgHandler} 218 """ 219 # null implementation (no cmd arguments): 220 return ArgHandler(self.program)
221
222 - def initapp(self, args):
223 """ 224 Hook method that controls the instantiation of the main app class. 225 If the application has a custom constructor, you can adjust the 226 app initialization by overriding this method. 227 228 @param args: an object containing the application arguments 229 @type args: argparse.Namespace 230 231 @return: the application instance 232 @rtype: L{Application} 233 """ 234 app = self.target 235 return app(args)
236
237 - def run(self):
238 """ 239 Get the L{self.command_line()} and run L{self.target}. Ensure clean 240 system exit. 241 """ 242 try: 243 app = self.target 244 cmd = self.command_line() 245 246 try: 247 assert issubclass(app, Application) 248 assert isinstance(cmd, ArgHandler) 249 250 args = cmd.parse(self.args) 251 app.USAGE = cmd.usage 252 app.HELP = cmd.help 253 254 self.initapp(args).main() 255 256 except AppExit as ae: 257 if ae.usage: 258 AppRunner.exit(ae.message, code=ae.code, usage=cmd.usage) 259 else: 260 AppRunner.exit(ae.message, code=ae.code) 261 262 except SystemExit as se: # this should never happen, but just in case 263 AppRunner.exit(se.message, code=se.code) 264 265 except Exception: 266 message = '{0} has crashed. Details: \n{1}'.format(self.program, traceback.format_exc()) 267 AppRunner.exit(message, code=ExitCodes.CRASH) 268 269 AppRunner.exit(code=ExitCodes.CLEAN)
270 271 @staticmethod
272 - def exit(message='', code=0, usage='', ending='\n'):
273 """ 274 Perform system exit. If the exit C{code} is 0, print all messages to 275 STDOUT, else write to STDERR. 276 277 @param message: message to print 278 @type message: str 279 @param code: application exit code 280 @type code: int 281 """ 282 283 ending = str(ending or '') 284 message = str(message or '') 285 stream = sys.stdout 286 287 if code > 0: 288 message = 'E#{0} {1}'.format(code, message) 289 stream = sys.stderr 290 291 if usage: 292 stream.write(usage.rstrip(ending)) 293 stream.write(ending) 294 if message: 295 stream.write(message) 296 stream.write(ending) 297 298 sys.exit(code)
299
300 -class ArgHandler(object):
301 """ 302 Command line argument handler. 303 304 @param program: (file)name of the program, usually sys.argv[0] 305 @type program: str 306 @param description: long description of the application, shown in help 307 pages. The usage line and the parameter lists are 308 generated automatically, so no need to put them here. 309 @type description: str 310 311 @note: a help argument (-h) is provided automatically. 312 """ 313 314 SHORT_PREFIX = '-' 315 LONG_PREFIX = '--' 316
317 - class Type(object):
318 319 POSITIONAL = 1 320 NAMED = 2
321
322 - def __init__(self, program, description=''):
323 324 self._argformat = re.compile('^[a-z][a-z0-9_-]*$', re.IGNORECASE) 325 self._optformat = re.compile('^[a-z0-9]$', re.IGNORECASE) 326 327 self._program = program 328 self._description = description 329 330 self._parser = argparse.ArgumentParser(prog=program, description=description)
331
332 - def _add(self, kind, name, shortname, *a, **k):
333 334 args = [] 335 kargs = dict(k) 336 337 if shortname is not None: 338 if not re.match(self._optformat, shortname): 339 raise ValueError('Invalid short option name: {0}.'.format(shortname)) 340 341 if kind == ArgHandler.Type.POSITIONAL: 342 args.append(shortname) 343 else: 344 args.append(ArgHandler.SHORT_PREFIX + shortname) 345 346 if name is not None or kind == ArgHandler.Type.POSITIONAL: 347 if not re.match(self._argformat, name): 348 raise ValueError('Malformed argument name: {0}.'.format(name)) 349 350 if kind == ArgHandler.Type.POSITIONAL: 351 args.append(name) 352 else: 353 args.append(ArgHandler.LONG_PREFIX + name) 354 355 assert len(args) in (1, 2) 356 args.extend(a) 357 358 self.parser.add_argument(*args, **kargs)
359
360 - def add_positional_argument(self, name, type, help, choices=None):
361 """ 362 Define a mandatory positional argument (an argument without a dash). 363 364 @param name: name of the argument (used in help only) 365 @type name: str 366 @param type: argument data type 367 @type type: type (type factory callable) 368 @param help: help text 369 @type help: str 370 @param choices: list of allowed argument values 371 @type choices: tuple 372 """ 373 self._add(ArgHandler.Type.POSITIONAL, name, None, 374 type=type, help=help, choices=choices)
375
376 - def add_array_argument(self, name, type, help, choices=None):
377 """ 378 Same as L{self.add_positional_argument()}, but allow unlimited number 379 of values to be specified on the command line. 380 381 @param name: name of the argument (used in help only) 382 @type name: str 383 @param type: argument data type 384 @type type: type (type factory callable) 385 @param help: help text 386 @type help: str 387 @param choices: list of allowed argument values 388 @type choices: tuple 389 """ 390 self._add(ArgHandler.Type.POSITIONAL, name, None, 391 type=type, help=help, choices=choices, nargs=argparse.ONE_OR_MORE)
392
393 - def add_boolean_option(self, name, shortname, help, default=False):
394 """ 395 Define an optional switch (a dashed argument with no value). 396 397 @param name: long name of the option (or None) 398 @type name: str, None 399 @param shortname: short (single character) name of the option (or None) 400 @type shortname:str, None 401 @param help: help text 402 @type help: str 403 @param default: default value, assigned when the option is omitted. 404 If the option is specified on the command line, the 405 inverse value is assigned 406 @type default: bool 407 """ 408 if not help: 409 help = '' 410 help = '{0} (default={1})'.format(help, default) 411 412 if default: 413 action = 'store_false' 414 else: 415 action = 'store_true' 416 417 self._add(ArgHandler.Type.NAMED, name, shortname, 418 help=help, action=action, default=bool(default))
419
420 - def add_scalar_option(self, name, shortname, type, help, default=None, choices=None, required=False):
421 """ 422 Define a scalar option (a dashed argument that accepts a single value). 423 424 @param name: long name of the option (or None) 425 @type name: str, None 426 @param shortname: short (single character) name of the option (or None) 427 @type shortname: str, None 428 @param type: argument data type 429 @type type: type (type factory callable) 430 @param help: help text 431 @type help: str 432 @param default: default value, assigned when the option is omitted 433 @param choices: list of allowed argument values 434 @type choices: tuple 435 @param required: make this option a named mandatory argument 436 @type required: bool 437 """ 438 if not help: 439 help = '' 440 if default is not None: 441 help = '{0} (default={1})'.format(help, default) 442 443 self._add(ArgHandler.Type.NAMED, name, shortname, 444 type=type, help=help, default=default, choices=choices, required=required)
445
446 - def add_array_option(self, name, shortname, type, help, default=None, choices=None, required=False):
447 """ 448 Define an array option (a dashed argument that may receive one 449 or multiple values on the command line, separated with spaces). 450 451 @param name: long name of the option (or None) 452 @type name: str, None 453 @param shortname: short (single character) name of the option (or None) 454 @type shortname: str, None 455 @param type: argument data type 456 @type type: type (type factory callable) 457 @param help: help text 458 @type help: str 459 @param choices: list of allowed argument values 460 @type choices: tuple 461 @param required: make this option a named mandatory argument 462 @type required: bool 463 """ 464 if not help: 465 help = '' 466 if default is not None: 467 help = '{0} (default={1})'.format(help, default) 468 469 self._add(ArgHandler.Type.NAMED, name, shortname, 470 nargs=argparse.ZERO_OR_MORE, type=type, help=help, choices=choices, required=required)
471
472 - def parse(self, args):
473 """ 474 Parse the command line arguments. 475 476 @param args: the list of user-provided command line arguments -- 477 normally sys.argv[1:] 478 @type args: tuple of str 479 480 @return: an object initialized with the parsed arguments 481 @rtype: argparse.Namespace 482 """ 483 try: 484 return self.parser.parse_args(args) 485 except SystemExit as se: 486 if se.code > 0: 487 raise AppExit('Bad command line', ExitCodes.USAGE_ERROR) 488 else: 489 raise AppExit(code=ExitCodes.CLEAN)
490 491 @property
492 - def parser(self):
493 return self._parser
494 495 @property
496 - def usage(self):
497 return self.parser.format_usage()
498 499 @property
500 - def help(self):
501 return self.parser.format_help()
502