How do you handle options that can't be used together (using OptionParser)?
My Python script (for todo lists) is started from the command line like this:
todo [options] <command> [command-options]
Some option开发者_如何学Pythons can not be used together, for example
todo add --pos=3 --end "Ask Stackoverflow"
would specify both the third position and the end of the list. Likewise
todo list --brief --informative
would confuse my program about being brief or informative. Since I want to have quite a powerful option control, cases like these will be a bunch, and new ones will surely arise in the future. If a users passes a bad combination of options, I want to give an informative message, preferably along with the usage help provided by optparse. Currently I handle this with an if-else statement that I find really ugly and poor. My dream is to have something like this in my code:
parser.set_not_allowed(combination=["--pos", "--end"],
message="--pos and --end can not be used together")
and the OptionParser would use this when parsing the options.
Since this doesn't exist as far as I know, I ask the SO community: How do you handle this?
Possibly by extending optparse.OptionParser
:
class Conflict(object):
__slots__ = ("combination", "message", "parser")
def __init__(self, combination, message, parser):
self.combination = combination
self.message = str(message)
self.parser = parser
def accepts(self, options):
count = sum(1 for option in self.combination if hasattr(options, option))
return count <= 1
class ConflictError(Exception):
def __init__(self, conflict):
self.conflict = conflict
def __str__(self):
return self.conflict.message
class MyOptionParser(optparse.OptionParser):
def __init__(self, *args, **kwds):
optparse.OptionParser.__init__(self, *args, **kwds)
self.conflicts = []
def set_not_allowed(self, combination, message):
self.conflicts.append(Conflict(combination, message, self))
def parse_args(self, *args, **kwds):
# Force-ignore the default values and parse the arguments first
kwds2 = dict(kwds)
kwds2["values"] = optparse.Values()
options, _ = optparse.OptionParser.parse_args(self, *args, **kwds2)
# Check for conflicts
for conflict in self.conflicts:
if not conflict.accepts(options):
raise ConflictError(conflict)
# Parse the arguments once again, now with defaults
return optparse.OptionParser.parse_args(self, *args, **kwds)
You can then handle ConflictError
where you call parse_args
:
try:
options, args = parser.parse_args()
except ConflictError as err:
parser.error(err.message)
Tamás's answer is a good start, but I couldn't get it to work, as it has (or had) a number of bugs, including a broken call to super, "parser"
missing in Conflict.__slots__
, always raising an error when a conflict is specified because of the use of parser.has_option()
in Conflicts.accepts()
, etc.
Since I really needed this feature, I rolled my own solution and have made it available from the Python Package Index as ConflictsOptionParser. It works pretty much as a drop in replacement for optparse.OptionParser
. (I do know argparse
is the new command line parsing hotness, but it is not available in Python 2.6 and below and has less adoption currently than optparse
. Send me an email if you'd like to hack up or have hacked up an additional argparse
-based solution.) The key is two new methods, register_conflict()
, and, to a lesser extent, unregister_conflict()
:
#/usr/bin/env python
import conflictsparse
parser = conflictsparse.ConflictsOptionParser("python %prog [OPTIONS] ARG")
# You can retain the Option instances for flexibility, in case you change
# option strings later
verbose_opt = parser.add_option('-v', '--verbose', action='store_true')
quiet_opt = parser.add_option('-q', '--quiet', action='store_true')
# Alternatively, you don't need to keep references to the instances;
# we can re-use the option strings later
parser.add_option('--no-output', action='store_true')
# Register the conflict. Specifying an error message is optional; the
# generic one that is generated will usually do.
parser.register_conflict((verbose_opt, quiet_opt, '--no-output'))
# Now we parse the arguments as we would with
# optparse.OptionParser.parse_args()
opts, args = parser.parse_args()
It has a few advantages over the solution begun by Támas:
- It works out of the box and is installable through pip (or
easy_install
, if you must). - Options in a conflict may be specified either by their option strings or by their
optparse.Option
instances, which helps with the DRY principle; if you use the instances, you can change the actual strings without worrying about breaking conflict code. - It follows normal
optparse.OptionParser.parse_args()
behavior and automatically callsoptparse.OptionParser.error()
when it detects conflicting options in the command line arguments, rather than throwing the error directly. (This is both a feature and a bug; kind of a bug inoptparse
's general design, but a feature for this package in that it is at least consistent withoptparse
behavior.)
精彩评论