Merged upstream
authorDmitriy Morozov <dmitriy@mrzv.org>
Thu, 24 Feb 2011 22:58:28 -0800
changeset 182 f8a82de0540d
parent 144 c66e1ada4c08 (current diff)
parent 181 c3cb22c01987 (diff)
child 189 a46bf1c7f346
Merged upstream
docs/_static/custom.css
opster.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgsub	Thu Feb 24 22:58:28 2011 -0800
@@ -0,0 +1,1 @@
+docs/cleanery = http://hg.piranha.org.ua/sphinx-cleanery/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgsubstate	Thu Feb 24 22:58:28 2011 -0800
@@ -0,0 +1,1 @@
+f333de6a34145b7fd49d6c2eac74a7189c0b6f15 docs/cleanery
--- a/.hgtags	Fri Dec 03 11:53:57 2010 -0800
+++ b/.hgtags	Thu Feb 24 22:58:28 2011 -0800
@@ -12,3 +12,8 @@
 bf6908d12aae4a54ac584df5a6f97f84157030d6 0.9.11
 8d3e644647f8b0ebf4c215352f39ad48bf5c5c4c 0.9.12
 e21e182229c16059d00d9b79158ba1f3dd6b70ef 0.9.13
+6dc40423b257bde179cbe4990f295f7edba9865c 1.0
+cfcc54fe6d49330d9ec2923e52f30869afc90485 1.1
+b8f101500f1d030a4b1e9f586136fdfa38de1f62 1.2
+a8a8ae1a3fbe7c27e90f5ae2fef09d78a8327c24 2.0
+a083e23ed554dd9d85d4f3362b9658acf1325259 2.1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Thu Feb 24 22:58:28 2011 -0800
@@ -0,0 +1,12 @@
+.PHONY: help docs arch
+
+help:
+	@echo "Use \`make <target>\` with one of targets:"
+	@echo "  docs  build docs"
+	@echo "  arch  update archlinux pkgbuild"
+
+docs:
+	cd docs && make
+
+arch:
+	python contrib/updatepkg.py
--- a/README	Fri Dec 03 11:53:57 2010 -0800
+++ b/README	Thu Feb 24 22:58:28 2011 -0800
@@ -54,3 +54,11 @@
 
 .. _documentation: http://hg.piranha.org.ua/opster/docs/
 .. _see description: http://hg.piranha.org.ua/opster/docs/overview.html#options-processing
+
+Plans
+-----
+
+ - Better documentation
+ - (under consideration) ability to have few command collectors in a single
+   application (more than one dispatching entry point)
+
--- a/contrib/PKGBUILD	Fri Dec 03 11:53:57 2010 -0800
+++ b/contrib/PKGBUILD	Thu Feb 24 22:58:28 2011 -0800
@@ -1,7 +1,7 @@
 # Maintainer: Andrey Vlasovskikh <andrey.vlasovskikh@gmail.com>
 
 pkgname=python-opster
-pkgver=0.9.13
+pkgver=2.1
 pkgrel=1
 pkgdesc="Python command line parsing speedster"
 arch=(any)
@@ -9,7 +9,7 @@
 license=('BSD')
 depends=('python2')
 source=("http://pypi.python.org/packages/source/o/opster/opster-$pkgver.tar.gz")
