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: e.g. 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_mytype(option, opt, value)

where ``option`` is an ``Option`` instance, ``opt`` is an option string
(e.g., ``"-f"``), and ``value`` is the string from the command line that
must be checked and converted to your desired type.  ``check_mytype()``
should return an object of the hypothetical type ``mytype``.  The value
returned by a type-checking function will wind up in the OptionValues
instance returned by ``OptionParser.parse_args()``, or be passed to a
callback 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 added built-in
support for complex numbers, 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", type="complex")

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
    current 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.

These are overlapping sets: some default "store" actions are ``store``,
``store_const``, ``append``, and ``count``, while 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 optik.option_parser.Values class,
  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, i.e. 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 ``error()``
2) subclass Option and override ``take_action()`` -- 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.

.. $Id: extending.txt 413 2004-09-28 00:59:13Z greg $
