diff options
author | Michele Simionato <michele.simionato@gmail.com> | 2010-06-01 13:22:00 +0200 |
---|---|---|
committer | Michele Simionato <michele.simionato@gmail.com> | 2010-06-01 13:22:00 +0200 |
commit | 7ee9559aa9313824ea327111b7287b716172c286 (patch) | |
tree | def050dfc4dfc55848be93dcd5725b50a4fb85b3 /plac | |
parent | 97dcd8ebb064319da3060d9795d4943b88870cd3 (diff) | |
download | micheles-7ee9559aa9313824ea327111b7287b716172c286.tar.gz |
Removed the clap project remnants
Diffstat (limited to 'plac')
-rw-r--r-- | plac/README.txt | 52 | ||||
-rw-r--r-- | plac/plac.py | 156 | ||||
-rw-r--r-- | plac/setup.py | 29 | ||||
-rw-r--r-- | plac/test_plac.py | 88 |
4 files changed, 325 insertions, 0 deletions
diff --git a/plac/README.txt b/plac/README.txt new file mode 100644 index 0000000..50ac30c --- /dev/null +++ b/plac/README.txt @@ -0,0 +1,52 @@ +plac, the easiest command line arguments parser in the world +============================================================ + +:Author: Michele Simionato +:E-mail: michele.simionato@gmail.com +:Requires: Python 2.3+ +:Download page: http://pypi.python.org/pypi/plac +:Installation: ``easy_install plac`` +:License: BSD license + +Installation +------------- + +If you are lazy, just perform + +$ easy_install plac + +which will install just the module on your system. Notice that +Python 3 requires the easy_install version of the distribute_ project. + +If you prefer to install the full distribution from source, including +the documentation, download the tarball_, unpack it and run + +$ python setup.py install + +in the main directory, possibly as superuser. + +.. _tarball: http://pypi.python.org/pypi/plac +.. _distribute: http://packages.python.org/distribute/ + +Testing +-------- + +Run + +$ python test_plac.py + +or + +$ nosetests test_plac + +or + +$ py.test test_plac + +Documentation +-------------- + +You can choose between the `HTML version`_ and the `PDF version`_ . + +.. _HTML version: http://micheles.googlecode.com/hg/plac/doc/plac.html +.. _PDF version: http://micheles.googlecode.com/hg/plac/doc/plac.pdf diff --git a/plac/plac.py b/plac/plac.py new file mode 100644 index 0000000..fdde35f --- /dev/null +++ b/plac/plac.py @@ -0,0 +1,156 @@ +########################## LICENCE ############################### +## +## Copyright (c) 2010, Michele Simionato +## All rights reserved. +## +## Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## Redistributions in bytecode form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. + +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +## OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +## TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +## USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +## DAMAGE. + +""" +plac, the easiest Command Line Arguments Parser in the world. +See plac/doc.html for the documentation. +""" +# this module should be kept Python 2.3 compatible + +__version__ = '0.2.0' + +import re, sys, inspect, argparse + +if sys.version >= '3': + from inspect import getfullargspec +else: + class getfullargspec(object): + "A quick and dirty replacement for getfullargspec for Python 2.X" + def __init__(self, f): + self.args, self.varargs, self.keywords, self.defaults = \ + inspect.getargspec(f) + self.annotations = getattr(f, '__annotations__', {}) + +def annotations(**ann): + """ + Returns a decorator annotating a function with the given annotations. + This is a trick to support function annotations in Python 2.X. + """ + def annotate(f): + fas = getfullargspec(f) + args = fas.args + if fas.varargs: + args.append(fas.varargs) + for argname in ann: + if argname not in args: + raise NameError( + 'Annotating non-existing argument: %s' % argname) + f.__annotations__ = ann + return f + return annotate + +def is_annotation(obj): + """ + An object is an annotation object if it has the attributes + help, kind, abbrev, type, choices, metavar. + """ + return (hasattr(obj, 'help') and hasattr(obj, 'kind') and + hasattr(obj, 'abbrev') and hasattr(obj, 'type') + and hasattr(obj, 'choices') and hasattr(obj, 'metavar')) + +class Annotation(object): + def __init__(self, help="", kind="positional", abbrev=None, type=str, + choices=None, metavar=None): + if kind == "positional": + assert abbrev is None, abbrev + else: + assert isinstance(abbrev, str) and len(abbrev) == 1, abbrev + self.help = help + self.kind = kind + self.abbrev = abbrev + self.type = type + self.choices = choices + self.metavar = metavar + + def from_(cls, obj): + "Helper to convert an object into an annotation, if needed" + if is_annotation(obj): + return obj # do nothing + elif hasattr(obj, '__iter__') and not isinstance(obj, str): + return cls(*obj) + return cls(obj) + from_ = classmethod(from_) + +NONE = object() # sentinel use to signal the absence of a default + +valid_attrs = getfullargspec(argparse.ArgumentParser.__init__).args[1:] + +def parser_from(func): + """ + Extract the arguments from the attributes of the passed function and + return an ArgumentParser instance. + """ + short_prefix = getattr(func, 'short_prefix', '-') + long_prefix = getattr(func, 'long_prefix', '--') + attrs = {'description': func.__doc__} + for n, v in vars(func).items(): + if n in valid_attrs: + attrs[n] = v + p = argparse.ArgumentParser(**attrs) + f = p.argspec = getfullargspec(func) + defaults = f.defaults or () + n_args = len(f.args) + n_defaults = len(defaults) + alldefaults = (NONE,) * (n_args - n_defaults) + defaults + for name, default in zip(f.args, alldefaults): + a = Annotation.from_(f.annotations.get(name, ())) + if default is NONE: + dflt, metavar = None, a.metavar + else: + dflt, metavar = default, a.metavar or str(default) + if a.kind in ('option', 'flag'): + short = short_prefix + a.abbrev + long = long_prefix + name + elif default is NONE: # required argument + p.add_argument(name, help=a.help, type=a.type, choices=a.choices, + metavar=metavar) + else: # default argument + p.add_argument(name, nargs='?', help=a.help, default=dflt, + type=a.type, choices=a.choices, metavar=metavar) + if a.kind == 'option': + p.add_argument(short, long, help=a.help, default=dflt, + type=a.type, choices=a.choices, metavar=metavar) + elif a.kind == 'flag': + if default is not NONE: + raise TypeError('Flag %r does not want a default' % name) + p.add_argument(short, long, action='store_true', help=a.help) + if f.varargs: + a = Annotation.from_(f.annotations.get(f.varargs, ())) + p.add_argument(f.varargs, nargs='*', help=a.help, default=[], + type=a.type, metavar=a.metavar) + return p + +def call(func, arglist=sys.argv[1:]): + """ + Parse the given arglist by using an argparser inferred from the + annotations of the given function (the main function of the script) + and call that function with the parsed arguments. The user can + provide a custom parse_annotation hook or replace the default one. + """ + p = parser_from(func) + argdict = vars(p.parse_args(arglist)) + args = [argdict[a] for a in p.argspec.args] + varargs = argdict.get(p.argspec.varargs, []) + func(*(args + varargs)) diff --git a/plac/setup.py b/plac/setup.py new file mode 100644 index 0000000..ca34c5b --- /dev/null +++ b/plac/setup.py @@ -0,0 +1,29 @@ +try: + from setuptools import setup +except ImportError: + from distutils.core import setup +import os.path +import plac + +if __name__ == '__main__': + setup(name=plac.__name__, + version=plac.__version__, + description='The easiest command line arguments parser in the world', + long_description=open('README.txt').read(), + author='Michele Simionato', + author_email='michele.simionato@gmail.com', + url='http://pypi.python.org/pypi/plac', + license="BSD License", + py_modules = ['plac'], + install_requires=['argparse>=1.1'], + keywords="command line arguments parser", + platforms=["All"], + classifiers=['Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries', + 'Topic :: Utilities'], + zip_safe=False) diff --git a/plac/test_plac.py b/plac/test_plac.py new file mode 100644 index 0000000..c961759 --- /dev/null +++ b/plac/test_plac.py @@ -0,0 +1,88 @@ +""" +The tests are runnable with nose, with py.test, or even as standalone script +""" + +import sys +import plac + +def expect(errclass, func, *args, **kw): + try: + func(*args, **kw) + except errclass: + pass + else: + raise RuntimeError('%s expected, got none!', errclass.__name__) + +def parser_from(f, **kw): + f.__annotations__ = kw + return plac.parser_from(f) + +p1 = parser_from(lambda delete, *args: None, + delete=('delete a file', 'option', 'd')) + +def test_p1(): + arg = p1.parse_args(['-d', 'foo', 'arg1', 'arg2']) + assert arg.delete == 'foo' + assert arg.args == ['arg1', 'arg2'] + + arg = p1.parse_args([]) + assert arg.delete is None, arg.delete + assert arg.args == [], arg.args + +p2 = parser_from(lambda arg1, delete, *args: None, + delete=('delete a file', 'option', 'd')) + +def test_p2(): + arg = p2.parse_args(['-d', 'foo', 'arg1', 'arg2']) + assert arg.delete == 'foo', arg.delete + assert arg.arg1 == 'arg1', arg.arg1 + assert arg.args == ['arg2'], arg.args + + arg = p2.parse_args(['arg1']) + assert arg.delete is None, arg.delete + assert arg.args == [], arg.args + assert arg, arg + + expect(SystemExit, p2.parse_args, []) + + +p3 = parser_from(lambda arg1, delete: None, + delete=('delete a file', 'option', 'd')) + +def test_p3(): + arg = p3.parse_args(['arg1']) + assert arg.delete is None, arg.delete + assert arg.arg1 == 'arg1', arg.args + + expect(SystemExit, p3.parse_args, ['arg1', 'arg2']) + expect(SystemExit, p3.parse_args, []) + +p4 = parser_from(lambda delete, delete_all, color="black": None, + delete=('delete a file', 'option', 'd'), + delete_all=('delete all files', 'flag', 'a'), + color=('color', 'option', 'c')) + +def test_p4(): + arg = p4.parse_args(['-a']) + assert arg.delete_all is True, arg.delete_all + + arg = p4.parse_args([]) + + arg = p4.parse_args(['--color=black']) + assert arg.color == 'black' + + arg = p4.parse_args(['--color=red']) + assert arg.color == 'red' + +def test_flag_with_default(): + expect(TypeError, parser_from, lambda yes_or_no='no': None, + yes_or_no=('A yes/no flag', 'flag', 'f')) + +if __name__ == '__main__': + n = 0 + for name, test in sorted(globals().items()): + if name.startswith('test_'): + print('Running ' + name) + test() + n +=1 + print('Executed %d tests OK' % n) |