@@ -2548,3 +2548,164 @@ In this case, the message #5 printed to ``stdout`` doesn't appear, as expected.
2548
2548
Of course, the approach described here can be generalised, for example to attach
2549
2549
logging filters temporarily. Note that the above code works in Python 2 as well
2550
2550
as 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