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
71
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
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
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):
121
122 @abstractmethod
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
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
177
178 self._module = argv[0]
179 self._program = os.path.basename(self.module)
180 self._args = argv[1:]
181
182 @property
185
186 @property
189
190 @property
193
194 @abstractproperty
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
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
220 return ArgHandler(self.program)
221
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
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
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):
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):
359
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
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
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
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
494
495 @property
497 return self.parser.format_usage()
498
499 @property
501 return self.parser.format_help()
502