4

Here's a subsection of my parser configuration

parser.add_argument(
    'infile', help="The file to be imported",
    type=argparse.FileType('r'), default=sys.stdin
)

parser.add_argument(
    '--carpark', nargs='+', dest='CarparkID', type=int, default=[],
    help="One or many carpark IDs"
)

However, the --carpark argument seems to be too greedy and eats anything that follows it:

$ mycommand --carpark 17 ~/path-to-file
mycommand: error: argument --carpark: invalid int value: '/home/oli/path-to-file'

What's a good way around something like this? I need to pass a list of integer IDs into the command but also have a positional file (which can also be stdin).

Is there —for example— a non-greedy nargs option that will only parse as much of this as makes sense?

Oli
  • 235,628
  • 64
  • 220
  • 299
  • Note that it doesn't make much sense to specify a default value for a positional argument, since they are not optional. – chepner Mar 31 '16 at 16:00
  • @chepner The idea there was to allow me to pipe things into the command as well, instead of doing silly redirects. – Oli Mar 31 '16 at 16:12

2 Answers2

5

If you want to specify multiple car park IDs, I would do one of two things instead of using nargs='+':

  1. Use the option once per ID (mycommand --carpark 17 --carpark 18)

    parser.add_argument('--carpark',
                        dest='carpark_ids',
                        type=int,
                        action='append',
                        default=[],
                        help="One carpark ID (can be used multiple times)"
    )
    
  2. Take a single, comma-delimited argument instead (mycommand --carpark 17,18)

    def intlist(s):
        rv = []
        for x in s.split(','):
            try:
                x = int(x)
            except ValueError:
                raise ArgumentTypeError("Non-integer carpark id {x}" % (x,))
            rv.append(x)
        return rv
    
    parser.add_argument('--carpark',
                        type=intlist,
                        dest='carpark_ids',
                        default=[],
                        help="One or more carpark IDs"
    )
    

    With a little more work, you could modify this to allow multiple uses of --carpark to accumulate all its values into a single list.

A third alternative, one I'm not particularly fond of, is to abandon the positional argument, making it an optional argument instead. (mycommand --carpark 17 18 --infile ~/path-to-file).

parser.add_argument('--infile',
                    help="The file to be imported",
                    type=argparse.FileType('r'),
                    default=sys.stdin
)

parser.add_argument('--carpark',
                    nargs='+',
                    dest='CarparkID',
                    type=int,
                    default=[],
                    help="One or many carpark IDs"
)
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Great suggestions. In python3, for option2, it should be `type=lambda s: list(map(str, s.split(","))),` – user3479780 Jun 12 '20 at 08:10
  • 1
    At that point, I would probably switch to `[int(x) for x in s.split(",")]` (which would work in Python 2, but I liked the brevity of using `map`). – chepner Jun 12 '20 at 11:39
  • 1
    Looking back, I would probably also define a named function (with a type-looking name) like `intlist` rather than using a `lambda`. That would also allow you to raise an `ArgumentTypeError` rather than propagating the `ValueError` if a non-integer value is included in the input. – chepner Jun 12 '20 at 11:40
4

Does this work?

$ mycommand ~/path-to-file --carpark 17 

There is a Python bug/issue over + Actions that consume too many arguments, leaving none for following Actions.

The argument allocation is based on argument counts, not on type. The type function is applied after allocation, and there's no provision for returning 'rejected' arguments.

In correct behavior it should take into account that infile is expecting an argument - and there aren't any further flag strings - and thus reserve one string to that Action.

I could look that bug/issue up, but for now the fix is to supply the arguments in a different order. Or define a --infile action (instead of the positional).

An earlier question, with the bug/issue link (my answer focuses more on getting the usage right).

Argparse - do not catch positional arguments with `nargs`.

Community
  • 1
  • 1
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • It does but it makes the argument positional. It has to be the last one. In practice there are actually a lot more arguments than I'm letting on and there is another nargs="+" argument that gives the same issue. Needless to say it's impossible to use both arguments together. – Oli Mar 31 '16 at 15:43