python-3.xcommand-lineargparse

How to check in argparse if a command line parameter is set from default?


I'm using argparse to parse the command line in a Python 3 program. After calling parse_args() I need to figure out whether the values in the returned namespace has been set on the command line or because a default has been applied. How do I do that?

For instance if I add an argument and parse the command line like this:

p.add_argument('--loglvl', help = "Set log level", default='warning')
p.add_argument('-t', '--test', help = "Run in test mode", action = 'store_true')

vals = p.parse_args()

If vals.test == True I need to know whether it was set from the default value, because it wasn't specified on the command line or if it was set because the command line contained something like this:

-t

or

--test

Or if vals.loglvl == 'warning' I need to know if the loglvl argument was left out of the command line or if the command line contains something like this:

--loglvl=warning

How do I do that? Preferably without having to write my own command line parser. In other words: Is there something in the parser, or the returned namespace, which contain a list of the arguments supplied on the command line.


Solution

  • I'm the OP who originally posed the question above. When I asked this question it was because I was developing a Package with some general functionality to help me and my then colleagues to streamline various parts of our Python development.

    I have since then left the company, and have re-implemented the whole thing on my own time. This time around I ended up inheriting from the argparse.ArgumentParser class, and overriding the add_argument and parse_args methods, as well as implementing 2 of my own methods:

    class JBArgumentParser(ap.ArgumentParser):
        envvars = {}
    
        def add_argument(self, *args, **kwargs):
                    # I added a new argument to the add_argument method: envvar
                    # This value is a string containing the name of the
                    # environment variable to read a value from, if a value
                    # isn't passed on the command line.
            envvar = kwargs.pop('envvar', None)
    
            res = super(JBArgumentParser, self).\
                                    add_argument(*args, **kwargs)
    
            if envvar is not None:
                # I couldn't solve the problem, of distinguishing an
                            # optional positional argument that have been given
                            # the default value by argparse, from the same
                            # argument being passed a value equal to the default
                            # value on the command line. And since a mandatory
                            # positional argument can't get to the point where
                            # it needs to read from an environment variable, I
                            # decided to just not allow reading the value of any
                            # positional argument from an environment variable.
                if (len(res.option_strings) == 0):
                    raise EJbpyArgparseEnvVarError(
                                        "Can't define an environment variable " +
                                        "for a positional argument.")
                self.envvars[res.dest] = envvar
    
            return res
    
        def parse_args(self, *args, **kwargs):
            res = super(JBArgumentParser, self).\
                                    parse_args(*args, **kwargs)
    
            if len(self.envvars) > 0:
                self.GetEnvVals(res)
    
            return res
    
        def GetEnvVals(self, parsedCmdln):
            for a in self._actions:
                name = a.dest
    
                # A value in an environment variable supercedes a
                            # default value, but a value given on the command
                            # line supercedes a value from an environment
                            # variable.
                if name not in self.envvars:
                    # If the attribute isn't in envvars, then
                                    # there's no reason to continue, since then
                                    # there's no environment variable we can get
                                    # a value from.
                    continue
    
                envVal = os.getenv(self.envvars[name])
    
                if (envVal is None):
                    # There is no environment variable if envVal
                                    # is None, so in that case we have nothing
                                    # to do.
                    continue
    
                if name not in vars(parsedCmdln):
                    # The attribute hasn't been set, but has an
                                    # environment variable defined, so we should
                                    # just set the value from that environment
                                    # variable.
                    setattr(parsedCmdln, name, envVal)
                    continue
    
                # The current attribute has an instance in the
                            # parsed command line, which is either a default
                            # value or an actual value, passed on the command
                            # line.
                val = getattr(parsedCmdln, name)
    
                if val is None:
                    # AFAIK you can't pass a None value on the
                                    # command line, so this has to be a default
                                    # value.
                    setattr(parsedCmdln, name, envVal)
                    continue
    
                # We have a value for the attribute. This value can
                            # either come from a default value, or from a value
                            # passed on the command line. We need to figure out
                            # which we have, by checking if the attribute was
                            # passed on the command line.
                if val != a.default:
                    # If the value of the attribute is not equal
                                    # to the default value, then we didn't get
                                    # the value from a default value, so in that
                                    # case we don't get the value form an
                                    # environment variable.
                    continue
    
                if not self.AttrOnCmdln(a):
                                    # The argument was not found among the
                                    # passed arguments.
                    setattr(parsedCmdln, name, envVal)
    
        # Check if given attribute was passed on the command line
        def AttrOnCmdln(self, arg):
            for a in sys.argv[1:]:
                # Arguments can either be long form (preceded by 
                            # --), short form (preceded by -) or positional (no
                            # flag given, so not preceded by -).
                if a[0:2] == '--':
                    # If a longform argument takes a value, then
                                    # the option string and the value will
                                    # either be separated by a space or a =.
                    if '=' in a:
                        a = a.split("=")[0]
                    if p in arg.option_strings:
                        return True
                elif a[0] == '-':
                    # Since we have already taken care of
                                    # longform arguments, we know this is a
                                    # shortform argument.
                    for i, c in enumerate(a[1:]):
                        optionstr = f"-{c}"
                        if optionstr in arg.option_strings:
                            return True
                        elif (((i + 1) < len(a[1:])) 
                                                and (a[1:][i + 1] == '=')) or \
                            isinstance( \
                                        self._option_string_actions[optionstr],
                                                    ap._StoreAction) or \
                            isinstance( \
                                        self._option_string_actions[optionstr], 
                                                    ap._AppendAction):
                            # We may need to test for
                                                    # more classes than these
                                                    # two, but for now these
                                                    # works. Maybe
                                                    # _StoreConstAction or
                                                    # _AppendConstAction?
                            # Similar to longform
                                                    # arguments, shortform
                                                    # arguments can take values
                                                    # and in the same way they
                                                    # can be separated from
                                                    # their value by a space, or
                                                    # a =, but unlike longform
                                                    # arguments the value can
                                                    # also come immediately
                                                    # after the option string.
                                                    # So we need to check if the
                                                    # option would take a value,
                                                    # and if so ignore the rest
                                                    # of the option, by getting
                                                    # out of the loop.
                            break
                else:
                    # This is a Positional argument. In case of
                                    # a mandatory positional argument we
                                    # shouldn't get to this point if it was
                                    # missing (a mandatory argument can't have a
                                    # default value), so in that case we know
                                    # it's present. In the case of a conditional
                                    # positional argument we could get here, if
                                    # the argument has a default value. Or maybe
                                    # if one, of multiple, positional arguments
                                    # is missing?
                    if isinstance(arg.nargs, str) and \
                                                        arg.nargs == '?':
                        # Is there any way we can
                                            # distinguish between the default
                                            # value and the same value being
                                            # passed on the command line? For
                                            # the time being we are denying
                                            # defining an environment variable
                                            # for any positional argument.
                        break
    
            return False