4
\$\begingroup\$

I want to be able to both provide options through command line and from a configuration file. A single option could be served either by the user or by the configuration. But if is provided by both, only user input is used. Such an option could be required and an error would be output if neither user nor configuration provide it.

My solution is to create a class that wraps argparse.ArgumentParser, and provide options from the configuration file as if it was provided by the user.

For the overwriting aspect (user option > configuration option), I rely on the fact that if the same option is provided several times to argparse, only the last one is used.

--option 'value1' --option 'value2' will result in Namespace(option='value2').

Here is how I went:

import sys
from argparse import ArgumentParser


class ConfArgParser(ArgumentParser):

    def __init__(self, *args, config=None, **kwargs):
        self.__config = config if config is not None else dict()
        self.__key_opts = {}
        super(ConfArgParser, self).__init__(*args, **kwargs)

    def add_argument(self, *args, conf_key=None, **kwargs):
        if conf_key is not None:
            self.__key_opts[conf_key] = args[0]

        return super(ConfArgParser, self).add_argument(*args, **kwargs)

    def parse_args(self, **kwargs):
        config_as_opts = []
        for key, opt in self.__key_opts.items():
            conf_value = self.__resolve_conf(key.split("."), self.__config)
            if conf_value is not None:
                config_as_opts.append(opt)
                config_as_opts.append(str(conf_value))
        sys.argv = sys.argv[0:1] + config_as_opts + sys.argv[1:]

        return super(ConfArgParser, self).parse_args(**kwargs)

    def __resolve_conf(self, key_frags, node):
        if key_frags[0] not in node:
            return None
        elif len(key_frags) == 1:
            return node[key_frags[0]]
        else:
            return self.__resolve_conf(key_frags[1:], node[key_frags[0]])

I could use it that way:

#!/usr/bin/env python3
from confargparse import ConfArgParser
import yaml

if __name__ == "__main__":

    with open("application.yml", "r") as config_file:
        config = yaml.load(config_file)

    parser = ConfArgParser(description="Cli prototype", config=config)

    parser.add_argument("--host", required=True, type=str, conf_key="mysql.host")
    parser.add_argument("-u", "--user", required=True, type=str, conf_key="mysql.user")

    args = parser.parse_args()

With such a configuration file, named application.yml:

mysql:
  host: 127.0.0.1
  user: root
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Perhaps, rather than rolling your own: ConfigArgParse is a maintained package on Pypi \$\endgroup\$
    – Alex
    Feb 8, 2019 at 1:39
  • \$\begingroup\$ It seems too complex for my use case and while it is still maintained, it's still in beta and it suffers from some bugs. I'm not quite sure making my own is a better way though. At least, I like it being much simpler than ConfigArgParse. I'm thinking of not using a class at all and dynamically setting options default values and required conditions but that comes with its own problems. Mainly this last approach is problematic because I call 'add_argument' from other modules than the main. \$\endgroup\$
    – Adrien H
    Feb 8, 2019 at 8:26

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.