@@ -2548,3 +2548,164 @@ In this case, the message #5 printed to ``stdout`` doesn't appear, as expected.
25482548Of course, the approach described here can be generalised, for example to attach
25492549logging filters temporarily. Note that the above code works in Python 2 as well
25502550as Python 3.
2551+
2552+
2553+ .. _starter-template :
2554+
2555+ A CLI application starter template
2556+ ----------------------------------
2557+
2558+ Here's an example which shows how you can:
2559+
2560+ * Use a logging level based on command-line arguments
2561+ * Dispatch to multiple subcommands in separate files, all logging at the same
2562+ level in a consistent way
2563+ * Make use of simple, minimal configuration
2564+
2565+ Suppose we have a command-line application whose job is to stop, start or
2566+ restart some services. This could be organised for the purposes of illustration
2567+ as a file ``app.py `` that is the main script for the application, with individual
2568+ commands implemented in ``start.py ``, ``stop.py `` and ``restart.py ``. Suppose
2569+ further that we want to control the verbosity of the application via a
2570+ command-line argument, defaulting to ``logging.INFO ``. Here's one way that
2571+ ``app.py `` could be written::
2572+
2573+ import argparse
2574+ import importlib
2575+ import logging
2576+ import os
2577+ import sys
2578+
2579+ def main(args=None):
2580+ scriptname = os.path.basename(__file__)
2581+ parser = argparse.ArgumentParser(scriptname)
2582+ levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
2583+ parser.add_argument('--log-level', default='INFO', choices=levels)
2584+ subparsers = parser.add_subparsers(dest='command',
2585+ help='Available commands:')
2586+ start_cmd = subparsers.add_parser('start', help='Start a service')
2587+ start_cmd.add_argument('name', metavar='NAME',
2588+ help='Name of service to start')
2589+ stop_cmd = subparsers.add_parser('stop',
2590+ help='Stop one or more services')
2591+ stop_cmd.add_argument('names', metavar='NAME', nargs='+',
2592+ help='Name of service to stop')
2593+ restart_cmd = subparsers.add_parser('restart',
2594+ help='Restart one or more services')
2595+ restart_cmd.add_argument('names', metavar='NAME', nargs='+',
2596+ help='Name of service to restart')
2597+ options = parser.parse_args()
2598+ # the code to dispatch commands could all be in this file. For the purposes
2599+ # of illustration only, we implement each command in a separate module.
2600+ try:
2601+ mod = importlib.import_module(options.command)
2602+ cmd = getattr(mod, 'command')
2603+ except (ImportError, AttributeError):
2604+ print('Unable to find the code for command \'%s\'' % options.command)
2605+ return 1
2606+ # Could get fancy here and load configuration from file or dictionary
2607+ logging.basicConfig(level=options.log_level,
2608+ format='%(levelname)s %(name)s %(message)s')
2609+ cmd(options)
2610+
2611+ if __name__ == '__main__':
2612+ sys.exit(main())
2613+
2614+ And the ``start ``, ``stop `` and ``restart `` commands can be implemented in
2615+ separate modules, like so for starting::
2616+
2617+ # start.py
2618+ import logging
2619+
2620+ logger = logging.getLogger(__name__)
2621+
2622+ def command(options):
2623+ logger.debug('About to start %s', options.name)
2624+ # actually do the command processing here ...
2625+ logger.info('Started the \'%s\' service.', options.name)
2626+
2627+ and thus for stopping::
2628+
2629+ # stop.py
2630+ import logging
2631+
2632+ logger = logging.getLogger(__name__)
2633+
2634+ def command(options):
2635+ n = len(options.names)
2636+ if n == 1:
2637+ plural = ''
2638+ services = '\'%s\'' % options.names[0]
2639+ else:
2640+ plural = 's'
2641+ services = ', '.join('\'%s\'' % name for name in options.names)
2642+ i = services.rfind(', ')
2643+ services = services[:i] + ' and ' + services[i + 2:]
2644+ logger.debug('About to stop %s', services)
2645+ # actually do the command processing here ...
2646+ logger.info('Stopped the %s service%s.', services, plural)
2647+
2648+ and similarly for restarting::
2649+
2650+ # restart.py
2651+ import logging
2652+
2653+ logger = logging.getLogger(__name__)
2654+
2655+ def command(options):
2656+ n = len(options.names)
2657+ if n == 1:
2658+ plural = ''
2659+ services = '\'%s\'' % options.names[0]
2660+ else:
2661+ plural = 's'
2662+ services = ', '.join('\'%s\'' % name for name in options.names)
2663+ i = services.rfind(', ')
2664+ services = services[:i] + ' and ' + services[i + 2:]
2665+ logger.debug('About to restart %s', services)
2666+ # actually do the command processing here ...
2667+ logger.info('Restarted the %s service%s.', services, plural)
2668+
2669+ If we run this application with the default log level, we get output like this:
2670+
2671+ .. code-block :: shell-session
2672+
2673+ $ python app.py start foo
2674+ INFO start Started the 'foo' service.
2675+
2676+ $ python app.py stop foo bar
2677+ INFO stop Stopped the 'foo' and 'bar' services.
2678+
2679+ $ python app.py restart foo bar baz
2680+ INFO restart Restarted the 'foo', 'bar' and 'baz' services.
2681+
2682+ The first word is the logging level, and the second word is the module or
2683+ package name of the place where the event was logged.
2684+
2685+ If we change the logging level, then we can change the information sent to the
2686+ log. For example, if we want more information:
2687+
2688+ .. code-block :: shell-session
2689+
2690+ $ python app.py --log-level DEBUG start foo
2691+ DEBUG start About to start foo
2692+ INFO start Started the 'foo' service.
2693+
2694+ $ python app.py --log-level DEBUG stop foo bar
2695+ DEBUG stop About to stop 'foo' and 'bar'
2696+ INFO stop Stopped the 'foo' and 'bar' services.
2697+
2698+ $ python app.py --log-level DEBUG restart foo bar baz
2699+ DEBUG restart About to restart 'foo', 'bar' and 'baz'
2700+ INFO restart Restarted the 'foo', 'bar' and 'baz' services.
2701+
2702+ And if we want less:
2703+
2704+ .. code-block :: shell-session
2705+
2706+ $ python app.py --log-level WARNING start foo
2707+ $ python app.py --log-level WARNING stop foo bar
2708+ $ python app.py --log-level WARNING restart foo bar baz
2709+
2710+ In this case, the commands don't print anything to the console, since nothing
2711+ at ``WARNING `` level or above is logged by them.
0 commit comments