Extending Optik
===============

Since the two major controlling factors in how Optik interprets
command-line options are the action and type of each option, the most
likely direction of extension is to add new actions and new types.

Also, the examples/ directory of the source distribution includes
several demonstrations of extending Optik in different ways: eg. a
case-insensitive option parser, or two kinds of option parsers that
implement "required options".


Adding new types
----------------

To add new types, you need to define your own subclass of Optik's Option
class.  This class has a couple of attributes that define Optik's types:
TYPES and TYPE_CHECKER.

TYPES is a tuple of type names; in your subclass, simply define a new
tuple TYPES that builds on the standard one.

TYPE_CHECKER is a dictionary mapping type names to type-checking
functions.  A type-checking function has the following signature::

  def check_foo (option : Option, opt : string, value : string)
                -> foo

You can name it whatever you like, and make it return any type you
like.  The value returned by a type-checking function will wind up in
the OptionValues instance returned by OptionParser.parse_args(), or be
passed to callbacks as the 'value' parameter.

Your type-checking function should raise OptionValueError if it
encounters any problems.  OptionValueError takes a single string
argument, which is passed as-is to OptionParser's error() method, which
in turn prepends the program name and the string "error:" and prints
everything to stderr before terminating the process.

Here's a silly example that demonstrates adding a "complex" option type
to parse Python-style complex numbers on the command line.  (This is
even sillier than it used to be, because Optik 1.3 adds built-in support
for complex numbers [purely for completeness], but never mind.)

First, the necessary imports::

  from copy import copy
  from optik import Option, OptionValueError

You need to define your type-checker first, since it's referred to later
(in the TYPE_CHECKER class attribute of your Option subclass)::

  def check_complex (option, opt, value):
      try:
          return complex(value)
      except ValueError:
          raise OptionValueError(
              "option %s: invalid complex value: %r" % (opt, value))

Finally, the Option subclass::

  class MyOption (Option):
      TYPES = Option.TYPES + ("complex",)
      TYPE_CHECKER = copy(Option.TYPE_CHECKER)
      TYPE_CHECKER["complex"] = check_complex

(If we didn't make a copy() of Option.TYPE_CHECKER, we would end up
modifying the TYPE_CHECKER attribute of Optik's Option class.  This
being Python, nothing stops you from doing that except good manners and
common sense.)

That's it!  Now you can write a script that uses the new option type
just like any other Optik-based script, except you have to instruct your
OptionParser to use MyOption instead of Option::

  parser = OptionParser(option_class=MyOption)
  parser.add_option("-c", action="store", type="complex", dest="c")

Alternately, you can build your own option list and pass it to
OptionParser; if you don't use add_option() in the above way,
you don't need to tell OptionParser which option class to use::

  option_list = [MyOption("-c", action="store", type="complex", dest="c")]
  parser = OptionParser(option_list=option_list)


Adding new actions
------------------

Adding new actions is a bit trickier, because you have to understand
that Optik has a couple of classifications for actions:

"store" actions
    actions that result in Optik storing a value to an attribute
    of the OptionValues instance; these options require a 'dest'
    attribute to be supplied to the Option constructor
"typed" actions
    actions that take a value from the command line and expect it to be
    of a certain type; or rather, a string that can be converted to a
    certain type.  These options require a 'type' attribute to the
    Option constructor.

Some default "store" actions are "store", "store_const", "append", and
"count".  The default "typed" actions are "store", "append", and
"callback".

When you add an action, you need to decide if it's a "store" action, a
"typed" action, neither, or both.  Three class attributes of
Option (or your Option subclass) control this:

ACTIONS
    all actions must be listed in ACTIONS
STORE_ACTIONS
    "store" actions are additionally listed here
TYPED_ACTIONS
    "typed" actions are additionally listed here

In order to actually implement your new action, you must override
Option's take_action() method and add a case that recognizes your
action.

For example, let's add an "extend" action.  This is similar to the
standard "append" action, but instead of taking a single value from the
command-line and appending it to an existing list, "extend" will take
multiple values in a single comma-delimited string, and extend an
existing list with them.  That is, if "--names" is an "extend" option of
type string, the command line ::

  --names=foo,bar --names blah --names ding,dong

would result in a list ::

  ["foo", "bar", "blah", "ding", "dong"]

Again we define a subclass of Option::

  class MyOption (Option):

      ACTIONS = Option.ACTIONS + ("extend",)
      STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",)
      TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",)

      def take_action (self, action, dest, opt, value, values, parser):
          if action == "extend":
              lvalue = value.split(",")
              values.ensure_value(dest, []).extend(lvalue)
          else:
              Option.take_action(
                  self, action, dest, opt, value, values, parser)

Features of note:

* "extend" both expects a value on the command-line and stores that
  value somewhere, so it goes in both STORE_ACTIONS and TYPED_ACTIONS

* MyOption.take_action() implements just this one new action, and
  passes control back to Option.take_action() for the standard
  Optik actions

* 'values' is an instance of the Values class defined in
  optik.option_parser, which provides the very useful
  ensure_value() method.  ensure_value() is essentially getattr()
  with a safety valve; it is called as ::

    values.ensure_value(attr, value)

  If the 'attr' attribute of 'values' doesn't exist or is None, then
  ensure_value() first sets it to 'value', and then returns 'value.
  This is very handy for actions like "extend", "append", and "count",
  all of which accumulate data in a variable and expect that variable
  to be of a certain type (a list for the first two, an integer for
  the latter).  Using 'ensure_value()' means that scripts using your
  action don't have to worry about setting a default value for the
  option destinations in question; they can just leave the default as
  None and ensure_value() will take care of getting it right when it's
  needed.


Other reasons to extend Optik
-----------------------------

Adding new types and new actions are the big, obvious reasons why you
might want to extend Optik.  I can think of at least two other areas to
play with.

First, the simple one: OptionParser tries to be helpful by calling
sys.exit() when appropriate, ie. when there's an error on the
command-line or when the user requests help.  In the former case, the
traditional course of letting the script crash with a traceback is
unacceptable; it will make users think there's a bug in your script when
they make a command-line error.  In the latter case, there's generally
not much point in carrying on after printing a help message.

If this behaviour bothers you, it shouldn't be too hard to "fix" it.
You'll have to

1) subclass OptionParser and override the error() method
2) subclass Option and override the take_action() method -- you'll
   need to provide your own handling of the "help" action that
   doesn't call sys.exit()

The second, much more complex, possibility is to override the
command-line syntax implemented by Optik.  In this case, you'd leave the
whole machinery of option actions and types alone, but rewrite the code
that processes sys.argv.  You'll need to subclass OptionParser in any
case; depending on how radical a rewrite you want, you'll probably need
to override one or all of parse_args(), _process_long_opt(), and
_process_short_opts().

Both of these are left as an exercise for the reader.  I have not tried
to implement either myself, since I'm quite happy with Optik's default
behaviour (naturally).

Happy hacking, and don't forget: Use the Source, Luke.
