[PEAK] options.txt

R. David Murray rdmurray at bitdance.com
Wed Dec 1 11:48:30 EST 2004


On Tue, 30 Nov 2004, Phillip J. Eby wrote:
> At 04:21 PM 11/30/04 -0500, R. David Murray wrote:
> Well, maybe you could explain what you have in mind, or at least propose
> your ideal API spelling, so I have some idea of what you mean.  :)

Well, just extending the options api so that I could make similar
declarations about command arguments (positional) would probably
work.  The tricky bit I suppose is spelling sub-subcommand dispatching.
Does peak currently have a spelling for that?  I poked around a bit
and couldn't find one, but I haven't looked too hard yet.  I'm
guessing there's some trick for reusing what the peak subcommand
dispatcher does.

Other than subsubcommand dispatching, I think the option handling
spelling would work, as long as I could specify what it applied to
by position rather than by option name.

For example, I have a command that goes like this:

set account 20 username fred
set product 15 quantity 2

I'm going to ignore the sub-subcommand dispatch and just talk about
what happens when, say, setaccountSubCmd gets called.  Suppose I
could spell it like this:


class setaccountSubCmd:

    fieldlist = ['username', 'address', 'etc']

    [args.arg_handler(pos=1, type=int, metavar="ACCTNUM",
        help="the number of the account to modify")]
    def getAccount(self, parser, argindex, argval, remaining_args):
        self.account = Accounts.get(argval)
        if account is None:
            raise ValueError("Unknown account number %s" % argval)

    [args.arg_handler(pos=2, type=string, metavar="FIELDNAME",
        help="the field to modify (one of %s)" % textList(self.fieldlist))]
    def getFieldname(self, parser, argindex, argval, remaining_args):
        self.fieldname = matchOneOf(argval, self.fieldlist)

    [args.arg_handler(pos=3, type=NOT_GIVEN, metavar="VALUE",
        help=("the value to assign to the field.  All arguments"
            " after the fieldname are part of the value"))]
    def getValue(self, parser, argindex, argval, remaining_args):
        value = ' '.join([argval]+remaining_args)
        self.value = getattr(self.account.__class__,
            self.fieldname).fromString(value)


My helper functions there do the obvious things of (textList)
formatting a list for printing (a, b, or c) and (matchOneOf) looking
for an unambiguous match in a list of keywords.  The autogenerated
help would look something like:

    set account ACCTNUM FIELDNAME VALUE

        arguments:
            ACCTNUM     the number of the account to modify
            FIELDNAME   the field to modify (one of 'username',
                          'address', or 'etc')
            VALUE       the value to assign to the field.  All
                          arguments after the fieldname are
                          part of the value.

    set product PRODUCTID FIELDNAME VALUE
        [...]

I happen to choose an example from my actual work that required
three functions, but there would be other cases where a simple
'int' or 'string' type field would be declared without a function.

Hrm.  Which makes me realize that I should have written these as
creation functions so that I could reuse them.  I have several
functions that need an account, so I should have a global function:

    def getAccount(argval):
        account = Accounts.get(argval)
        if account is None:
            raise ValueError("Unknown account number %s" % argval)
        return account

and then do:

    acct_arg = args.Set(pos=1, type=getAccount, help=("the numeric identifier"
        " for an account"))

    class setaccountSubCmd:
        args.metadata(account=[acct_arg])

This has a reuse problem since in some other command the account id
might not be argument 1.  Not sure how to spell it to fix that,
though.

On the other hand, the way I have spelled this over the years, using
code I've hacked up as I needed it and never fully systematized, is
like this:

class setaccountSubCmd:

    fieldnames = 'username', 'address', 'etc'

    def _run(self):
        args = self.argv[1:]
        account, args = getAccount(args)
        fieldname, args = matchOneOf(args, self.fieldnames)
        value = getattr(account.__class__, fieldname).fromString(' '.join(args))

(Of course, when I developed this idiom I wasn't using it in a peak
function, I was using it at the top level of a script.  The full
idiom involves wrapping the parsing in a try/except, catching the
parsing errors my functions throw, and exiting cleanly with an error
message.)

The general pattern is a parsing function that does its thing and
returns a tuple of the parsed argument and the remaining args.  I
also have a function "nomoreargs" which just throws the equivalent
of an InvocationError if args isn't null by that point.  My functions
take optional help strings, but I don't have any way of producing
automatic help text out of it.  I only use them for error messages,
and I've never gotten around to trying to fix that defect.

This spelling seems to be slightly more compact and solves the
reuse-in-different-positions problem, but it has the severe
disadvantage that it doesn't give you access to the metadata
to do things like automatically build help text.  It also
doesn't allow a subclass to modify the argument parsing other
than by completely rewriting it.

Well, that was a lot of rambling and may not be of much use, since
there's nothing like optparse to build on top of to implement any
of this, nor have I suggested a solution to the specification of
argument position that allows reuse.  But it's at least a fuller
explanation of my thoughts.

--David



More information about the PEAK mailing list