5

I'm trying to get to know optparse a bit better, but I'm struggling to understand why the following code behaves the way it does. Am I doing something stupid?

import optparse

def store_test(option, opt_str, value, parser, args=None, kwargs=None):
    print 'opt_str:', opt_str
    print 'value:', value

op = optparse.OptionParser()
op.add_option('-t', '--test', action='callback', callback=store_test, default='test', 
    dest='test', help='test!')

(opts, args) = op.parse_args(['test.py', '-t', 'foo'])

print
print 'opts:'
print opts
print 'args:'
print args

Output:

opt_str: -t
value: None

opts:
{'test': 'test'}
args:
['foo']

Why is 'foo' not being passed to store_test() and instead being interpreted as an extra argument? Is there something wrong with op.parse_args(['-t', 'foo'])?

http://codepad.org/vq3cvE13

Edit:

Here's the example from the docs:

def store_value(option, opt_str, value, parser):
    setattr(parser.values, option.dest, value)
[...]
parser.add_option("--foo",
                  action="callback", callback=store_value,
                  type="int", nargs=3, dest="foo")
Acorn
  • 49,061
  • 27
  • 133
  • 172
  • 1
    Perhaps off-topic, but you should definitely give `argparse` a try : http://docs.python.org/dev/library/argparse.html – Evpok Jul 05 '11 at 14:16
  • 1
    You don't actually want to pass in the filename - http://docs.python.org/library/optparse.html says "by default [parse_args()] uses `sys.argv[1:]`". – user812786 Jul 05 '11 at 14:17
  • If you're overriding the default by giving it an argument then that's not relevant though is it? Here's the relevant code from `optparse.py`: `if args is None: return sys.argv[1:] else: return args[:]` – Acorn Jul 05 '11 at 14:20
  • It'll still work, but just ignore the filename since it's not an option. – user812786 Jul 05 '11 at 14:29
  • 1
    Seconding @Evpok, chances are, you shouldn't be wasting your time learning `optparse`, which is [deprecated](http://docs.python.org/library/optparse.html#optparse-option-callbacks) as of python 2.7. Learn [`argparse`](http://docs.python.org/library/argparse.html) instead. – senderle Jul 05 '11 at 14:31
  • It will not ignore the filename if you are supplying a list of arguments to `parse_args()`. @senderle - not keen on adding another dependency to a simple script though.. – Acorn Jul 05 '11 at 14:33
  • @Acorn, not sure what you mean. Assuming you're using python 2.7+, `argparse` is in the standard library -- is that really a "dependency"? Also, you wouldn't be adding a dependency, you'd be exchanging one "dependency" on a standard module for another. Of course if you're using an older version of python, that's another matter. In any case, it was just a suggestion :) – senderle Jul 05 '11 at 14:38
  • 1
    @senderle - I'm using 2.6. I'll definitely be taking a look into `argparse` soon though, thanks for the suggestion :) – Acorn Jul 05 '11 at 14:39

1 Answers1

6

You're missing a "type" or "nargs" option attribute:

op.add_option('-t', '--test', action='callback', callback=store_test, default='test',
    dest='test', help='test!', type='str')

This option will cause it to consume the next argument.

Reference: http://docs.python.org/library/optparse.html#optparse-option-callbacks

type
has its usual meaning: as with the "store" or "append" actions, it instructs optparse to consume one argument and convert it to type. Rather than storing the converted value(s) anywhere, though, optparse passes it to your callback function.

nargs
also has its usual meaning: if it is supplied and > 1, optparse will consume nargs arguments, each of which must be convertible to type. It then passes a tuple of converted values to your callback.

This seems to be the relevant code from optparse.py:

def takes_value(self):
    return self.type is not None

def _process_short_opts(self, rargs, values):
    [...]
        if option.takes_value():
            # Any characters left in arg?  Pretend they're the
            # next arg, and stop consuming characters of arg.
            if i < len(arg):
                rargs.insert(0, arg[i:])
                stop = True

            nargs = option.nargs
            if len(rargs) < nargs:
                if nargs == 1:
                    self.error(_("%s option requires an argument") % opt)
                else:
                    self.error(_("%s option requires %d arguments")
                               % (opt, nargs))
            elif nargs == 1:
                value = rargs.pop(0)
            else:
                value = tuple(rargs[0:nargs])
                del rargs[0:nargs]

        else:                       # option doesn't take a value
            value = None

        option.process(opt, value, values, self)
Acorn
  • 49,061
  • 27
  • 133
  • 172
user812786
  • 4,302
  • 5
  • 38
  • 50
  • Thanks, turns out that it doesn't work without the `str` option. Odd. The `nargs` option is definitely not needed as the default number of `args` to consume is `1`. – Acorn Jul 05 '11 at 14:28
  • After testing, `type` is the key, not `nargs`. If you specify `nargs=1`, but no `type` it still doesn't work. – Acorn Jul 05 '11 at 14:36
  • This is kind of a nasty gotcha, IMHO. I can see that, if I had explicitly said "type=None", it might imply, "hey, there should be No Argument Here." But I can't justify *implicitly* implying that the callback should have 0-arity – skatenerd May 21 '15 at 08:33