1

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.

Gabe
  • 131
  • 1
  • 13

2 Answers2

2

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
jisrael18
  • 719
  • 3
  • 10
  • Thanks jisrael. I think your answer is best for most use cases; however, I was writing a module that saves the command-line args and the argparse state (default vs. actuals, etc) in a way that required only importing it and it "did all the magic". @hpaulj 's answer might be a bit more "risky" (I've already run into some edge cases, but it's still good enough for me), but it works really well for that purpose. – Gabe Mar 25 '22 at 03:13
1

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.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • Thanks hpaulj. This worked well for what I needed (inferring argparse information from a module imported and which automagically logs the state of the running process). – Gabe Mar 25 '22 at 03:15