79717350

Date: 2025-07-28 12:52:47
Score: 2
Natty:
Report link

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
Reasons:
  • Blacklisted phrase (1): help me
  • Blacklisted phrase (1): Is there any
  • Long answer (-1):
  • Has code block (-0.5):
  • Self-answer (0.5):
  • Starts with a question (0.5): When I as
  • Low reputation (0.5):
Posted by: Jens Bang