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