Tuesday, October 15, 2013

Python, argparse and Environment Variables

argparse more likely is the one of frequently used Python's libraries. It covers all standard cases out of the box. When a case goes beyond the box a developer is encouraged to extend the library with the specially provided API.

What to do when the argument is marked as required and its value is not changed during some quite long time? Make the argument's value persistent; store somewhere. Otherwise your application more likely has all chances to be recognized as unfriendly by an end-user. You may want to store a value of the argument somewhere in a configuration file. But what the file format should be? How to organize the file? Where should it be located? The are more questions than answers.

Why not to be able to pass the arguments to argparse's parser through the system environment? Such approach is widely recognized and makes your application easily scriptable.

Optional Arguments


When an argument is marked as optional a value from the system environment could be accessed by evaluating a default one:

parser.add_argument("-c", "--crt", type = str, default = os.environ.get("X509_CRT"), required = False, help = "Path to X509 Certificate")

Or in a bit complicated way when the value is not allowed to be empty:

parser.add_argument("-c", "--crt", type = str, default = os.environ.get("X509_CRT") or "~/.work/vpn.crt", required = False, help = "Path to X509 Certificate")

Required Arguments


Due to the design mandatory arguments in argparse library are not allowed to have default values.
There is an interface in argparse called Action which is associated with the argument being processed. The interface is intended to customize the way how an argument is processed/stored. Providing own implementation of the interface will allow you to look the desired value of the argument in system environment:

class FindValueInEnvironmentAction(argparse.Action):
    def __init__(self, varName, **kwargs):
        assert kwargs.get("required")
         
        valueFromEnv = os.environ.get(varName)
        requiredValue = True
        
        if valueFromEnv:
            kwargs["required"] = False
            kwargs["default"] = valueFromEnv
            
        argparse.Action.__init__(self, **kwargs)

    def __call__(self, parser, namespace, values, option_string):
        setattr(namespace, self.dest, values)
...
parser.add_argument("-c", "--crt", type = str, action = FindValueInEnvironmentAction, varName = "X509_CRT", required = True, help = "Path to X509 Certificate")

When the argument's value is found in the system environment variable scope, the built-in options in **kwargs are patched:
  • required attribute is removed;
  • default value is set to the read one.
These steps allow to pretend that an optional argument with a predefined default value is being processed. Here a value of the argument passed through the command line has a priority over a value set through "X509_CRT" environment variable.

I would also inject to our implementation a dictionary where to look up; it will allow us to cover the class with unit tests. And if you a user of Python3 feel free to try an alternative way.

2 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. This library also does what you want:
    https://pypi.python.org/pypi/ConfigArgParse/

    ReplyDelete