-md5sums=('462b102563886fc9dda9719470549c9e')
+md5sums=('771ff7f0b5de4ed99b2228b5a9f85c0e')
 
 build() {
     cd "$srcdir/opster-$pkgver"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/updatepkg.py	Thu Feb 24 22:58:28 2011 -0800
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+
+import os, sys, re, hashlib
+
+ROOT = os.path.join(os.path.dirname(__file__), '..')
+PKGBUILD = os.path.join(ROOT, 'contrib', 'PKGBUILD')
+VER = re.compile('^pkgver=[\d\.]+$', re.M)
+MD5 = re.compile("^md5sums=\('[0-9a-f]+'\)$", re.M)
+
+sys.path.insert(0, ROOT)
+
+if __name__ == '__main__':
+    import opster
+    dist = os.path.join(ROOT, 'dist', 'opster-%s.tar.gz' % opster.__version__)
+    if not os.path.exists(dist):
+        print 'dist .tar.gz is not built yet, exiting'
+        sys.exit(1)
+
+    pkg = open(PKGBUILD).read()
+    pkg = VER.sub('pkgver=%s' % opster.__version__, pkg)
+    md5 = hashlib.md5(open(dist).read()).hexdigest()
+    pkg = MD5.sub("md5sums=('%s')" % md5, pkg)
+    open(PKGBUILD, 'w').write(pkg)
+
+    print 'PKGBUILD updated'
--- a/docs/_static/custom.css	Fri Dec 03 11:53:57 2010 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-@import url("default.css");
-
-body { max-width: 70em; margin: 0 auto; }
--- a/docs/changelog.rst	Fri Dec 03 11:53:57 2010 -0800
+++ b/docs/changelog.rst	Thu Feb 24 22:58:28 2011 -0800
@@ -1,25 +1,57 @@
 Changelog
 ---------
 
-0.9.13
-~~~~~~
+2.1 (2010.01.23)
+~~~~~~~~~~~~~~~~
+
+ - fix help display in case middleware returns original function
+
+2.0 (2010.01.23)
+~~~~~~~~~~~~~~~~
+
+ - fix help display when there is no __doc__ declared for function
+ - ``dict`` type `handling`_
+ - ``.help()`` attribute for every function, printing help on call
+
+.. _handling: http://hg.piranha.org.ua/opster/docs/overview.html#options-processing
+
+1.2 (2010.12.29)
+~~~~~~~~~~~~~~~~
+
+ - fix option display for a list of subcommands if docstring starts with a blank
+   line
+
+1.1 (2010.12.07)
+~~~~~~~~~~~~~~~~
+
+ - _completion was failing to work when global options were supplied to command
+   dispatcher
+
+1.0 (2010.12.06)
+~~~~~~~~~~~~~~~~
+
+ - when middleware was used and command called without arguments, instead of
+   help, traceback was displayed
+
+0.9.13 (2010.11.18)
+~~~~~~~~~~~~~~~~~~~
 
  - fixed exception handling (cleanup previous fix, actually)
  - display only name of application, without full path
 
-0.9.12
-~~~~~~
+0.9.12 (2010.11.02)
+~~~~~~~~~~~~~~~~~~~
 
  - fixed trouble with non-ascii characters in docstrings
 
-0.9.11
-~~~~~~
+0.9.11 (2010.09.19)
+~~~~~~~~~~~~~~~~~~~
 
  - fixed exceptions handling
  - autocompletion improvements (skips middleware, ability of options completion)
 
-0.9.10
-~~~~~~
+0.9.10 (2010.04.10)
+~~~~~~~~~~~~~~~~~~~
 
  - if default value of an option is a fuction, always call it (None is passed in
    case when option is not supplied)
@@ -27,55 +59,7 @@
  - some cleanup with better support for python 3
  - initial support for autocompletion (borrowed from PIP)
 
-0.9.9
-~~~~~
- - Now it's possible to call commands as regular function, where every
-   non-supplied option will receive proper default (defined in option spec)
- - Globaloptions were simply dropped after parsing, fold them in regular options
- - Replace _ with - in command names, same as in options names
- - Respect empty strings as usage
-
-0.9.8
-~~~~~
-Fixed bug with option names clashing with name of arguments for call_cmd. 
-
-0.9.7
-~~~~~
-Library renamed to opster.
-
-0.9.6
-~~~~~
- - Checks for option definition: long name should be specified always, short
-   name should be 1 character in length if available.
- - More specific argument name in guessed usage (this happens if you have not
-   specified usage for command).
- - Ability to add global decorator for all commands. See ``test.py`` in
-   repository for example: ``ui`` object, to handle verbose/quiet options.
+0.9 - 0.9.9 (since 2009.07.13)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-0.9.5
-~~~~~
-Fixed bug, which prevented programs to work without arguments (displayed help
-instead) if they are not using subcommands.
-
-0.9.4
-~~~~~
- - Ability to hide subcommands from help listing.
- - Append program name to subcommand usage.
-
-0.9.3
-~~~~~
-Minor fix for setup.py, to avoid troubles with installing when there is no docs
-in package.
-
-0.9.2
-~~~~~
-Ability to call commands as regular functions, using arguments and keyword
-arguments.
-
-0.9.1
-~~~~~
-Fixed problem with multiple help options in subcommands
-
-0.9
-~~~
-Initial version
+Ancient history ;-)
--- a/docs/conf.py	Fri Dec 03 11:53:57 2010 -0800
+++ b/docs/conf.py	Thu Feb 24 22:58:28 2011 -0800
@@ -7,22 +7,15 @@
 # -- General configuration -----------------------------------------------------
 
 extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest']
-templates_path = ['_templates']
 source_suffix = '.rst'
 master_doc = 'index'
 project = u'Opster'
-copyright = u'2009-2010, Alexander Solovyov'
+copyright = u'2009-2011, Alexander Solovyov'
 version = release = opster.__version__
 exclude_trees = ['_build']
