Python argparse multiple metavar names - python

I am working with the argparse library in python. At some time, I use an argument called param that takes 2 args: a key and a value. The line of code I use is the following:
parser.add_argument("-p", "--param", nargs=2, action="append",
help="key and value for query",
type=str, metavar="key value"
)
What's wrong here is when I call the help, it displays like this:
optional arguments:
-h, --help show this help message and exit
-p key value key value, --param key value key value
key and value for query parameters
The name 'key value' is repeated twice. I tried with lists, and generators but the only way i found was creating a little class containing the different values and yielding them when ask to __str__ like this:
class Meta:
def __init__(self, iterable):
self.gene = itertools.cycle(iterable)
def __str__(self):
return self.gene.__next__()
and I call add_argument like this:
parser.add_argument("-p", "--param", nargs=2, action="append",
help="key and value for query parameters",
type=str, metavar=Meta(["key", "value"])
)
And it displays correctly:
-p key value, --param key value
key and value for query parameters
But I find pretty ugly using a temporary class like Meta, and I feel like there must be another (better) way of doing this. Am I doing it right ?

From scrolling the doc deeply, I've found my answer
Different values of nargs may cause the metavar to be used multiple
times. Providing a tuple to metavar specifies a different display for
each of the arguments:
indeed, this works perfectly fine:
parser.add_argument("-p", "--param", nargs=2, action="append",
help="key and value for query parameters",
type=str, metavar=("key", "value")
)

The metavar handler isn't particularly sophisticated, but does take advantage of the information that tuple class provides.
It wasn't obvious as to why your class worked, so I dug into the code.
Metavar is handled in the Formatter class with:
def _metavar_formatter(self, action, default_metavar):
if action.metavar is not None:
result = action.metavar
elif action.choices is not None:
choice_strs = [str(choice) for choice in action.choices]
result = '{%s}' % ','.join(choice_strs)
else:
result = default_metavar
def format(tuple_size):
if isinstance(result, tuple):
return result
else:
return (result, ) * tuple_size
return format
and
def _format_args(self, action, default_metavar):
get_metavar = self._metavar_formatter(action, default_metavar)
....
else:
# for numeric nargs
formats = ['%s' for _ in range(action.nargs)]
result = ' '.join(formats) % get_metavar(action.nargs)
return result
So with your Meta:
In [261]: x = Meta(['one', 'two'])
In [262]: x
Out[262]: <__main__.Meta at 0x7f36980f65c0>
In [263]: x = (x,)*2
In [264]: x
Out[264]: (<__main__.Meta at 0x7f36980f65c0>, <__main__.Meta at 0x7f36980f65c0>)
In [265]: '%s %s'%x
Out[265]: 'one two'
With the tuple metavar:
In [266]: '%s %s'%('one','two')
Out[266]: 'one two'
and with a single string
In [267]: '%s %s'%(('one two',)*2)
Out[267]: 'one two one two'

Related

bool and not bool returning false [duplicate]

