Using LayeredConfig with argparse

The standard module for handling command line arguments in python is argparse. This module handles much of the same things as LayeredConfig does (eg. defining the default values and types of arguments and making them easily accessed), but it isn’t able to read parameter values from other sources such as INI files or environment variables.

LayeredConfig integrates with argparse through the Commandline config source. If you have existing code to set up an argparse.ArgumentParser object, you can re-use that with LayeredConfig.

import sys
import argparse
from datetime import date, datetime
from layeredconfig import LayeredConfig, Defaults, INIFile, Commandline, UNIT_SEP

After this setup, you might want to create any number of config sources. In this example we use a Defaults object, mostly used for specifying the type of different arguments.

defaults = Defaults({'home': str,
                     'name': 'MyApp',
                     'dostuff': bool,
                     'times': int,
                     'duedate': date,
                     'things': list,
                     'submodule': {'retry': bool,
                                   'lastrun': datetime
                               }
                     })

And also an INIFile that is used to store actual values for most parameters.

with open("myapp.ini", "w") as fp:
    fp.write("""[__root__]
home = /tmp/myapp
dostuff = False
times = 4
duedate = 2014-10-30
things = Huey, Dewey, Louie

[submodule]
retry = False
lastrun = 2014-10-30 16:40:22
""")
inifile = INIFile("myapp.ini")

Next up, we create an instance of argparse.ArgumentParser in the normal way. Note that in this example, we specify the types of some of the parameters, since this is representative of how ArgumentParser normally is used. But you can also omit this information (the action and type parameters to add_argument()) as long as you provide information through a Defaults config source object.

Note: we don’t add arguments for --duedate or --submodule-lastrun to show that LayeredConfig can define these arguments based on other sources. Also note that defaults values are automatically fetched from either defaults or inifile.

parser = argparse.ArgumentParser("This is a simple program")
parser.add_argument("--home", help="The home directory of the app")
parser.add_argument('--dostuff', action="store_true", help="Do some work")
parser.add_argument("-t", "--times", type=int, help="Number of times to do it")
parser.add_argument('--things', action="append", help="Extra things to crunch")
parser.add_argument('--retry', action="store_true", help="Try again")
parser.add_argument("file", metavar="FILE", help="The filename to process")

Now, instead of calling parse_args(), you can pass this initialized parser object as a named parameter when creating a Commandline source, and use this to create a LayeredConfig object.

Note that you can use short parameters if you want, as long as you define long parameters (that map to your other parameter names) as well

sys.argv = ['./myapp.py', '--home=/opt/myapp', '-t=2', '--dostuff', 'file.txt']
cfg = LayeredConfig(defaults,
                    inifile,
                    Commandline(parser=parser))
print("Starting %s in %s for %r times (doing work: %s)" % (cfg.name,
                                                           cfg.home,
                                                           cfg.times,
                                                           cfg.dostuff))
# should print "Starting MyApp in /opt/myapp for 2 times (doing work: True)"

The standard feature of argparse to create a help text if the -h parameter is given still exists. Note that it will also display parameters such as –name`, which was defined in the Defaults object, not in the parser object.

sys.argv = ['./myapp.py', '-h']
cfg = LayeredConfig(defaults,
                    inifile,
                    Commandline(parser=parser))

Warning

Using a bespoke argparse.ArgumentParser together with subsections is a bit more complicated. If you want to do that, you will need to setup each argument to the ArgumenParser object by explicitly naming the internal name for the attribute as specifid by the dest parameter, and separating the subsections with the special layeredconfig.UNIT_SEP delimiter, eg:

parser.add_argument("--submodule-retry", help="Whether to retry the submodule",
                    dest="submodule"+UNIT_SEP+"retry")