-pygments_style = 'sphinx'
-
 
 # -- Options for HTML output ---------------------------------------------------
 
-html_theme = 'default'
-html_style = 'custom.css'
+html_theme = 'cleanery'
+html_theme_path = ['.']
 html_title = "%s v%s" % (project, version)
-html_static_path = ['_static']
-html_use_smartypants = True
-html_use_index = False
-html_show_sourcelink = False
--- a/docs/index.rst	Fri Dec 03 11:53:57 2010 -0800
+++ b/docs/index.rst	Thu Feb 24 22:58:28 2011 -0800
@@ -1,13 +1,6 @@
-=============
+========
  Opster
-=============
-
-.. toctree::
-   :maxdepth: 1
-
-   changelog
-   overview
-   api
+========
 
 Opster is a command line parser, intended to make writing command line
 applications easy and painless. It uses built-in Python types (lists,
@@ -15,28 +8,87 @@
 concise. Additionally it contains possibility to handle subcommands (i.e.
 ``hg commit`` or ``svn update``).
 
+* Page on PyPI: http://pypi.python.org/pypi/opster/
+* Repository: http://hg.piranha.org.ua/opster/
+
 Features
 --------
 
- - parsing arguments from sys.argv or custom strings
- - converting from string to appropriate Python objects
- - help message generation
- - positional and named arguments
- - subcommands support
- - short, clean and concise definitions
- - ability to shorten names of subcommand and long options
+- parsing arguments from ``sys.argv`` or custom strings
+- :ref:`converting <options-processing>` from string to appropriate Python
+  objects
+- :ref:`help message <help-generation>` generation
+- positional and named arguments (i.e. arguments and options)
+- :ref:`subcommands <subcommands>` support
+- short, clean and concise definitions
+- :ref:`ability to shorten <partial-names>` names of subcommand and long options
+
+Quick example
+-------------
+
+That's an example of an option definition::
+
+  import sys
+  from opster import command
+
+  @command(usage='%name [-n] MESSAGE')
+  def main(message,
+           no_newline=('n', False, "don't print a newline")):
+      'Simple echo program'
+      sys.stdout.write(message)
+      if not no_newline:
+          sys.stdout.write('\n')
+
+  if __name__ == '__main__':
+      main()
+
+Running this program will print the help::
+
+  > ./echo.py
+  echo.py: invalid arguments
+  echo.py [-n] MESSAGE
+
+  Simple echo program
+
+  options:
+
+   -n --no-newline  don't print a newline
+   -h --help        show help
+
+As you can see, here we have defined option to not print newline: keyword
+argument name is a long name for option, default value is a 3-tuple, containing
+short name for an option (can be empty), default value (on base of which
+processing is applied - :ref:`see description <options-processing>`) and a help
+string.
+
+Underscores in long names are converted into dashes.
+
+If you are calling a command with option using long name, you can supply it
+partially. In this case it could look like ``./echo.py --nonew``. This is also
+true for subcommands: read about them and everything else you'd like to know
+further in documentation.
 
 What's nice
 -----------
 
- - Opster is a `single file`_, which means that you can easily include it with
-   your application
- - When you've decorated function as command, you can continue to use it as
-   usual Python function.
- - It's easy to switch between usual command line options parser and
-   subcommands.
+- Opster is a `single file`_, which means that you can easily include it with
+  your application
+- When you've decorated function as command, you can continue to use it as
+  usual Python function.
+- It's easy to switch between usual command line options parser and
+  subcommands.
 
 Read more in :doc:`overview`.
 
+More documentation
+------------------
+
+.. toctree::
+   :maxdepth: 1
+
+   changelog
+   overview
+   api
+   tests
 
 .. _single file: http://hg.piranha.org.ua/opster/file/tip/opster.py
--- a/docs/overview.rst	Fri Dec 03 11:53:57 2010 -0800
+++ b/docs/overview.rst	Thu Feb 24 22:58:28 2011 -0800
@@ -3,7 +3,7 @@
 ==============
 
 Options
--------
+=======
 
 Configuration of option parser is a list of tuples::
 
@@ -13,31 +13,35 @@
           ('', 'pid-file', '', 'name of file to write process ID to')]
 
 Options contents
-^^^^^^^^^^^^^^^^
+----------------
 
 Each tuple is a definition of some option, consisting of 4 elements:
 
- 1. short name
- 2. long name (read note_)
- 3. default value
- 4. help string
+1. short name
+2. long name (read :ref:`note <renaming-note>`)
+3. default value
+4. help string
 
 If a short name renders to False (for example, empty string), then it's not used
 at all. Long name is pretended to be available in any case.
 
