How do you handle options that can't be used together (using OptionParser)?
Asked Answered
B

2

7

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?

Bandit answered 28/4, 2010 at 12:39 Comment(0)
A
6

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)
Archivolt answered 28/4, 2010 at 12:55 Comment(4)
super() will not work for Python 2.X, as OptionParser is created as an old-style class. A solution is provided on Stack Overflow question 2023940.Fenestrated
Thanks, I've fixed it by spelling out the superclass explicitly.Salpinx
@Tamás: I don't think this solution actually works in practice, for several reasons. First, parser is missing from the "slots" definition in the Conflict class, and I think the parser.has_option() call should be self.parser.has_option(). More importantly, the parser.has_option() bit just checks if the parser has those options defined, not if those options were actually passed by the user.Janae
@tonycpsu: thanks for the head up, I have fixed these problems now. I missed gotgenes's answer previously since I never came back to this question once I posted my answer. Hopefully the current version works properly.Salpinx
F
3

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 calls optparse.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 in optparse's general design, but a feature for this package in that it is at least consistent with optparse behavior.)
Fenestrated answered 20/1, 2011 at 6:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.