Reputation: 2406
When parsing the arguments for my program, I'd like to be able to also take certain arguments from the environment if not specified as arguments to the program.
I currently have:
parser = argparse.ArgumentParser(
description="Foo Bar Baz")
parser.add_argument('--metadata-api-user', type=str,
help='Username for basic authentication access to the metadata API. If not given, will be extracted from env variable {}'.format("API_USER"),
default=os.environ.get("API_USER"))
parser.add_argument('--metadata-api-password', type=str,
help='Password for basic authentication access to the metadata API. If not given, will be extracted from env variable {}'.format("API_PASS"),
default=os.environ.get("API_PASS"))
parser.add_argument('--version', type=int, help='Version number', default=1)
parser.add_argument('--location', type=str, help='Location to place thing (default: ./)',default="./")
args = parser.parse_args(args)
This provides the functionality that I want, but if the env variables are not given and they are not given in the command line, I'd like argparse to raise an ArgumentError.
environ[keyname] would raise a keyerror when creating arguments if it was only specified on the command line and not in the env variables, which isn't great.
Something like 'allow-none'=false would be great as a parameter when creating the argument, but if anyone knows another solution to this that would be awesome.
Upvotes: 5
Views: 6587
Reputation: 1251
An elegant solution is to subclass the argument parser to transparently implement the logic you described:
class SaneArgumentParser(argparse.ArgumentParser):
"""
Argument parser for which arguments are required on the CLI unless:
- required=False is provided
and/or
- a default value is provided that is not None.
"""
def add_argument(self, *args, default=None, **kwargs):
if default is None:
# Tentatively make this argument required
kwargs.setdefault("required", True)
super().add_argument(*args, **kwargs, default=default)
You'd use it as usual:
if __name__ == "__main__":
parser = SaneArgumentParser()
parser.add_argument("--foo", default=os.environ.get("FOO"))
args = parser.parse_args()
print(args)
Of course, providing an argument on the CLI works as usual:
$ ./prog.py --foo=42
Namespace(foo='42')
Providing an argument in the environment also works:
$ FOO=42 ./prog.py
Namespace(foo='42')
And providing no argument at all raises an ArgumentError
:
$ ./prog.py
usage: prog.py [-h] --foo FOO
prog.py: error: the following arguments are required: --foo
Note that catching ArgumentError
is only possible since Python 3.9 when using exit_on_error=False
: https://docs.python.org/3/library/argparse.html#exit-on-error
Upvotes: 2
Reputation: 1069
why not check for the environment variable while building the arguments?
if "API_USER" in os.environ:
parser.add_argument('--metadata-api-user', type=str, default=os.environ.get("API_USER"))
else:
parser.add_argument('--metadata-api-user', type=str)
this seems like the easiest way to get the result you need.
Upvotes: 0
Reputation: 1344
default
will simply return the given value if the user doesn't provide one, however it has a special case for None
and for strings.
Passing None
to default
will short circuit and return None
.
Passing a string to default
will return a parsed string using the type
constructor.
I implemented a custom parameterized type called not_none
, which will attempt to parse the string with a given type constructor, or fail loudly if the string is empty.
import argparse
from os import environ
from typing import Callable, TypeVar
T = TypeVar('T')
def not_none(type_constructor: Callable[[str], T] = str):
"""Convert a string to a type T."""
def parse(s: str) -> T:
"""argparse will call this function if `s` is an empty string, but not when `s` is None."""
if not s:
raise ValueError
else:
return type_constructor(s)
return parse
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--foo', help="A foo...", type=not_none(int), default='1')
parser.add_argument('--bar', help="A bar...", type=not_none(), default=environ.get('BAR', ''))
# args = parser.parse_args(['--foo', '1'])
args = parser.parse_args(['--bar', 'BAR'])
print(args.foo, args.bar)
Upvotes: 4
Reputation: 2406
Unfortunately I was unable to find a solution, so I've had to go with adding following for every argument that can be obtained from env variables:
api_user_action = parser.add_argument('--metadata-api-user', type=str,
help='Username for basic authentication access to the metadata API. If not given, will be extracted from env variable {}'.format("API_USER"),
default=os.environ.get("API_USER"))
...
if args.metadata_api_user is None:
raise argparse.ArgumentError(api_user_action,
"API Username was not supplied in either command line or environment variables. \n" + parser.format_help())
This prints the usage and an appropriate error message if they aren't supplied by either, but it needs to be repeated from every argument that this can apply to.
Upvotes: 1