+.. _options-processing:
+
 Options processing
-^^^^^^^^^^^^^^^^^^
+------------------
 
 Default value also determines how supplied argument should be parsed:
 
- - function: return value of function called with a specified value is passed
- - integer: value is convert to integer
- - string: value is passed as is
- - list: value is appended to this list
- - boolean/None: ``not default`` is passed and option takes no value
+- function: return value of function called with a specified value is passed
+- integer: value is convert to integer
+- string: value is passed as is
+- list: value is appended to this list
+- dictionary: value is then assumed being in format ``key=value`` and is
+  then assigned to this dictionary, :ref:`example <definitions-test>`
+- boolean/None: ``not default`` is passed and option takes no value
 
 Usage
-^^^^^
+-----
 
 Usage is easy like that::
 
@@ -56,14 +60,15 @@
            pid_file=('', '', 'name of file to write process ID to')):
       pass
 
-.. _note:
+.. _renaming-note:
+.. note::
 
-I think it's easy to understand what's going on here, except that you need to
-know that underscores in the long name will be replaced with dashes at the
-command line. Of course, reverse process happens: if you have option with a dash
-in long name in a definition, it will be replaced with underscore when passed to
-function. This is done to comply with standarts of writing both console
-interfaces and Python application.
+  I think it's easy to understand what's going on here, except that you need to
+  know that underscores in the long name will be replaced with dashes at the
+  command line. Of course, reverse process happens: if you have option with a
+  dash in long name in a definition, it will be replaced with underscore when
+  passed to function. This is done to comply with standarts of writing both
+  console interfaces and Python applications.
 
 After that you can simply call this function as an entry point to your program::
 
@@ -88,8 +93,10 @@
 In this case no type conversion (which is done upon arguments parsing) will be
 performed.
 
+.. _subcommands:
+
 Subcommands
------------
+===========
 
 It's pretty usual for complex application to have some system of subcommands,
 and opster provides facility for handling them. Configuration is simple::
@@ -140,11 +147,13 @@
   if __name__ == '__main__':
       dispatch(cmdtable=cmdtable)
 
+.. _partial-names:
+
 Example usage, calling ``complex_`` with 5 as an argument for ``exit`` option,
 shows that command dispatcher will understand partial names of commands and
 options::
 
-  app har --ex 5
+  app comp --ex 5
 
 But if your program is something like program shown earlier, you can use
 shortened api::
@@ -161,8 +170,10 @@
 special global command table, which allows to call ``dispatch()`` without
 arguments.
 
+.. _help-generation:
+
 Help generation
