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 super(AppExit, self).__init__(message, code, usage)
93
94 -class Application(object):
95 """ 96 Base CSB application class. 97 98 @param args: an object containing the application arguments 99 @type args: argparse.Namespace 100 """ 101 __metaclass__ = ABCMeta 102
103 - def __init__(self, args, log=sys.stdout):
104 105 self.__args = None 106 self._log = log 107 108 self.args = args
109 110 @property
111 - def args(self):
112 """ 113 The object containing application's arguments, as returned by the 114 command line parser. 115 """ 116 return self.__args
117 @args.setter
118 - def args(self, args):
119 self.__args = args
120 121 @abstractmethod
122 - def main(self):
123 """ 124 The main application hook. 125 """ 126 pass
127
128 - def log(self, message, ending='\n'):
129 """ 130 Write C{message} to the logging stream and flush it. 131 132 @param message: message 133 @type message: str 134 """ 135 136 self._log.write(message) 137 self._log.write(ending) 138 self._log.flush()
139 140 @staticmethod
141 - def exit(message, code=0, usage=False):
142 """ 143 Notify the app runner about an application exit. 144 145 @param message: exit message 146 @type message: str 147 @param code: exit code (see L{ExitCodes} for common constants) 148 @type code: int 149 @param usage: advise the client to show the usage line 150 @type usage: bool 151 152 @note: you re not supposed to use C{sys.exit()} for the same purpose. 153 It is L{AppRunner}'s responsibility to handle the real system 154 exit, if the application has been started as an executable. 155 Think about your app being executed by some Python client as a 156 regular Python class, imported from a module -- in that case you 157 only want to ask the client to terminate the app, not to kill 158 the whole interpreter. 159 """ 160 raise AppExit(message, code, usage)
161
162 -class AppRunner(object):
163 """ 164 A base abstract class for all application runners. Concrete sub-classes 165 must define their corresponding L{Application} using the L{self.target} 166 property and must customize the L{Application}'s command line parser using 167 L{self.command_line()}. 168 169 @param argv: the list of command line arguments passed to the program. By 170 default this is C{sys.argv}. 171 @type argv: tuple of str 172 """ 173 __metaclass__ = ABCMeta 174
175 - def __init__(self, argv=sys.argv):
176 177 self._module = argv[0] 178 self._program = os.path.basename(self.module) 179 self._args = argv[1:]
180 181 @property
182 - def module(self):
183 return self._module
184 185 @property
186 - def program(self):
187 return self._program
188 189 @property
190 - def args(self):
191 return self._args
192 193 @abstractproperty
194 - def target(self):
195 """ 196 Reference to the concrete L{Application} class to run. This is 197 an abstract property that couples the current C{AppRunner} to its 198 corresponding L{Application}. 199 200 @rtype: type (class reference) 201 """ 202 return Application
203 204 @abstractmethod
205 - def command_line(self):
206 """ 207 Command line factory: build a command line parser suitable for the 208 application. 209 This is a hook method that each concrete AppRunner must implement. 210 211 @return: a command line parser object which knows how to handle 212 C{sys.argv} in the context of the concrete application. See the 213 documentation of L{ArgHandler} for more info on how to define command 214 line arguments. 215 216 @rtype: L{ArgHandler} 217 """ 218 # null implementation (no cmd arguments): 219 return ArgHandler(self.program)
220
221 - def initapp(self, args):
222 """ 223 Hook method that controls the instantiation of the main app class. 224 If the application has a custom constructor, you can adjust the 225 app initialization by overriding this method. 226 227 @param args: an object containing the application arguments 228 @type args: argparse.Namespace 229 230 @return: the application instance 231 @rtype: L{Application} 232 """ 233 app = self.target 234 return app(args)
235
236 - def run(self):
237 """ 238 Get the L{self.command_line()} and run L{self.target}. Ensure clean 239 system exit. 240 """ 241 try: 242 app = self.target 243 cmd = self.command_line() 244 245 try: 246 assert issubclass(app, Application) 247 assert isinstance(cmd, ArgHandler) 248 249 args = cmd.parse(self.args) 250 app.USAGE = cmd.usage 251 app.HELP = cmd.help 252 253 self.initapp(args).main() 254 255 except AppExit as ae: 256 if ae.usage: 257 AppRunner.exit(ae.message, code=ae.code, usage=cmd.usage) 258 else: 259 AppRunner.exit(ae.message, code=ae.code) 260 261 except SystemExit as se: # this should never happen, but just in case 262 AppRunner.exit(se.message, code=se.code) 263 264 except Exception: 265 message = '{0} has crashed. Details: \n{1}'.format(self.program, traceback.format_exc()) 266 AppRunner.exit(message, code=ExitCodes.CRASH) 267 268 AppRunner.exit(code=ExitCodes.CLEAN)
269 270 @staticmethod
271 - def exit(message='', code=0, usage='', ending='\n'):
272 """ 273 Perform system exit. If the exit C{code} is 0, print all messages to 274 STDOUT, else write to STDERR. 275 276 @param message: message to print 277 @type message: str 278 @param code: application exit code 279 @type code: int 280 """ 281 282 ending = str(ending or '') 283 message = str(message or '') 284 stream = sys.stdout 285 286 if code > 0: 287 message = 'E#{0} {1}'.format(code, message) 288 stream = sys.stderr 289 290 if usage: 291 stream.write(usage.rstrip(ending)) 292 stream.write(ending) 293 if message: 294 stream.write(message) 295 stream.write(ending) 296 297 sys.exit(code)
298
299 -class ArgHandler(object):
300 """ 301 Command line argument handler. 302 303 @param program: (file)name of the program, usually sys.argv[0] 304 @type program: str 305 @param description: long description of the application, shown in help 306 pages. The usage line and the parameter lists are 307 generated automatically, so no need to put them here. 308 @type description: str 309 310 @note: a help argument (-h) is provided automatically. 311 """ 312 313 SHORT_PREFIX = '-' 314 LONG_PREFIX = '--' 315
316 - class Type(object):
317 318 POSITIONAL = 1 319 NAMED = 2
320
321 - def __init__(self, program, description=''):
322 323 self._argformat = re.compile('^[a-z][a-z0-9_-]*$', re.IGNORECASE) 324 self._optformat = re.compile('^[a-z0-9]$', re.IGNORECASE) 325 326 self._program = program 327 self._description = description 328 329 self._parser = argparse.ArgumentParser(prog=program, description=description)
330
331 - def _add(self, kind, name, shortname, help="", *a, **k):
332 333 args = [] 334 kargs = dict(k) 335 336 if shortname is not None: 337 if not re.match(self._optformat, shortname): 338 raise ValueError('Invalid short option name: {0}.'.format(shortname)) 339 340 if kind == ArgHandler.Type.POSITIONAL: 341 args.append(shortname) 342 else: 343 args.append(ArgHandler.SHORT_PREFIX + shortname) 344 345 if name is not None or kind == ArgHandler.Type.POSITIONAL: 346 if not re.match(self._argformat, name): 347 raise ValueError('Malformed argument name: {0}.'.format(name)) 348 349 if kind == ArgHandler.Type.POSITIONAL: 350 args.append(name) 351 else: 352 args.append(ArgHandler.LONG_PREFIX + name) 353 354 assert len(args) in (1, 2) 355 args.extend(a) 356 kargs["help"] = help.replace("%", "%%") # workaround for a bug in argparse 357 358 self.parser.add_argument(*args, **kargs)
359
360 - def _format_help(self, help, default):
361 362 if not help: 363 help = '' 364 if default is not None: 365 help = '{0} (default={1})'.format(help, default) 366 367 return help
368
369 - def add_positional_argument(self, name, type, help, choices=None):
370 """ 371 Define a mandatory positional argument (an argument without a dash). 372 373 @param name: name of the argument (used in help only) 374 @type name: str 375 @param type: argument data type 376 @type type: type (type factory callable) 377 @param help: help text 378 @type help: str 379 @param choices: list of allowed argument values 380 @type choices: tuple 381 """ 382 self._add(ArgHandler.Type.POSITIONAL, name, None, 383 type=type, help=help, choices=choices)
384
385 - def add_array_argument(self, name, type, help, choices=None):
386 """ 387 Same as L{self.add_positional_argument()}, but allow unlimited number 388 of values to be specified on the command line. 389 390 @param name: name of the argument (used in help only) 391 @type name: str 392 @param type: argument data type 393 @type type: type (type factory callable) 394 @param help: help text 395 @type help: str 396 @param choices: list of allowed argument values 397 @type choices: tuple 398 """ 399 self._add(ArgHandler.Type.POSITIONAL, name, None, 400 type=type, help=help, choices=choices, nargs=argparse.ONE_OR_MORE)
401
402 - def add_boolean_option(self, name, shortname, help, default=False):
403 """ 404 Define an optional switch (a dashed argument with no value). 405 406 @param name: long name of the option (or None) 407 @type name: str, None 408 @param shortname: short (single character) name of the option (or None) 409 @type shortname:str, None 410 @param help: help text 411 @type help: str 412 @param default: default value, assigned when the option is omitted. 413 If the option is specified on the command line, the 414 inverse value is assigned 415 @type default: bool 416 """ 417 if not default: 418 default = False 419 help = self._format_help(help, default) 420 421 if default: 422 action = 'store_false' 423 else: 424 action = 'store_true' 425 426 self._add(ArgHandler.Type.NAMED, name, shortname, 427 help=help, action=action, default=bool(default))
428
429 - def add_scalar_option(self, name, shortname, type, help, default=None, choices=None, required=False):
430 """ 431 Define a scalar option (a dashed argument that accepts a single value). 432 433 @param name: long name of the option (or None) 434 @type name: str, None 435 @param shortname: short (single character) name of the option (or None) 436 @type shortname: str, None 437 @param type: argument data type 438 @type type: type (type factory callable) 439 @param help: help text 440 @type help: str 441 @param default: default value, assigned when the option is omitted 442 @param choices: list of allowed argument values 443 @type choices: tuple 444 @param required: make this option a named mandatory argument 445 @type required: bool 446 """ 447 help = self._format_help(help, default) 448 449 self._add(ArgHandler.Type.NAMED, name, shortname, 450 type=type, help=help, default=default, choices=choices, required=required)
451
452 - def add_array_option(self, name, shortname, type, help, default=None, choices=None, required=False):
453 """ 454 Define an array option (a dashed argument that may receive one 455 or multiple values on the command line, separated with spaces). 456 457 @param name: long name of the option (or None) 458 @type name: str, None 459 @param shortname: short (single character) name of the option (or None) 460 @type shortname: str, None 461 @param type: argument data type 462 @type type: type (type factory callable) 463 @param help: help text 464 @type help: str 465 @param choices: list of allowed argument values 466 @type choices: tuple 467 @param required: make this option a named mandatory argument 468 @type required: bool 469 """ 470 help = self._format_help(help, default) 471 472 self._add(ArgHandler.Type.NAMED, name, shortname, 473 nargs=argparse.ZERO_OR_MORE, type=type, help=help, default=default, 474 choices=choices, required=required)
475
476 - def parse(self, args):
477 """ 478 Parse the command line arguments. 479 480 @param args: the list of user-provided command line arguments -- 481 normally sys.argv[1:] 482 @type args: tuple of str 483 484 @return: an object initialized with the parsed arguments 485 @rtype: argparse.Namespace 486 """ 487 try: 488 return self.parser.parse_args(args) 489 except SystemExit as se: 490 if se.code > 0: 491 raise AppExit('Bad command line', ExitCodes.USAGE_ERROR) 492 else: 493 raise AppExit(code=ExitCodes.CLEAN)
494 495 @property
496 - def parser(self):
497 return self._parser
498 499 @property
500 - def usage(self):
501 return self.parser.format_usage()
502 503 @property
504 - def help(self):
505 return self.parser.format_help()
506