1

I want to input args that not configured in argparse:

parser = argparse.ArgumentParser(prog='PROG')

subparsers = parser.add_subparsers(help='sub-command help', dest="character", required=False)
subparsers.required = False

base_subparser = argparse.ArgumentParser(add_help=False)
# define common shared arguments
base_subparser.add_argument('--disable', choices=['false', 'true'])
base_subparser.add_argument('--foo', choices=['false', 'true'])
# create the parser for the "a" command
parser_a = subparsers.add_parser('a', help='a help', parents=[base_subparser])
parser_a.add_argument('--bar', choices='ABC', help='bar help')

# create the parser for the "b" command
parser_b = subparsers.add_parser('b', help='b help', parents=[base_subparser])
parser_b.add_argument('--baz', choices='XYZ', help='baz help')

argcomplete.autocomplete(parser)
args = parser.parse_known_args()
print(args)

I use the parse_known_args() which use a list to store the args not configured in argparse. However, when I use ./prog.py key = val, it shows argument character: invalid choice: 'key=val' (choose from 'a', 'b'). So I have to choose 'a' or 'b', how can I input the args not configured in argparse without choose one of the subparsers.

JAY
  • 13
  • 2
  • Even without subparsers you can't use a undeefined `key=value` argument. `subparsers` is the first (and only) positional argument. Uses optionals instead. – hpaulj Sep 16 '22 at 04:06
  • This is a [known open issue](https://github.com/python/cpython/issues/91199). – metatoaster Sep 16 '22 at 04:06
  • @metatoaster, in that issue the user wants to run several subparsers; that's not the case here. This poster has effectively defined a `nargs='?'` positional with two choices. The parser will try to match any nonflag string with those choices. `positionals` are used by position, not value. – hpaulj Sep 16 '22 at 05:14
  • @hpaulj fair, but given that `parse_known_args()` is [documented](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.parse_known_args) to "works much like `parse_args()` except that it does not produce an error when extra arguments are present", not accepting that extra argument would indicate a bug, or at best a mismatching behavior by the function vs. its documentation. Perhaps there is a better candidate on the issue tracker? – metatoaster Sep 16 '22 at 05:17
  • `parse_args` calls `parse_known_args` and raises an error if there's something in the `extras`. Here `parse_known_args` hasn't gotten to the point of putting 'key=value' in `extras`; it' failed at parsing that string as a subparser name. Originally `subparsers` where always `required`. The 'required=false' possibility actually arose by accident, and should be treated with caution. The documetation attempts to describe the code's behavior, it doesn't define it.. – hpaulj Sep 16 '22 at 05:50
  • By the way I used to be heavily involved in `argparse` development, but haven't done much since it moved to github, That linked issue is one of the last I commented on. – hpaulj Sep 16 '22 at 05:54
  • @hpaulj if I don't use subparsers and positional argument, the undefined `key=val` can be processed by a list with the method `parse_known_args()`. But once I use the subparsers, the argument `key=value` will be parsed as a subparser name and failed. – JAY Sep 16 '22 at 06:05
  • @hpaulj subparsers is the positional argument, are there some other optionals can realize the function of subparsers? – JAY Sep 16 '22 at 06:18

1 Answers1

1

The error you see is the same as produced by a '?' positional with choices:

In [25]: import argparse

In [26]: parser = argparse.ArgumentParser() 
In [27]: parser.add_argument('foo', nargs='?', choices=['a','b'])

'foo' is optional:

In [28]: parser.parse_known_args([])
Out[28]: (Namespace(foo=None), [])

In [29]: parser.parse_known_args(['a'])
Out[29]: (Namespace(foo='a'), [])

but any string is parsed as a possible 'foo' value:

In [30]: parser.parse_known_args(['c'])
usage: ipykernel_launcher.py [-h] [{a,b}]
ipykernel_launcher.py: error: argument foo: invalid choice: 'c' (choose from 'a', 'b')

providing a proper choice first, allows it to treat 'c' as an extra:

In [31]: parser.parse_known_args(['a','c'])
Out[31]: (Namespace(foo='a'), ['c'])

Or if the string looks like a optional's flag:

In [32]: parser.parse_known_args(['-c'])
Out[32]: (Namespace(foo=None), ['-c'])

Another possibility is to go ahead and name a subparser, possibly a dummy one, and provide the extra. The subparser will be the one that actually puts that string in the 'unknowns' category.

In [40]: parser = argparse.ArgumentParser()
In [41]: subp = parser.add_subparsers(dest='cmd')
In [44]: p1 = subp.add_parser('a')

In [45]: parser.parse_known_args(['a','c'])
Out[45]: (Namespace(cmd='a'), ['c'])

In [46]: parser.parse_known_args([])       # not-required is the default
Out[46]: (Namespace(cmd=None), [])

Keep in mind that the main parser does not "know" anything about subparsers, except is a positional. It's doing its normal allocating strings to actions. But once it calls a subparser, that parser has full control over the parsing. Once it's done it passes the namespace back to the main, but the main doesn't do any more parsing - it just wraps things up and exits (with results or error).


Since subp is a positional with a special Action subclass, _SubParsersAction, I was thinking it might be possible to create a flagged argument with that class

parser.add_argument('--foo', action=argparse._SubParsersAction)

but there's more going on in add_subparsers, so it isn't a trivial addition. This is a purely speculative idea.

hpaulj
  • 221,503
  • 14
  • 230
  • 353