I would like to use argparse to parse boolean command-line arguments written as "--foo True" or "--foo False". For example:
my_program --my_boolean_flag False
However, the following test code does not do what I would like:
import argparse
parser = argparse.ArgumentParser(description="My parser")
parser.add_argument("--my_bool", type=bool)
cmd_line = ["--my_bool", "False"]
parsed_args = parser.parse(cmd_line)
Sadly, parsed_args.my_bool evaluates to True. This is the case even when I change cmd_line to be ["--my_bool", ""], which is surprising, since bool("") evalutates to False.
How can I get argparse to parse "False", "F", and their lower-case variants to be False?
I think a more canonical way to do this is via:
command --feature
and
command --no-feature
argparse supports this version nicely:
Python 3.9+:
parser.add_argument('--feature', action=argparse.BooleanOptionalAction)
Python < 3.9:
parser.add_argument('--feature', action='store_true')
parser.add_argument('--no-feature', dest='feature', action='store_false')
parser.set_defaults(feature=True)
Of course, if you really want the --arg <True|False> version, you could pass ast.literal_eval as the "type", or a user defined function ...
def t_or_f(arg):
ua = str(arg).upper()
if 'TRUE'.startswith(ua):
return True
elif 'FALSE'.startswith(ua):
return False
else:
pass #error condition maybe?
Yet another solution using the previous suggestions, but with the "correct" parse error from argparse:
def str2bool(v):
if isinstance(v, bool):
return v
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected.')
This is very useful to make switches with default values; for instance
parser.add_argument("--nice", type=str2bool, nargs='?',
const=True, default=False,
help="Activate nice mode.")
allows me to use:
script --nice
script --nice <bool>
and still use a default value (specific to the user settings). One (indirectly related) downside with that approach is that the 'nargs' might catch a positional argument -- see this related question and this argparse bug report.
If you want to allow --feature and --no-feature at the same time (last one wins)
This allows users to make a shell alias with --feature, and overriding it with --no-feature.
Python 3.9 and above
parser.add_argument('--feature', default=True, action=argparse.BooleanOptionalAction)
Python 3.8 and below
I recommend mgilson's answer:
parser.add_argument('--feature', dest='feature', action='store_true')
parser.add_argument('--no-feature', dest='feature', action='store_false')
parser.set_defaults(feature=True)
If you DON'T want to allow --feature and --no-feature at the same time
You can use a mutually exclusive group:
feature_parser = parser.add_mutually_exclusive_group(required=False)
feature_parser.add_argument('--feature', dest='feature', action='store_true')
feature_parser.add_argument('--no-feature', dest='feature', action='store_false')
parser.set_defaults(feature=True)
You can use this helper if you are going to set many of them:
def add_bool_arg(parser, name, default=False):
group = parser.add_mutually_exclusive_group(required=False)
group.add_argument('--' + name, dest=name, action='store_true')
group.add_argument('--no-' + name, dest=name, action='store_false')
parser.set_defaults(**{name:default})
add_bool_arg(parser, 'useful-feature')
add_bool_arg(parser, 'even-more-useful-feature')
Here is another variation without extra row/s to set default values. The boolean value is always assigned, so that it can be used in logical statements without checking beforehand:
import argparse
parser = argparse.ArgumentParser(description="Parse bool")
parser.add_argument("--do-something", default=False, action="store_true",
help="Flag to do something")
args = parser.parse_args()
if args.do_something:
print("Do something")
else:
print("Don't do something")
print(f"Check that args.do_something={args.do_something} is always a bool.")
oneliner:
parser.add_argument('--is_debug', default=False, type=lambda x: (str(x).lower() == 'true'))
There seems to be some confusion as to what type=bool and type='bool' might mean. Should one (or both) mean 'run the function bool(), or 'return a boolean'? As it stands type='bool' means nothing. add_argument gives a 'bool' is not callable error, same as if you used type='foobar', or type='int'.
But argparse does have registry that lets you define keywords like this. It is mostly used for action, e.g. `action='store_true'. You can see the registered keywords with:
parser._registries
which displays a dictionary
{'action': {None: argparse._StoreAction,
'append': argparse._AppendAction,
'append_const': argparse._AppendConstAction,
...
'type': {None: <function argparse.identity>}}
There are lots of actions defined, but only one type, the default one, argparse.identity.
This code defines a 'bool' keyword:
def str2bool(v):
#susendberg's function
return v.lower() in ("yes", "true", "t", "1")
p = argparse.ArgumentParser()
p.register('type','bool',str2bool) # add type keyword to registries
p.add_argument('-b',type='bool') # do not use 'type=bool'
# p.add_argument('-b',type=str2bool) # works just as well
p.parse_args('-b false'.split())
Namespace(b=False)
parser.register() is not documented, but also not hidden. For the most part the programmer does not need to know about it because type and action take function and class values. There are lots of stackoverflow examples of defining custom values for both.
In case it isn't obvious from the previous discussion, bool() does not mean 'parse a string'. From the Python documentation:
bool(x): Convert a value to a Boolean, using the standard truth testing procedure.
Contrast this with
int(x): Convert a number or string x to an integer.
Simplest & most correct way is:
from distutils.util import strtobool
parser.add_argument('--feature', dest='feature',
type=lambda x: bool(strtobool(x)))
Do note that True values are y, yes, t, true, on and 1;
false values are n, no, f, false, off and 0. Raises ValueError if val is anything else.
A quite similar way is to use:
feature.add_argument('--feature',action='store_true')
and if you set the argument --feature in your command
command --feature
the argument will be True, if you do not set type --feature the arguments default is always False!
I was looking for the same issue, and imho the pretty solution is :
def str2bool(v):
return v.lower() in ("yes", "true", "t", "1")
and using that to parse the string to boolean as suggested above.
This works for everything I expect it to:
add_boolean_argument(parser, 'foo', default=True)
parser.parse_args([]) # Whatever the default was
parser.parse_args(['--foo']) # True
parser.parse_args(['--nofoo']) # False
parser.parse_args(['--foo=true']) # True
parser.parse_args(['--foo=false']) # False
parser.parse_args(['--foo', '--nofoo']) # Error
The code:
def _str_to_bool(s):
"""Convert string to bool (in argparse context)."""
if s.lower() not in ['true', 'false']:
raise ValueError('Need bool; got %r' % s)
return {'true': True, 'false': False}[s.lower()]
def add_boolean_argument(parser, name, default=False):
"""Add a boolean argument to an ArgumentParser instance."""
group = parser.add_mutually_exclusive_group()
group.add_argument(
'--' + name, nargs='?', default=default, const=True, type=_str_to_bool)
group.add_argument('--no' + name, dest=name, action='store_false')
Simplest. It's not flexible, but I prefer simplicity.
parser.add_argument('--boolean_flag',
help='This is a boolean flag.',
type=eval,
choices=[True, False],
default='True')
EDIT: If you don't trust the input, don't use eval.
In addition to what #mgilson said, it should be noted that there's also a ArgumentParser.add_mutually_exclusive_group(required=False) method that would make it trivial to enforce that --flag and --no-flag aren't used at the same time.
This is actually outdated. For Python 3.7+, Argparse now supports boolean args (search BooleanOptionalAction).
The implementation looks like this:
import argparse
ap = argparse.ArgumentParser()
# List of args
ap.add_argument('--foo', default=True, type=bool, help='Some helpful text that is not bar. Default = True')
# Importable object
args = ap.parse_args()
One other thing to mention: this will block all entries other than True and False for the argument via argparse.ArgumentTypeError. You can create a custom error class for this if you want to try to change this for any reason.
A simpler way would be to use as below.
parser.add_argument('--feature', type=lambda s: s.lower() in ['true', 't', 'yes', '1'])
After previously following #akash-desarda 's excellence answer https://stackoverflow.com/a/59579733/315112 , to use strtobool via lambda, later, I decide to use strtobool directly instead.
import argparse
from distutils import util
parser.add_argument('--feature', type=util.strtobool)
Yes you're right, strtobool is returning an int, not a bool. But strtobool will not returning any other value except 0 and 1, and python will get them converted to a bool value seamlessy and consistently.
>>> 0 == False
True
>>> 0 == True
False
>>> 1 == False
False
>>> 1 == True
True
While on receiving a wrong input value like
python yours.py --feature wrong_value
An argparse.Action with strtobool compared to lambda will produce a slightly clearer/comprehensible error message:
yours.py: error: argument --feature: invalid strtobool value: 'wrong_value'
Compared to this code,
parser.add_argument('--feature', type=lambda x: bool(util.strtobool(x))
Which will produce a less clear error message:
yours.py: error: argument --feature: invalid <lambda> value: 'wrong_value'
Simplest way would be to use choices:
parser = argparse.ArgumentParser()
parser.add_argument('--my-flag',choices=('True','False'))
args = parser.parse_args()
flag = args.my_flag == 'True'
print(flag)
Not passing --my-flag evaluates to False. The required=True option could be added if you always want the user to explicitly specify a choice.
As an improvement to #Akash Desarda 's answer, you could do
import argparse
from distutils.util import strtobool
parser = argparse.ArgumentParser()
parser.add_argument("--foo",
type=lambda x:bool(strtobool(x)),
nargs='?', const=True, default=False)
args = parser.parse_args()
print(args.foo)
And it supports python test.py --foo
(base) [costa#costa-pc code]$ python test.py
False
(base) [costa#costa-pc code]$ python test.py --foo
True
(base) [costa#costa-pc code]$ python test.py --foo True
True
(base) [costa#costa-pc code]$ python test.py --foo False
False
I think the most canonical way will be:
parser.add_argument('--ensure', nargs='*', default=None)
ENSURE = config.ensure is None
Expanding on gerardw's answer
The reason parser.add_argument("--my_bool", type=bool) doesn't work is that bool("mystring") is True for any non-empty string so bool("False") is actually True.
What you want is
my_program.py
import argparse
parser = argparse.ArgumentParser(description="My parser")
parser.add_argument(
"--my_bool",
choices=["False", "True"],
)
parsed_args = parser.parse_args()
my_bool = parsed_args.my_bool == "True"
print(my_bool)
$ python my_program.py --my_bool False
False
$ python my_program.py --my_bool True
True
$ python my_program.py --my_bool true
usage: my_program.py [-h] [--my_bool {False,True}]
my_program.py: error: argument --my_bool: invalid choice: 'true' (choose from 'False', 'True')
Quick and easy, but only for arguments 0 or 1:
parser.add_argument("mybool", default=True,type=lambda x: bool(int(x)))
myargs=parser.parse_args()
print(myargs.mybool)
The output will be "False" after calling from terminal:
python myscript.py 0
I found good way to store default value of parameter as False and when it is present in commandline argument then its value should be true.
cmd command
when you want argument to be true:
python main.py --csv
when you want your argument should be false:
python main.py
import argparse
from ast import parse
import sys
parser = argparse.ArgumentParser(description='')
parser.add_argument('--csv', action='store_true', default = False
,help='read from csv')
args = parser.parse_args()
if args.csv:
print('reading from csv')
class FlagAction(argparse.Action):
# From http://bugs.python.org/issue8538
def __init__(self, option_strings, dest, default=None,
required=False, help=None, metavar=None,
positive_prefixes=['--'], negative_prefixes=['--no-']):
self.positive_strings = set()
self.negative_strings = set()
for string in option_strings:
assert re.match(r'--[A-z]+', string)
suffix = string[2:]
for positive_prefix in positive_prefixes:
self.positive_strings.add(positive_prefix + suffix)
for negative_prefix in negative_prefixes:
self.negative_strings.add(negative_prefix + suffix)
strings = list(self.positive_strings | self.negative_strings)
super(FlagAction, self).__init__(option_strings=strings, dest=dest,
nargs=0, const=None, default=default, type=bool, choices=None,
required=required, help=help, metavar=metavar)
def __call__(self, parser, namespace, values, option_string=None):
if option_string in self.positive_strings:
setattr(namespace, self.dest, True)
else:
setattr(namespace, self.dest, False)
Similar to #Akash but here is another approach that I've used. It uses str than lambda because python lambda always gives me an alien-feelings.
import argparse
from distutils.util import strtobool
parser = argparse.ArgumentParser()
parser.add_argument("--my_bool", type=str, default="False")
args = parser.parse_args()
if bool(strtobool(args.my_bool)) is True:
print("OK")
just do the following , you can make --test = True by using
python filename --test
parser.add_argument("--test" , default=False ,help="test ?", dest='test', action='store_true')
Convert the value:
def __arg_to_bool__(arg):
"""__arg_to_bool__
Convert string / int arg to bool
:param arg: argument to be converted
:type arg: str or int
:return: converted arg
:rtype: bool
"""
str_true_values = (
'1',
'ENABLED',
'ON',
'TRUE',
'YES',
)
str_false_values = (
'0',
'DISABLED',
'OFF',
'FALSE',
'NO',
)
if isinstance(arg, str):
arg = arg.upper()
if arg in str_true_values:
return True
elif arg in str_false_values:
return False
if isinstance(arg, int):
if arg == 1:
return True
elif arg == 0:
return False
if isinstance(arg, bool):
return arg
# if any other value not covered above, consider argument as False
# or you could just raise and error
return False
[...]
args = ap.parse_args()
my_arg = options.my_arg
my_arg = __arg_to_bool__(my_arg)
You can create a BoolAction and then use it
class BoolAction(Action):
def __init__(
self,
option_strings,
dest,
nargs=None,
default: bool = False,
**kwargs,
):
if nargs is not None:
raise ValueError('nargs not allowed')
super().__init__(option_strings, dest, default=default, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
input_value = values.lower()
b = input_value in ['true', 'yes', '1']
if not b and input_value not in ['false', 'no', '0']:
raise ValueError('Invalid boolean value "%s".)
setattr(namespace, self.dest, b)
and then set action=BoolAction in parser.add_argument()

Conditional passing of arguments to methods in python

I have many possible arguments from argparse that I want to pass to a function. If the variable hasn't been set, I want the method to use its default variable. However, handling which arguments have been set and which haven't is tedious:
import argparse
def my_func(a = 1, b = 2):
return a+b
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Get the numeric values.')
parser.add_argument('-a', type=int)
parser.add_argument('-b', type=int)
args = parser.parse_args()
if not args.a is None and not args.b is None:
result = my_func(a = args.a, b = args.b)
elif not args.a is None and args.b is None:
result = my_func(a = args.a)
elif not args.b is None and args.a is None:
result = my_func(b = args.b)
else:
result = my_func()
It seems like I should be able to do something like this:
result = my_func(a = args.a if not args.a is None, b = args.b if not args.b is None)
But this gives a syntax error on the comma.
I could set default values in the argparser, but I want to use the defaults set in the method definition.
Use a dictionary with the kwargs unpacking syntax.
args = parser.parse_args()
result = my_func(**vars(args))
Edit
Use the SUPPRESS argument to ArgumentParser to remove empty values:
parser = argparse.ArgumentParser(description='Get the numeric values.',
argument_default=argparse.SUPPRESS)
The first solution that comes to me seems kind of hacky...but here it is.
Use inspect to write a function that looks at the arguments of a function and only passes it those arguments from args which it accepts and are not None. My guess is that this would be widely considered bad practice...
import inspect
def call_function(fn, args):
argspec = inspect.getargspec(fn)
arglist = {}
for arg in argspec.args:
if arg in args.__dict__.keys() and args.__dict__[arg] is not None:
arglist[arg] = args.__dict__[arg]
return fn(**arglist)
Here it is in action:
import argparse
def my_func(a=1, c=2):
return a,c
a=None
b=2
c=3
args=argparse.Namespace(a=a,b=b,c=c)
call_function(my_func, args)
>> (1, 3)
This solution is quite under-tested and might need work to make it more robust, but the idea is there and should work in simple cases.

How to have sub-parser arguments in separate namespace with argparse?

I have the following test-code
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", default = 0, type=int)
subparsers = parser.add_subparsers(dest = "parser_name")
parser_lan = subparsers.add_parser('car')
parser_lan.add_argument("--boo")
parser_lan.add_argument("--foo")
parser_serial = subparsers.add_parser('bus')
parser_serial.add_argument("--fun")
print parser.parse_args()
which defines two sub-parsers, having a different set of arguments. When I call the testcode as
tester.py --verbose 3 car --boo 1 --foo 2
I get the expected result
Namespace(boo='1', foo='2', parser_name='car', verbose=3)
What I want to have instead is the values from each subparser in a separate namespace or dict, something like
Namespace(subparseargs={boo:'1', foo:'2'}, parser_name='car', verbose=3)
so that the arguments from each subparser are logical separated from the arguments from the main parser (as verbose in this example).
How can I achieve this, with the arguments for each subparser in the same namespace (subparseargs in the example).
You need to go into the bowels of argparse a bit but changing your script to the following should do the trick:
import argparse
from argparse import _HelpAction, _SubParsersAction
class MyArgumentParser(argparse.ArgumentParser):
def parse_args(self, *args, **kw):
res = argparse.ArgumentParser.parse_args(self, *args, **kw)
from argparse import _HelpAction, _SubParsersAction
for x in parser._subparsers._actions:
if not isinstance(x, _SubParsersAction):
continue
v = x.choices[res.parser_name] # select the subparser name
subparseargs = {}
for x1 in v._optionals._actions: # loop over the actions
if isinstance(x1, _HelpAction): # skip help
continue
n = x1.dest
if hasattr(res, n): # pop the argument
subparseargs[n] = getattr(res, n)
delattr(res, n)
res.subparseargs = subparseargs
return res
parser = MyArgumentParser()
parser.add_argument("--verbose", default = 0, type=int)
subparsers = parser.add_subparsers(dest = "parser_name")
parser_lan = subparsers.add_parser('car')
parser_lan.add_argument("--boo")
parser_lan.add_argument("--foo")
parser_serial = subparsers.add_parser('bus')
parser_serial.add_argument("--fun")
print parser.parse_args()
I have started to develop a different approach (but similar to the suggestion by Anthon) and come up with a much shorter code. However, I am not sure my approach is a general solution for the problem.
To similar what Anthon is proposing, I define a new method which creates a list of 'top-level' arguments which are kept in args, while all the other arguments are returned as an additional dictionary:
class MyArgumentParser(argparse.ArgumentParser):
def parse_subargs(self, *args, **kw):
# parse as usual
args = argparse.ArgumentParser.parse_args(self, *args, **kw)
# extract the destination names for top-level arguments
topdest = [action.dest for action in parser._actions]
# loop over all arguments given in args
subargs = {}
for key, value in args.__dict__.items():
# if sub-parser argument found ...
if key not in topdest:
# ... remove from args and add to dictionary
delattr(args,key)
subargs[key] = value
return args, subargs
Comments on this approach welcome, especially any loopholes I overlooked.
Or manually, you could parse the args and create a dict with details:
# parse args
args = parser.parse_args()
args_dict = {}
for group in parser._action_groups:
# split into groups based on title
args_dict[group.title] = {}
for arg in group._group_actions:
if hasattr(args, arg.dest):
args_dict[group.title][arg.dest] = getattr(args, arg.dest)
# or args_dict[arg.dest] = getattr(args, arg.dest)
delattr(args, arg.dest)
# add remaining items into subparser options
args_dict["subparser"] |= vars(args)
return args_dict

argparse argument order

I have a little problem.
I use argparse to parse my arguments, and it's working very well.
To have the args, I do :
p_args = parser.parse_args(argv)
args = dict(p_args._get_kwargs())
But the problem with p_args is that I don't know how to get these arguments ordered by their position in the command line, because it's a dict.
So is there any possibility to have the arguments in a tuple/list/ordered dict by their order in the command line?
To keep arguments ordered, I use a custom action like this:
import argparse
class CustomAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
if not 'ordered_args' in namespace:
setattr(namespace, 'ordered_args', [])
previous = namespace.ordered_args
previous.append((self.dest, values))
setattr(namespace, 'ordered_args', previous)
parser = argparse.ArgumentParser()
parser.add_argument('--test1', action=CustomAction)
parser.add_argument('--test2', action=CustomAction)
To use it, for example:
>>> parser.parse_args(['--test2', '2', '--test1', '1'])
Namespace(ordered_args=[('test2', '2'), ('test1', '1')], test1=None, test2=None)
If you need to know the order in which the arguments appear in your parser, you can set up the parser like this:
import argparse
parser = argparse.ArgumentParser(description = "A cool application.")
parser.add_argument('--optional1')
parser.add_argument('positionals', nargs='+')
parser.add_argument('--optional2')
args = parser.parse_args()
print args.positionals
Here's a quick example of running this code:
$ python s.py --optional1 X --optional2 Y 1 2 3 4 5
['1', '2', '3', '4', '5']
Note that args.positionals is a list with the positional arguments in order. See the argparse documentation for more information.
This is a bit fragile since it relies on understanding the internals of argparse.ArgumentParser, but in lieu of rewriting argparse.ArgumentParser.parse_known_args, here's what I use:
class OrderedNamespace(argparse.Namespace):
def __init__(self, **kwargs):
self.__dict__["_arg_order"] = []
self.__dict__["_arg_order_first_time_through"] = True
argparse.Namespace.__init__(self, **kwargs)
def __setattr__(self, name, value):
#print("Setting %s -> %s" % (name, value))
self.__dict__[name] = value
if name in self._arg_order and hasattr(self, "_arg_order_first_time_through"):
self.__dict__["_arg_order"] = []
delattr(self, "_arg_order_first_time_through")
self.__dict__["_arg_order"].append(name)
def _finalize(self):
if hasattr(self, "_arg_order_first_time_through"):
self.__dict__["_arg_order"] = []
delattr(self, "_arg_order_first_time_through")
def _latest_of(self, k1, k2):
try:
print self._arg_order
if self._arg_order.index(k1) > self._arg_order.index(k2):
return k1
except ValueError:
if k1 in self._arg_order:
return k1
return k2
This works through the knowledge that argparse.ArgumentParser.parse_known_args runs through the entire option list once setting default values for each argument. Meaning that user specified arguments begin the first time __setattr__ hits an argument that it's seen before.
Usage:
options, extra_args = parser.parse_known_args(sys.argv, namespace=OrderedNamespace())
You can check options._arg_order for the order of user specified command line args, or use options._latest_of("arg1", "arg2") to see which of --arg1 or --arg2 was specified later on the command line (which, for my purposes was what I needed: seeing which of two options would be the overriding one).
UPDATE: had to add _finalize method to handle pathological case of sys.argv() not containing any arguments in the list)
There is module especially made to handle this :
https://github.com/claylabs/ordered-keyword-args
without using orderedkwargs module
def multiple_kwarguments(first , **lotsofothers):
print first
for i,other in lotsofothers:
print other
return True
multiple_kwarguments("first", second="second", third="third" ,fourth="fourth" ,fifth="fifth")
output:
first
second
fifth
fourth
third
On using orderedkwargs module
from orderedkwargs import ordered kwargs
#orderedkwargs
def mutliple_kwarguments(first , *lotsofothers):
print first
for i, other in lotsofothers:
print other
return True
mutliple_kwarguments("first", second="second", third="third" ,fourth="fourth" ,fifth="fifth")
Output:
first
second
third
fourth
fifth
Note: Single asterik is required while using this module with decorator above the function.
I needed this because, for logging purposes, I liked to print the arguments after they were parsed. The problem was that the arguments are not printed in order, which was really annoying.
The custom action class just flat out did not work for me. I had other arguments which used a different action such as 'store_true' and default arguments also don't work since the custom action class is not called if the argument is not given in the command line. What worked for me was creating a wrapper class like this:
import collections
from argparse import ArgumentParser
class SortedArgumentParser():
def __init__(self, *args, **kwargs):
self.ap = ArgumentParser(*args, **kwargs)
self.args_dict = collections.OrderedDict()
def add_argument(self, *args, **kwargs):
self.ap.add_argument(*args, **kwargs)
# Also store dest kwarg
self.args_dict[kwargs['dest']] = None
def parse_args(self):
# Returns a sorted dictionary
unsorted_dict = self.ap.parse_args().__dict__
for unsorted_entry in unsorted_dict:
self.args_dict[unsorted_entry] = unsorted_dict[unsorted_entry]
return self.args_dict
The pros are that the add_argument method should have the exact same functionality as the original ArgumentParser. The cons are that if you want other methods you will have to write wrapped for all of them. Luckily for me all I ever used was add_argument and parse_args, so this served my purposes pretty well. You would also need to do more work if you wanted to use parent ArgumentParsers.
This is my simple solution based on the existing ones:
class OrderedNamespace(argparse.Namespace):
def __init__(self, **kwargs):
self.__dict__["_order"] = [None]
super().__init__(**kwargs)
def __setattr__(self, attr, value):
super().__setattr__(attr, value)
if attr in self._order:
self.__dict__["_order"].clear()
self.__dict__["_order"].append(attr)
def ordered(self):
if self._order and self._order[0] is None:
self._order.clear()
return ((attr, getattr(self, attr)) for attr in self._order)
parser = argparse.ArgumentParser()
parser.add_argument('--test1', default=1)
parser.add_argument('--test2')
parser.add_argument('-s', '--slong', action='store_false')
parser.add_argument('--test3', default=3)
args = parser.parse_args(['--test2', '2', '--test1', '1', '-s'], namespace=OrderedNamespace())
print(args)
print(args.test1)
for a, v in args.ordered():
print(a, v)
Output:
OrderedNamespace(_order=['test2', 'test1', 'slong'], slong=False, test1='1', test2='2', test3=3)
1
test2 2
test1 1
slong False
It allows actions in add_argument(), which is harder for customized action class solution.

python + argparse - how to get order of optional arguments from command line

I would like to know how to get order of optional argument passed from commandline to argparse
I have image processing class which is able to apply different actions to image - like rotate, crop, resize...
And order in which these actions are applied is often essential (for example: you want to crop image before you resize it)
I have this code:
parser = argparse.ArgumentParser(description='Image processing arguments')
parser.add_argument('source_file', help='source file')
parser.add_argument('target_file', help='target file')
parser.add_argument('-resize', nargs=2, help='resize image', metavar=('WIDTH', 'HEIGHT'))
parser.add_argument('-rotate', nargs=1, help='rotate image', metavar='ANGLE')
parser.add_argument('-crop', nargs=4, help='crop image', metavar=('START_X','START_Y','WIDTH','HEIGHT'))
ar = parser.parse_args()
print ar
But - no matter in which order I pass parameters to script:
cmd.py test.jpg test2.jpg -crop 10 10 200 200 -resize 450 300
cmd.py test.jpg test2.jpg -resize 450 300 -crop 10 10 200 200
in Namespace items are always in same order (alphabetical I suppose):
Namespace(crop=['10', '10', '200', '200'], resize=['450', '300'], rotate=None, source_file='test.jpg', target_file='test
2.jpg')
Is there way to order them by position in command line string or to get their index?
You could always peek at sys.argv which is a list (and thus ordered) and simply iterate over it checking which argument comes first or use the list.index() to see the respective positions of your keywords in the list...
sys.argv contains a list of the words entered in the command line (the delimiter of such "word" is a space unless a string was surrounded by quotation marks). This means that if the user entered something like ./my_proggie -resize 500 then sys.argv would contain a list like this: ['./my_proggie', '-resize', '500'].
The Namespace is a simple object whose str() lists its attributes according to the order of the keys in its __dict__. Attributes are set with setattr(namespace, dest, value).
One solution is to define a custom Namespace class. For example:
class OrderNamespace(argparse.Namespace):
def __init__(self, **kwargs):
self.__dict__['order'] = []
super(OrderNamespace, self).__init__(**kwargs)
def __setattr__(self,attr,value):
self.__dict__['order'].append(attr)
super(OrderNamespace, self).__setattr__(attr, value)
and use
args = parser.parse_args(None, OrderNamespace())
producing for your two examples
OrderNamespace(crop=..., order=[..., 'crop', 'resize'], resize=...)
OrderNamespace(crop=..., order=[..., 'resize', 'crop'], resize=...)
The order attribute gives the order in which the other attributes are set. The initial items are for defaults and the file positionals. Adding default=argparse.SUPPRESS to the arguments will suppress some of these items. This custom class could be more elaborate, using for example an OrderedDictionary, only noting the order for selected arguments, or using order to control the display of the attributes.
Another option is to use a custom Action class that creates this order attribute, e.g.
class OrderAction(argparse._StoreAction):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
order = getattr(namespace, 'order') if hasattr(namespace, 'order') else []
order.append(self.dest)
setattr(namespace, 'order', order)
I adapted the approach from hpaulj:
class OrderNamespace(argparse.Namespace):
def __init__(self, **kwargs):
self.__dict__['order'] = []
super(OrderNamespace, self).__init__(**kwargs)
def __setattr__(self,attr,value):
if value:
self.__dict__['order'].append(attr)
super(OrderNamespace, self).__setattr__(attr, value)
parser.add_argument('-g',action='append',default=argparse.SUPPRESS,help='some action')
By adding the "if value:" ... you only get every used argument the correct number of times.
There is a problem with #Martin 's solution: it does not work with cases like this:
parser.add_argument('-s', '--slong', action='store_false')
Here is my solution:
import argparse
class OrderedNamespace(argparse.Namespace):
def __init__(self, **kwargs):
self.__dict__["_order"] = []
super().__init__(**kwargs)
def __setattr__(self, attr, value):
super().__setattr__(attr, value)
if attr in self._order:
self.__dict__["_order"].clear()
self.__dict__["_order"].append(attr)
def ordered(self):
return ((attr, getattr(self, attr)) for attr in self._order)
parser = argparse.ArgumentParser()
parser.add_argument('--test1', default=1)
parser.add_argument('--test2')
parser.add_argument('-s', '--slong', action='store_false')
parser.add_argument('--test3', default=3)
args = parser.parse_args(['--test2', '2', '--test1', '1', '-s'], namespace=OrderedNamespace())
print(args)
print(args.test1)
for a, v in args.ordered():
print(a, v)
Output is:
OrderedNamespace(_order=['test2', 'test1', 'slong'], slong=False, test1='1', test2='2', test3=3)
1
test2 2
test1 1
slong False

Categories

Resources