Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resolve argparse alias back to the original command

I'm using a subparser/subcommand that has an alias.

I'm using the dest option for the subparser to store the name of the subcommand so I can get it later.

Currently if the subcommand's name is reallyLongName and the alias is r (say) then the dest option stores either reallyLongName or r exactly - whatever I typed in gets stored. This is annoying because I now have to check for the name of the command or any of its aliases in order to identify the command.

Is there a way to get argparse to store the subcommand's name in the dest field in some sort of single, canonical text string?

For example, given the following code:

import argparse

parser = argparse.ArgumentParser()

subparsers = parser.add_subparsers(dest='command', help='sub-command help')

parser_ag = subparsers.add_parser(  'mySubcommand',
                                    aliases=['m'],
                                    help='Subcommand help')

print(parser.parse_args('mySubcommand'.split()))

print(parser.parse_args('m'.split()))

the following output is produced:

Namespace(command='mySubcommand')
Namespace(command='m')

Desired result: command has a single, canonical value for both, for example:

Namespace(command='mySubcommand')
Namespace(command='mySubcommand')
like image 876
MikeTheTall Avatar asked Dec 26 '19 21:12

MikeTheTall


People also ask

How do I create an argparse parser in Python?

Creating a parser ¶. The first step in using the argparse is creating an ArgumentParser object: >>>. >>> parser = argparse.ArgumentParser(description='Process some integers.') The ArgumentParser object will hold all the information necessary to parse the command line into Python data types.

How to get/resolve the command from the alias in PowerShell?

How to get/resolve the command from the alias in PowerShell? To resolve the command name from given alias, you need to use the below command. This means the above commands will provide the same result. For example, the output of the below commands would remain the same. You can also use Get-Alias to resolve the alias name. For example,

How do I add optional arguments to argparse?

In general, the argparse module assumes that flags like -f and --bar indicate optional arguments, which can always be omitted at the command line. To make an option required, True can be specified for the required= keyword argument to add_argument ():

What is the default namespace in argparse?

The default is taken from sys.argv. namespace – An object to take the attributes. The default is a new empty Namespace object In most cases, this means a simple Namespace object will be built up from attributes parsed out of the command line: These were the root concepts you need to be familiar with to deal with argparse.


2 Answers

There was a Python bug/issue requesting this - saving the 'base' name, rather than the alias. You can't change that without changing argparse.py code. I think the change would limited to the Action subclass that handles subparsers. https://bugs.python.org/issue36664

But I point out that there's simpler way of handling this. Just use set_defaults as documented near the end of the https://docs.python.org/3/library/argparse.html#sub-commands section. There

parser_foo.set_defaults(func=foo)

is used to set a subparser specific function, but it could just as well be used to set the 'base' name.

parser_foo.set_defaults(name='theIncrediblyLongAlias')
like image 88
hpaulj Avatar answered Oct 20 '22 01:10

hpaulj


This was surprisingly difficult to dig out. When you add a subparser, it gets stored in the parents ._actions attribute. From there it is just digging through attributes to get what you need. Below I create dictionaries to reference the subparser arguments by the dest name, and then added a function that lets us remap the inputted arguments to the primary argument name.

from collections import defaultdict

def get_subparser_aliases(parser, dest):
    out = defaultdict(list)
    prog_str = parser.prog
    dest_dict = {a.dest: a for a in parser._actions}
    try:
        choices = dest_dict.get(dest).choices
    except AttributeError:
        raise AttributeError(f'The parser "{parser}" has no subparser with a `dest` of "{dest}"')

    for k, v in choices.items():
        clean_v = v.prog.replace(prog_str, '', 1).strip()
        out[clean_v].append(k)
    return dict(out)

def remap_args(args, mapping, dest):
    setattr(args, dest, mapping.get(getattr(args, dest)))
    return args

Using your example, we can remap the parse args using:

import argparse

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='command', help='sub-command help')
parser_ag = subparsers.add_parser('mySubcommand',
                                  aliases=['m'],
                                  help='Subcommand help')

args = parser.parse_args('m'.split())
mapping = get_subparser_aliases(parser, 'command')
remap_args(args, mapping, 'command')
print(args)
# prints:
Namespace(command='mySubcommand')

Here is an example of it at work with multiple subparser levels.. We have a parser with an optional argument and a subparser. The subparser has 3 possible arguments, the last of which invoke another subparser (a sub-subparser), with 2 possible arguments.

You can examine either the top level parser or the first level subparser to see alias mappings.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--someoption', '-s', action='store_true')

subparser1 = parser.add_subparsers(help='sub-command help', dest='sub1')
parser_r = subparser1.add_parser('reallyLongName', aliases=['r'])
parser_r.add_argument('foo', type=int, help='foo help')
parser_s = subparser1.add_parser('otherReallyLong', aliases=['L'])
parser_s.add_argument('bar', choices='abc', help='bar help')
parser_z = subparser1.add_parser('otherOptions', aliases=['oo'])

subparser2 = parser_z.add_subparsers(help='sub-sub-command help', dest='sub2')
parser_x = subparser2.add_parser('xxx', aliases=['x'])
parser_x.add_argument('fizz', type=float, help='fizz help')
parser_y = subparser2.add_parser('yyy', aliases=['y'])
parser_y.add_argument('blip', help='blip help')

get_subparser_aliases(parser, 'sub1')
# returns:
{'reallyLongName': ['reallyLongName', 'r'],
 'otherReallyLong': ['otherReallyLong', 'L'],
 'otherOptions': ['otherOptions', 'oo']}

get_subparser_aliases(parser_z, 'sub2')
# returns:
{'xxx': ['xxx', 'x'], 'yyy': ['yyy', 'y']}

Using this with the function above, we can remap the collected args to their longer names.

args = parser.parse_args('-s oo x 1.23'.split())
print(args)
# prints:
Namespace(fizz=1.23, someoption=True, sub1='oo', sub2='x')

for p, dest in zip((parser, parser_z), ('sub1', 'sub2')):
    mapping = get_subparser_aliases(p, dest)
    remap_args(args, mapping, dest)

print(args)
# prints:
Namespace(fizz=1.23, someoption=True, sub1='otherOptions', sub2='xxx')
like image 33
James Avatar answered Oct 20 '22 00:10

James