Search code examples
pythonoptparse

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 options 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?


Solution

  • 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)