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):
93
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
104
105 self.__args = None
106 self._log = log
107
108 self.args = args
109
110 @property
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):
120
121 @abstractmethod
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
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
176
177 self._module = argv[0]
178 self._program = os.path.basename(self.module)
179 self._args = argv[1:]
180
181 @property
184
185 @property
188
189 @property
192
193 @abstractproperty
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
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
219 return ArgHandler(self.program)
220
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
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
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):
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):
359
368
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
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
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
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
498
499 @property
501 return self.parser.format_usage()
502
503 @property
505 return self.parser.format_help()
506