--- /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
'''