----------------
+===============
 
 Help is generated automatically and is available by the ``-h/--help`` command
 line option or by ``help`` subcommand (if you're using subcommand system).
@@ -183,9 +194,23 @@
       --exit  exit with supplied code (default: 0)
    -h --help  show help
 
+.. _innerhelp:
+
+If you need to display help from inside your application, you can always use the
+fact that help-displaying function is attached to your function object, i.e.::
+
+  @command()
+  def something():
+      if some_consequences:
+          something.help()
+
+See `example from tests`_.
+
+.. _example from tests: http://hg.piranha.org.ua/opster/file/default/tests/selfhelp.py
+
 
 Tips and tricks
----------------
+===============
 
 There is one thing which may be obvious: it's easy to have "semi-global"
 options. If your subcommands (or scripts) tend to have same options in some
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/tests.rst	Thu Feb 24 22:58:28 2011 -0800
@@ -0,0 +1,1 @@
+../tests/opster.t
\ No newline at end of file
--- a/opster.py	Fri Dec 03 11:53:57 2010 -0800
+++ b/opster.py	Thu Feb 24 22:58:28 2011 -0800
@@ -1,12 +1,12 @@
-# (c) Alexander Solovyov, 2009, under terms of the new BSD License
+# (c) Alexander Solovyov, 2009-2011, under terms of the new BSD License
 '''Command line arguments parser
 '''
 
-import sys, traceback, getopt, types, textwrap, inspect, os
+import sys, traceback, getopt, types, textwrap, inspect, os, copy
 from itertools import imap
 
 __all__ = ['command', 'dispatch']
-__version__ = '0.9.13'
+__version__ = '2.1'
 __author__ = 'Alexander Solovyov'
 __email__ = 'piranha@piranha.org.ua'
 
@@ -18,12 +18,15 @@
 except locale.Error:
     ENCODING = 'UTF-8'
 
-def write(text, out=sys.stdout):
+def write(text, out=None):
+    '''Write output to a given stream (stdout by default)'''
+    out = out or sys.stdout
     if isinstance(text, unicode):
         return out.write(text.encode(ENCODING))
     out.write(text)
 
 def err(text):
+    '''Write output to stderr'''
     write(text, out=sys.stderr)
 
 CMDTABLE = {}
@@ -37,7 +40,7 @@
 
     All arguments are optional:
 
-     - ``options``: options in format described in docs. If not supplied,
+     - ``options``: options in format described later. If not supplied,
        will be determined from function.
      - ``usage``: usage string for function, replaces ``%name`` with name
        of program or subcommand. In case if it's subcommand and ``%name``
@@ -48,6 +51,21 @@
        only with multiple subcommands
      - ``hide``: if command should be hidden from help listing. Used only
        with multiple subcommands, overrides ``shortlist``
+
+    Options should be a list of 4-tuples in format::
+
+      (shortname, longname, default, help)
+
+    Where:
+
+     - ``shortname`` is a single letter which can be used then as an option
+       specifier on command line (like ``-a``). Will be not used if contains
+       falsy value (empty string, for example)
+     - ``longname`` - main identificator of an option, can be used as on a
+       command line with double dashes (like ``--longname``)
+     - ``default`` value for an option, type of it determines how option will be
+       processed
+     - ``help`` string displayed as a help for an option when asked to
     '''
     def wrapper(func):
         try:
@@ -69,6 +87,7 @@
 
         def help_func(name=None):
             return help_cmd(func, replace_name(usage_, sysname()), options_)
+        func.help = help_func
 
         @wraps(func)
         def inner(*args, **opts):
@@ -81,23 +100,23 @@
 
             argv = opts.pop('argv', sys.argv[1:])
             if opts.pop('help', False):
-                return help_func()
+                return func.help()
 
             if args or opts:
                 # no catcher here because this is call from Python
                 return call_cmd_regular(func, options_)(*args, **opts)
 
             try:
-                opts, args = catcher(lambda: parse(argv, options_), help_func)
+                opts, args = catcher(lambda: parse(argv, options_), func.help)
             except Abort:
                 return -1
 
             if opts.pop('help', False):
-                return help_func()
+                return func.help()
 
             try:
                 return catcher(lambda: call_cmd(name_, func)(*args, **opts),
-                               help_func)
+                               func.help)
             except Abort:
                 return -1
 
@@ -149,7 +168,9 @@
     if name == '_completion':       # skip middleware
         worker = lambda: call_cmd(name, func)(*args, **kwargs)
     else:
-        worker = lambda: call_cmd(name, middleware(func))(*args, **kwargs)
+        mwfunc = middleware(func)
+        depth = func == mwfunc and 1 or 2
+        worker = lambda: call_cmd(name, mwfunc, depth=depth)(*args, **kwargs)
 
     try:
         return catcher(worker, help_func)
@@ -161,6 +182,8 @@
 # --------
 
 def help_(cmdtable, globalopts):
+    '''Help generator for a command table
+    '''
     def help_inner(name=None):
         '''Show help for a given help topic or a help overview
 
@@ -180,8 +203,8 @@
                 if shortlist and not cmd.startswith('^'):
                     continue  # short help contains only marked commands
                 cmd = cmd.lstrip('^~')
-                doc = info[0].__doc__ or '(no help text available)'
-                hlp[cmd] = doc.splitlines()[0].rstrip()
+                doc = pretty_doc_string(info[0])
+                hlp[cmd] = doc.strip().splitlines()[0].rstrip()
 
             hlplist = sorted(hlp)
             maxlen = max(map(len, hlplist))
@@ -190,11 +213,7 @@
             write('\ncommands:\n\n')
             for cmd in hlplist:
                 doc = hlp[cmd]
-                if False:  # verbose?
-                    write(' %s:\n     %s\n' % (cmd.replace('|', ', '), doc))
-                else:
-                    write(' %-*s  %s\n' % (maxlen, cmd.split('|', 1)[0],
-                                              doc))
+                write(' %-*s  %s\n' % (maxlen, cmd.split('|', 1)[0], doc))
 
         if not cmdtable:
             return err('No commands specified!\n')
@@ -241,15 +260,16 @@
      -p --port       port to listen on (default: 8000)
      -d --daemonize  daemonize process
         --pid-file   name of file to write process ID to
-    <BLANKLINE>
     '''
     write(usage + '\n')
-    doc = func.__doc__ or '(no help text available)'
+    doc = pretty_doc_string(func)
     write('\n' + doc.strip() + '\n\n')
     if options:
         write(''.join(help_options(options)))
 
 def help_options(options):
+    '''Generator for help on options
+    '''
     yield 'options:\n\n'
     output = []
     for o in options:
@@ -308,8 +328,8 @@
         defmap[pyname] = default
 
         # copy defaults to state
-        if isinstance(default, list):
-            state[pyname] = default[:]
+        if isinstance(default, (list, dict)):
+            state[pyname] = copy.copy(default)
         elif hasattr(default, '__call__'):
             funlist.append(pyname)
             state[pyname] = None
@@ -318,8 +338,10 @@
 
         # getopt wants indication that it takes a parameter
         if not (default is None or default is True or default is False):
-            if short: short += ':'
-            if name: name += '='
+            if short:
+                short += ':'
+            if name:
+                name += '='
         if short:
             shortlist += short
         if name:
@@ -334,8 +356,15 @@
         if t is types.FunctionType:
             del funlist[funlist.index(name)]
             state[name] = defmap[name](val)
-        elif t is types.ListType:
+        elif t is list:
             state[name].append(val)
+        elif t is dict:
+            try:
+                k, v = val.split('=')
+            except ValueError:
+                raise ParseError(name, "wrong definition: '%s' "
+                                 "(should be in format KEY=VALUE)" % val)
+            state[name][k] = v
         elif t in (types.NoneType, types.BooleanType):
             state[name] = not defmap[name]
         else:
@@ -352,6 +381,8 @@
 # --------
 
 def _dispatch(args, cmdtable, globalopts):
+    '''Dispatch arguments list by a command table
+    '''
     cmd, func, args, options = cmdparse(args, cmdtable, globalopts)
 
     if options.pop('help', False):
@@ -362,6 +393,8 @@
     return cmd, func, args, options
 
 def cmdparse(args, cmdtable, globalopts):
+    '''Parse arguments list to find a command, options and arguments
+    '''
     # command is the first non-option
     cmd = None
     for arg in args:
@@ -388,6 +421,7 @@
     return (cmd, cmd and info[0] or None, args, options)
 
 def aliases_(cmdtable_key):
+    '''Get aliases from a command table key'''
     return cmdtable_key.lstrip("^~").split("|")
 
 def findpossible(cmd, table):
@@ -433,7 +467,16 @@
 # --------
 
 def guess_options(func):
-    args, varargs, varkw, defaults = inspect.getargspec(func)
+    '''Get options definitions from function
+
+    They should be declared in a following way:
+
+    def func(longname=(shortname, default, help)):
+        pass
+
+    See docstring of ``command()`` for description of those variables.
+    '''
+    args, _, _, defaults = inspect.getargspec(func)
     for name, option in zip(args[-len(defaults):], defaults):
         try:
             sname, default, hlp = option[:3]
@@ -443,6 +486,8 @@
             pass
 
 def guess_usage(func, options):
+    '''Get usage definition for a function
+    '''
     usage = '%name '
     if options:
         usage += '[OPTIONS] '
@@ -469,7 +514,7 @@
             (e.args[0], ' '.join(e.args[1])))
         raise Abort()
     except ParseError, e:
-        err('%s: %s\n' % (e.args[0], e.args[1]))
+        err('%s: %s\n\n' % (e.args[0], e.args[1].strip()))
         help_func(e.args[0])
         raise Abort()
     except getopt.GetoptError, e:
@@ -480,17 +525,23 @@
         err('%s\n' % e)
         raise Abort()
 
-def call_cmd(name, func):
+def call_cmd(name, func, depth=1):
+    '''Wrapper for command call, catching situation with insufficient arguments
+
+    ``depth`` is necessary when there is a middleware in setup
+    '''
     def inner(*args, **kwargs):
         try:
             return func(*args, **kwargs)
         except TypeError:
-            if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
+            if len(traceback.extract_tb(sys.exc_info()[2])) == depth:
                 raise ParseError(name, "invalid arguments")
             raise
     return inner
 
 def call_cmd_regular(func, opts):
+    '''Wrapper for command for handling function calls from Python
+    '''
     def inner(*args, **kwargs):
         funcargs, _, varkw, defaults = inspect.getargspec(func)
         if len(args) > len(funcargs):
@@ -506,11 +557,13 @@
     return inner
 
 def replace_name(usage, name):
+    '''Replace name placeholder with a command name'''
     if '%name' in usage:
         return usage.replace('%name', name, 1)
     return name + ' ' + usage
 
 def sysname():
+    '''Returns name of executing file'''
     name = sys.argv[0]
     if name.startswith('/'):
         return name.rsplit('/', 1)[1]
@@ -518,11 +571,22 @@
         return name[2:]
     return name
 
+def pretty_doc_string(item):
+    "Doc string with adjusted indentation level of the 2nd line and beyond."
+    raw_doc = item.__doc__ or '(no help text available)'
+    lines = raw_doc.strip().splitlines()
+    if len(lines) <= 1:
+        return raw_doc
+    indent = len(lines[1]) - len(lines[1].lstrip())
+    return '\n'.join([lines[0]] + map(lambda l: l[indent:], lines[1:]))
+
 try:
     from functools import wraps
 except ImportError:
     def wraps(wrapped, assigned=('__module__', '__name__', '__doc__'),
               updated=('__dict__',)):
+        '''functools.wraps replacement for Python 2.4
+        '''
         def inner(wrapper):
             for attr in assigned:
                 setattr(wrapper, attr, getattr(wrapped, attr))
@@ -616,11 +680,14 @@
     }
 
 @command(name='_completion', hide=True)
-def completion(type=('t', 'bash', 'Completion type (bash or zsh)')):
+def completion(type=('t', 'bash', 'Completion type (bash or zsh)'),
+               # kwargs will catch every global option, which we get
+               # anyway, because middleware is skipped
+               **kwargs):
     """Outputs completion script for bash or zsh."""
 
     prog_name = os.path.split(sys.argv[0])[1]
-    print COMPLETIONS[type] % prog_name
+    print COMPLETIONS[type].strip() % prog_name
 
 # --------
 # Exceptions
@@ -641,3 +708,7 @@
 
 class Abort(OpsterError):
     'Processing error, abort execution'
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
--- a/setup.py	Fri Dec 03 11:53:57 2010 -0800
+++ b/setup.py	Thu Feb 24 22:58:28 2011 -0800
@@ -23,9 +23,8 @@
     version = opster.__version__,
     author = opster.__author__,
     author_email = opster.__email__,
-    url = 'http://hg.piranha.org.ua/opster/',
+    url = 'http://piranha.org.ua/opster/',
     classifiers = [
-        'Development Status :: 4 - Beta',
         'Environment :: Console',
         'Intended Audience :: Developers',
         'License :: OSI Approved :: BSD License',
--- a/tests/multicommands.py	Fri Dec 03 11:53:57 2010 -0800
+++ b/tests/multicommands.py	Thu Feb 24 22:58:28 2011 -0800
@@ -9,7 +9,8 @@
 @command(usage='[-t]', shortlist=True)
 def simple(ui,
            test=('t', False, 'just test execution')):
-    '''Just simple command to print keys of received arguments.
+    '''
+    Just simple command to print keys of received arguments.
 
     I assure you! Nothing to look here. ;-)
     '''
@@ -37,6 +38,10 @@
     if opts.get('exit'):
         sys.exit(opts['exit'])
 
+@command(shortlist=True)
+def nodoc():
+    pass
+
 def ui_middleware(func):
     def extract_dict(source, *keys):
         dest = {}
--- a/tests/opster.t	Fri Dec 03 11:53:57 2010 -0800
+++ b/tests/opster.t	Thu Feb 24 22:58:28 2011 -0800
@@ -1,10 +1,29 @@
+.. -*- mode: rst -*-
+
+==============
+ Opster tests
+==============
+
 This is a test suite for opster library. Just read it to get some idea of how it
 works.
 
+
+Actors cast
+-----------
+
 Define some help functions::
 
   $ function run() { name=$1; shift; python "$TESTDIR/$name" "$@"; }
 
+Main characters:
+
+* `multicommands.py <http://hg.piranha.org.ua/opster/file/tip/tests/multicommands.py>`_
+* `test_opts.py <http://hg.piranha.org.ua/opster/file/tip/tests/test_opts.py>`_
+
+
+Action
+------
+
 Check if usage is working::
 
   $ run multicommands.py
@@ -12,8 +31,10 @@
   
   commands:
   
+   nodoc   (no help text available)
    simple  Just simple command to print keys of received arguments.
 
+
 Ok, then let's run it::
 
   $ run multicommands.py simple
@@ -39,34 +60,84 @@
    -q --quiet    suppress output
    -h --help     display help
 
+
+We also have completion::
+
+  $ run multicommands.py _completion
+  # opster bash completion start
+  _opster_completion()
+  {
+      COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \
+                     COMP_CWORD=$COMP_CWORD \
+                     OPSTER_AUTO_COMPLETE=1 $1 ) )
+  }
+  complete -o default -F _opster_completion multicommands.py
+  # opster bash completion end
+
+
 Now we're going to test if a script with a single command will work (not
 everyone needs subcommands, you know)::
 
   $ run test_opts.py
   another: invalid arguments
