pythonargparse

argparse option to create a mapping


C compilers have the -D switch to define a preprocessor constant at compilation time. It is called like -D name=value, or -D name and value defaults to 1.

Can a similar thing be done with python argparse? That is, can you create an option that meets these criteria?

Desired usage:

ap = argparse.ArgumentParser()
ap.add_argument("-D",
    #### what goes here ? ####
    default=1
)
ap.parse_args(["-D", "foo=bar", "-D", "baz"])
# ==> Namespace(D={'foo': 'bar', 'baz': 1})

If I just use nargs="append", I can not specify an automatic default in the add_argument call, and I additionally have to do the string processing myself on the resultant list, which is kind of hairy, and I would like to avoid that.


Solution

  • There is nothing in argparse directly for updating mapping types, currently. I've seen people use a normal option and then put type=json.loads as a hackaround, but it's not very likable.

    I think a custom Action type meets the requirements in the question:

    import argparse
    
    class MappingAction(argparse.Action):
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self._default = self.default
            self.default = {}
    
        def __call__(self, parser, namespace, values, option_string=None):
            items = getattr(namespace, self.dest) or {}
            key, sep, val = values.partition("=")
            if not sep:
                val = self._default
            items[key] = val
            setattr(namespace, self.dest, items)
    

    Using the default=1 to indicate the default value of unspecified mapping values is a bit off, so I'd encourage you to rethink that API. In the context of an add_argument call, using default=1 would suggest you want a default value of 1 in the namespace if no -D were specified. Whereas I think you'd probably prefer an empty dict as the default value in that case.

    If the default doesn't need to be configurable, I'd probably recommend to remove the "indirection" used in this action and just hardcode the "1" instead of using self._default. This would simplify your action to this:

    class MappingAction(argparse.Action):
    
        def __call__(self, parser, namespace, values, option_string=None):
            items = getattr(namespace, self.dest) or {}
            key, sep, val = values.partition("=")
            if not sep:
                val = "1"
            items[key] = val
            setattr(namespace, self.dest, items)
    

    Usage demo:

    import argparse
    
    ap = argparse.ArgumentParser()
    ap.add_argument("-D", action=MappingAction)
    args = ap.parse_args(['-Dfoo=bar', '-Dbaz', '-D', 'asd=qwe', '-D', 'hi=1'])
    print(args)  # Namespace(D={'foo': 'bar', 'baz': '1', 'asd': 'qwe', 'hi': '1'})