From: Laura C. <la...@st...> - 2004-07-04 20:20:51
|
One of the problems with finding out how new users see your code is that 'new users' have a very limited shelf-life. They become experienced users, who know what to expect. So when I try something new, I try to keep a record of how it goes to report back. With optparse, it didn't go all that well... I think that some of my frustration comes out in this doc, which is what happens when you are posting your initial impressions of a thing that is causing you problems. I decided to leave the frustration in, assuming that you will know enough not to take it personally, since its a more accurate record of what I went through this way. It means it repeates a little bit, too. I'm using Python 2.3.4 (#2, May 29 2004, 03:31:27) in case it matters, because this is what happened to be on my laptop. Laura ----- I'm writing a program that reads in a file, or stdin, parses it and then rewrites it according to some complicated rules. The question is what to do with the output? There are 3 choices, writing to stdout (the default), rewriting the file inplace, or making a new file, with a new suffix. I figured that I would use this oportunity to investigate optparse. Ok. I have my usage string: "usage: %prog [-s [filename ...] | [-i|-c filename ...]] I figured that I would use this oportunity to investigate optparse. So I get out the Python Library Reference. Skipping the philosophy section, we get down to business with 6.20.2 The first confusion was in 6.20.2.2 and 6.20.2.3 The examples are: parser.add_option("-v", action="store_true", dest="verbose") parser.add_option("-q", action="store_false", dest="verbose") which of course gets me excited, because I figure these examples are mutually exclusive, and thus this should be pretty much what I want. Since I have three values, not two, I figure that I will be able to find an action that stores something other than a Bool. And I am correct, though it takes me reading all the way to 6.20.3.2 Defining options before I find "store_const" and actually all the way to 6.20.3.3 Option actions before I find out how to use the things. Problem #1 ++++++++++ is that '6.20.2.2 Other store_* actions' is incomplete. It needs to mention store_const. A forward reference to 6.20.3.3 would also be helpful. But I think I am in business. I write this as my first attempt:: from optparse import OptionParser usage = "usage: %prog [-s [filename ...] | [-i | -c filename ...]]" optparser = OptionParser(usage) optparser.add_option("-s", "--stdout", action="store_const", dest="output", const='s', default='s', help="send your output to stdout") optparser.add_option("-i", "--InPlace", action="store_const", dest="output", const='i', default='s', help="overwrite files in place") optparser.add_option("-c", "--MakeCopy", action="store_const", dest="output", const='c', default='s', help="copy files ... fn.py --> fn.new.py") options, args = optparser.parse_args() print 'options are: ', options print 'args are: ', args This doesn't handle the 'required filenames' problem, but I figure that it will handle the 'mutually exclusive options' just fine, and I can figure out how to add that later. The whole thing does not work as I expected. Exclusivity is not enforced. Experiment shows that the last guy in wins. Problem #2 ++++++++++ By using mutually exclusive options in your example you create the false sense that you are handling mutually exclusive options. This is a particular problem in '6.20.2.2 Other store_* actions' which reads Flag options--set a variable to true or false when a particular option is seen--are quite common. optparse supports them with two separate actions, ``store_true'' and ``store_false''. For example, you might have a verbose flag that is turned on with -v and off with -q: parser.add_option("-v", action="store_true", dest="verbose") parser.add_option("-q", action="store_false", dest="verbose") Here we have two different options with the same destination, which is perfectly OK. (It just means you have to be a bit careful when setting default values--see below.) This is the point where mentioning that these options are not mutually exclusive would be a fine idea, and I think you really should mention that when you add options that have the same dest, _the last guy in wins_. Right here, I think, is where optparser *should* implement exclusive options. I'm still confused, and surprised that exclusivity isn't being enforced, but I see a section that I expect will be most useful. 6.20.3.6 Conflicts between options It's not. Problem #3 ++++++++++ Section 6.20.3.6 I think is very misleading. It is not about _defining options that conflict with each other_, as you might expect, but instead about defining the same option multiple times. Given that you have used 'conflict' to mean this in your code, I can see where the language comes from, but I think that both the language and the code should be renamed. My first thought is to replace 'conflict' with 'collision', but I have only read the code twice, and hurredly at that, so this may not be the best choice. And there may be reasons in there for liking the name 'conflict' of which I am unaware ... Ok, by this time I have read the whole manual twice, and the code once. I am now firmly convinced that my whole preconceptions about what I ought to find here were _wrong_. And it looks to me as if there is no builtin supplied way to specify 'these options are mutually exclusive', which surprises me a lot. This quick hack is already taking a lot longer than I expected .... But by this time I am committed to seeing this one through, and I am going to see how to get this done with optparse. I figure that I can get away with using a callback. I make this as my next attempt:: from optparse import OptionParser usage = "usage: %prog [-s [filename ...] | [-i | -c filename ...]]" optparser = OptionParser(usage) def select_output (option, opt, value, optparser, **kw): if hasattr(optparser, 'output'): optparser.error( 'Cannot combine -s -i and -c options. Use one only.') else: optparser.output = kw['output'] optparser.add_option("-s", "--stdout", action="callback", callback=select_output, callback_kwargs={'output':'stdout'}, help="send your output to stdout") optparser.add_option("-i", "--InPlace", action="callback", callback=select_output, callback_kwargs={'output':'InPlace'}, help="overwrite files in place") optparser.add_option("-c", "--MakeCopy", action="callback", callback=select_output, callback_kwargs={'output':'MakeCopy'}, help="copy files ... fn.py --> fn.new.py") options, args = optparser.parse_args() print 'options are: ', options print 'args are: ', args Ok, this program enforces exclusivity just fine. And the only thing I would change about how callbacks are documents is to change in 6.20.4.2 How callbacks are called instead of *args and **kwargs, say *callback_args and **callback_kwargs that is to say what you called them when you described how to define them, the page before, in 6.20.4.1 Defining a callback option There will be some people reading this who have never seen a callback before, and they have a hard time seeing where the arguments they want to pass go. Calling the variable the same thing helps these people a lot. But now I am pretty happy. I figure that I can demand that the i and c options take a filename by simply adding type='string', to the signatures. Like this:: from optparse import OptionParser usage = "usage: %prog [-s [filename ...] | [-i | -c filename ...]]" optparser = OptionParser(usage) def select_output (option, opt, value, optparser, **kw): if hasattr(optparser, 'output'): optparser.error( 'Cannot combine -s -i and -c options. Use one only.') else: optparser.output = kw['output'] optparser.add_option("-s", "--stdout", action="callback", callback=select_output, callback_kwargs={'output':'stdout'}, help="send your output to stdout") optparser.add_option("-i", "--InPlace", action="callback", type='string', callback=select_output, callback_kwargs={'output':'InPlace'}, help="overwrite files in place") optparser.add_option("-c", "--MakeCopy", action="callback", type='string', callback=select_output, callback_kwargs={'output':'MakeCopy'}, help="copy files ... fn.py --> fn.new.py") options, args = optparser.parse_args() print 'options are: ', options print 'args are: ', args I try it. :: $ python opt3.py -si usage: opt3.py [-s [filename ...] | [-i | -c filename ...]] opt3.py: error: -i option requires a value So far, so good, though I am not pleased with the blank line. And :: $ python opt3.py -si foo usage: opt3.py [-s [filename ...] | [-i | -c filename ...]] pt3.py: error: Cannot combine -s -i and -c options. Use one only. does what I want, too. But :: $ python opt3.py -is foo options are: <Values at 0x401e76ec: {}> args are: ['foo'] completely surprises me. First of all, I would expect that '-is' would be parsed as 2 options, not using 's' as a filename. But if that is the behaviour that is wanted, I would expect that the args would be ['s', 'foo'], not just foo. Here I believe that I am just in fundamental disagreement with the intent of the program, in the same way that getopt and gnu_getopt disagree about what to do with further arguments after the first non-option argument, because my policy has always been to tell people who want to write 'program -fFILE' to learn to type the space between options and the arguments ;-) I conclude that I don't want the type option, because the future direction of this program is to add more options, but always end with 0 or more filenames, so what I need to do things the hard way. I wonder if there would be value in having an optparse option forbidding -fFILE, and taking such things as options f and F and I and L and E. So I write this:: from optparse import OptionParser usage = "usage: %prog [-s [filename ...] | [-i | -c filename ...]]" optparser = OptionParser(usage) def select_output (option, opt, value, optparser, **kw): if hasattr(optparser, 'output'): optparser.error( 'Cannot combine -s -i and -c options. Use one only.') else: optparser.output = kw['output'] optparser.add_option("-s", "--stdout", action="callback", callback=select_output, callback_kwargs={'output':'stdout'}, help="send your output to stdout") optparser.add_option("-i", "--InPlace", action="callback", callback=select_output, callback_kwargs={'output':'InPlace'}, help="overwrite files in place") optparser.add_option("-c", "--MakeCopy", action="callback", callback=select_output, callback_kwargs={'output':'MakeCopy'}, help="copy files ... fn.py --> fn.new.py") options, args = optparser.parse_args() output = getattr(optparser, 'output', 'stdout') if output in ['InPlace', 'MakeCopy'] and not args: optparser.error( '-i and -c option require at least one filename') print 'options are: ', options print 'args are: ', args This works, but already I am unhappy, because I don't want to hard code that '-i and -c option' .... I want to refer to whatever it was that the user typed which I parsed as an option ... that options looks tempting, but no... dir(options) yields: ['__doc__', '__init__', '__module__', '__repr__', '_update', '_update_careful', '_update_loose', 'ensure_value', 'read_file', 'read_module'] Now, I could go back and rewrite each optparser.add_option, making two versions for each option, and also storing the values that the user typed, but I am getting a little tired of this. And I feel as if I have been trying to use a sledge-hammer in order to crack a small nut. What am I doing wrong? There has _got_ to be an easier way to get what I want than this. Laura |