+  
   test_opts.py [-l HOST] DIR
   
   Command with option declaration as keyword arguments
   
   options:
   
-   -l --listen     ip to listen on (default: localhost)
-   -p --port       port to listen on (default: 8000)
-   -d --daemonize  daemonize process
-      --pid-file   name of file to write process ID to
-   -t --test       testing help for a function (default: test)
-   -h --help       show help
+   -l --listen       ip to listen on (default: localhost)
+   -p --port         port to listen on (default: 8000)
+   -d --daemonize    daemonize process
+      --pid-file     name of file to write process ID to
+   -D --definitions  just some definitions
+   -t --test         testing help for a function (default: test)
+   -h --help         show help
+
+
+Yeah, I've got it, I should supply some arguments::
 
-Yeah, I've got it, I should supply some argument::
+  $ run test_opts.py -d -p 5656 --listen anywhere right-here
+  {'daemonize': True,
+   'definitions': {},
+   'dirname': 'right-here',
+   'listen': 'anywhere',
+   'pid_file': '',
+   'port': 5656,
+   'test': 'test'}
 
-  $ run test_opts.py right-here
+.. _definitions-test:
+
+Now let's test our definitions::
+
+  $ run test_opts.py -D a=b so-what?
   {'daemonize': False,
-   'dirname': 'right-here',
+   'definitions': {'a': 'b'},
+   'dirname': 'so-what?',
    'listen': 'localhost',
    'pid_file': '',
    'port': 8000,
    'test': 'test'}
 
