Gabe
Gabe

Reputation: 169

Python Argparse: Get the command-line argument used for a Namespace variable

Is there a proper, or at least better, way to get which command-line argument was used to set an Namespace argument (attribute) value?

I am currently using something like this:

>>> import argparse
>>>
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--do-a', '-a',
...     default=False, action='store_true',
...     dest='process_foo',
...     help="Do some awesome a to the thing.")
>>> args = parser.parse_args()
>>>
>>> def get_argument(parser, dest):
...     for action in parser._actions:
...         if action.dest == dest:
...             return action.option_strings[0], action.help
...     return dest, ''
...
>>> get_argument(parser, 'process_foo')
('--do-a', 'Do some awesome a to the thing.')

This will probably work in 99% of cases; however, if more than one command-line argument can set process_foo, this wont work, and accessing a 'hidden' instance attribute (parser._actions) is kludgy at best. Is there a better way to do this?

I'm adding this to a module that all data science processes inherit which logs environment and other things so that we have better reproducibility. The module in question already auto-logs settings, parameters, command-line arguments, etc. but is not very user friendly in some aspects.

Upvotes: 2

Views: 2158

Answers (2)

hpaulj
hpaulj

Reputation: 231395

Don't worry about the "hiddenness" of _actions. That is the primary list where references to all Actions created by add_argument are stored. You shouldn't fiddle with the list, but you certainly can use it to collect information.

add_argument creates an Action object, puts it in _actions (via the _add_action method), and also returns it. If you don't like using _actions you can collect your own list of references, using the object returned by add_argument.

I see from _add_action that it also puts flagged actions in a self._option_string_actions dict, making it easier to pair an option string with its action.

Parsing does not make any changes to the parser, its attributes or the actions. While it has various local variables (in the _parse_known_args method), the only thing that is changed is the args Namespace.

It keeps the access to args as generic as possible, with getattr, setattr and hasattr. This includes setting the defaults at the start of parsing. The parser does not maintain a record of which option-string triggered a particular take_action and subsequent setattr. However the __call__ of an Action does get the string. For the most common 'store_action' the call is

def __call__(self, parser, namespace, values, option_string=None):
    setattr(namespace, self.dest, values)

I think all defined Action subclasses use the self.dest, but user defined ones don't have to. They can even set other namespace attributes, or none (e.g. help doesn't set anything). They could also record the option_string.

It is also possible to set a namespace attribute without going through the defined Actions. Your dest test won't help with these.

https://docs.python.org/3/library/argparse.html#parser-defaults

shows how attributes can be set without defining them in an argument. Subcommands shows how this can be used to define a function that will be used with a particular parser.

https://docs.python.org/3/library/argparse.html#the-namespace-object also shows that it's possible to supply a partially initialize namespace.

Upvotes: 1

izzy18
izzy18

Reputation: 804

I would suggest creating your own action class derived from argarse.Action that will not only store the parsed value in the namespace, but also store the parsed value's option string in the namespace.

Full working example:

import argparse

class StoreTrueWithOptionStringAction(argparse.Action):
    def __init__(self,
                 option_strings,
                 dest,
                 default=None,
                 required=False,
                 help=None,
                 metavar=None):
        super().__init__(option_strings=option_strings,
                         dest=dest,
                         nargs=0,
                         const=True,
                         default=default,
                         required=required,
                         help=help)
    
    def __call__(self, parser, namespace, values, option_string=None):
        setattr(namespace, self.dest, self.const)
        if option_string is not None:
            setattr(namespace, f'{self.dest}_option_string', option_string)

def get_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument('--bar', action=StoreTrueWithOptionStringAction, dest='foo', default=False)
    parser.add_argument('--baz', action=StoreTrueWithOptionStringAction, dest='foo', default=False)
    return parser

def main():
    parser = get_parser()
    args = parser.parse_args()
    print(args.foo)
    print(args.foo_option_string)

if __name__ == '__main__':
    main()

Output:

$ python3 main.py --bar
True
--bar
$ python3 main.py --baz
True
--baz

Upvotes: 2

Related Questions