+  $ run test_opts.py -D can-i-haz fail?
+  definitions: wrong definition: 'can-i-haz' (should be in format KEY=VALUE)
+  
+  test_opts.py [-l HOST] DIR
+  
+  Command with option declaration as keyword arguments
+  
+  options:
+  
+   -l --listen       ip to listen on (default: localhost)
+   -p --port         port to listen on (default: 8000)
+   -d --daemonize    daemonize process
+      --pid-file     name of file to write process ID to
+   -D --definitions  just some definitions
+   -t --test         testing help for a function (default: test)
+   -h --help         show help
+
+
 Should we check passing some invalid arguments? I think so::
 
   $ run test_opts.py --wrong-option
@@ -78,11 +149,43 @@
   
   options:
   
-   -l --listen     ip to listen on (default: localhost)
-   -p --port       port to listen on (default: 8000)
-   -d --daemonize  daemonize process
-      --pid-file   name of file to write process ID to
-   -t --test       testing help for a function (default: test)
-   -h --help       show help
+   -l --listen       ip to listen on (default: localhost)
+   -p --port         port to listen on (default: 8000)
+   -d --daemonize    daemonize process
+      --pid-file     name of file to write process ID to
+   -D --definitions  just some definitions
+   -t --test         testing help for a function (default: test)
+   -h --help         show help
+
+
+Another things should be checked: calling help display from the function
+itself::
+
+  $ run selfhelp.py --assist
+  selfhelp.py [OPTIONS] 
+  
+  Displays ability to show help
+  
+  options:
+  
+      --assist  show help
+   -h --help    show help
+
+
+Are we getting nicely stripped body when not following subject/body convention
+of writing commands?
+
+  $ run hello.py --help
+  hello.py [options]
+  
+  Hello world continues the long established tradition
+  of delivering simple, but working programs in all
+  kinds of programming languages.
+  
+  options:
+  
+   -n --name  your name (default: world)
+   -h --help  show help
+
 
 That's all for today; see you next time!
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/selfhelp.py	Thu Feb 24 22:58:28 2011 -0800
@@ -0,0 +1,12 @@
+from opster import command
+
+@command()
+def selfhelp(assist=('', False, 'show help')):
+    '''Displays ability to show help'''
+    if assist:
+        selfhelp.help()
+    else:
+        print 'no help for you!'
+
+if __name__ == '__main__':
+    selfhelp()
--- a/tests/test_opts.py	Fri Dec 03 11:53:57 2010 -0800
+++ b/tests/test_opts.py	Thu Feb 24 22:58:28 2011 -0800
@@ -7,6 +7,7 @@
             port=('p', 8000, 'port to listen on'),
             daemonize=('d', False, 'daemonize process'),
             pid_file=('', '', 'name of file to write process ID to'),
+            definitions=('D', {}, 'just some definitions'),
             test=('t', lambda x: x or 'test', 'testing help for a function')):
     '''Command with option declaration as keyword arguments
     '''