From 19c260e63ffe20b5196c8401ed1cb2d7ccf71f7f Mon Sep 17 00:00:00 2001 From: Michele Simionato Date: Fri, 11 Jun 2010 14:18:43 +0200 Subject: Initial commit for release 0.5.0 --- plac/CHANGES.txt | 9 +- plac/Makefile | 4 + plac/README.txt | 9 +- plac/doc/dbcli.help | 12 +- plac/doc/dbcli.py | 11 +- plac/doc/example10.py | 4 +- plac/doc/example12.help | 10 +- plac/doc/example12.py | 10 +- plac/doc/example13.help | 10 + plac/doc/example13.py | 27 + plac/doc/example14.help | 8 + plac/doc/example14.py | 15 + plac/doc/example3.help | 9 + plac/doc/example6.help | 8 + plac/doc/example6.py | 14 +- plac/doc/example7_.help | 10 + plac/doc/example7_.py | 11 + plac/doc/example8.help | 2 +- plac/doc/example8_.help | 9 + plac/doc/example8_.py | 2 +- plac/doc/plac.html | 573 ++++---- plac/doc/plac.pdf | 3418 ++++++++++++++++++++++------------------------- plac/doc/plac.txt | 369 +++-- plac/doc/plac_adv.html | 904 +++++++++++++ plac/doc/plac_adv.pdf | 1543 +++++++++++++++++++++ plac/doc/plac_adv.txt | 279 ++++ plac/doc/vcs.help | 10 + plac/doc/vcs.py | 23 + plac/plac.py | 224 +--- plac/plac_core.py | 218 +++ plac/plac_ext.py | 156 +++ plac/setup.py | 13 +- plac/test_plac.py | 29 +- 33 files changed, 5349 insertions(+), 2604 deletions(-) create mode 100644 plac/doc/example13.help create mode 100644 plac/doc/example13.py create mode 100644 plac/doc/example14.help create mode 100644 plac/doc/example14.py create mode 100644 plac/doc/example3.help create mode 100644 plac/doc/example6.help create mode 100644 plac/doc/example7_.help create mode 100644 plac/doc/example7_.py create mode 100644 plac/doc/example8_.help create mode 100644 plac/doc/plac_adv.html create mode 100644 plac/doc/plac_adv.pdf create mode 100644 plac/doc/plac_adv.txt create mode 100644 plac/doc/vcs.help create mode 100644 plac/doc/vcs.py create mode 100644 plac/plac_core.py create mode 100644 plac/plac_ext.py diff --git a/plac/CHANGES.txt b/plac/CHANGES.txt index 2fb6f07..f511500 100644 --- a/plac/CHANGES.txt +++ b/plac/CHANGES.txt @@ -1,7 +1,14 @@ HISTORY ---------- -0.4.1. Changed the default formatting class and fixed a bug in the +0.5.0 Introduced smart options and removed the default formatter class. + Made the split plac_code and plac_ext and added an Interpreter class + (2010-06-XX). +0.4.3 Fixed the installation procedure to automatically download argparse + if needed (2010-06-11) +0.4.2 Added missing .help files, made the tests generative and added a + note about Clap in the documentation (2010-06-04) +0.4.1 Changed the default formatter class and fixed a bug in the display of the default arguments. Added more stringent tests. (2010-06-03) 0.4.0 abbrev is now optional. Added a note about CLIArgs and opterate. Added keyword arguments recognition. ``plac.call`` now returns the diff --git a/plac/Makefile b/plac/Makefile index 22302fc..f83245c 100644 --- a/plac/Makefile +++ b/plac/Makefile @@ -1,4 +1,8 @@ +default: + make doc/plac.pdf; make doc/plac_adv.pdf doc/plac.pdf: doc/plac.txt cd doc; rst2pdf --footer=###Page### plac.txt; rst2html --stylesheet=$(HOME)/gcode/df.css plac.txt plac.html +doc/plac_adv.pdf: doc/plac_adv.txt + cd doc; rst2pdf --footer=###Page### plac_adv.txt; rst2html --stylesheet=$(HOME)/gcode/df.css plac_adv.txt plac_adv.html upload: python setup.py register sdist upload diff --git a/plac/README.txt b/plac/README.txt index 6c5d887..4cbcab6 100644 --- a/plac/README.txt +++ b/plac/README.txt @@ -5,7 +5,7 @@ plac, the easiest command line arguments parser in the world :E-mail: michele.simionato@gmail.com :Requires: Python 2.3+ :Download page: http://pypi.python.org/pypi/plac -:Installation: ``easy_install plac`` +:Installation: ``easy_install -U plac`` :License: BSD license Installation @@ -15,10 +15,11 @@ If you are lazy, just perform :: - $ easy_install plac + $ easy_install -U plac -which will install just the module on your system. Notice that -Python 3 requires the easy_install version of the distribute_ project. +which will install the module on your system (and possibly argparse +too, if it is not already installed). 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 diff --git a/plac/doc/dbcli.help b/plac/doc/dbcli.help index c606181..8c0c18a 100644 --- a/plac/doc/dbcli.help +++ b/plac/doc/dbcli.help @@ -3,11 +3,11 @@ usage: dbcli.py [-h] [-H] [-c SQL] [-d |] db [scripts [scripts ...]] A script to run queries and SQL scripts on a database positional arguments: - db Connection string - scripts SQL scripts + db Connection string + scripts SQL scripts optional arguments: - -h, --help show this help message and exit - -H, --header Header - -c, --sqlcmd SQL SQL command - -d, --delimiter | Column separator + -h, --help show this help message and exit + -H, --header Header + -c SQL, --sqlcmd SQL SQL command + -d |, --delimiter | Column separator diff --git a/plac/doc/dbcli.py b/plac/doc/dbcli.py index 7f0cec6..d5db392 100644 --- a/plac/doc/dbcli.py +++ b/plac/doc/dbcli.py @@ -11,16 +11,19 @@ from sqlalchemy.ext.sqlsoup import SqlSoup ) def main(db, header, sqlcmd, delimiter="|", *scripts): "A script to run queries and SQL scripts on a database" - print('Working on %s' % db.bind.url) + yield 'Working on %s' % db.bind.url + if sqlcmd: result = db.bind.execute(sqlcmd) if header: # print the header - print(delimiter.join(result.keys())) + yield delimiter.join(result.keys()) for row in result: # print the rows - print(delimiter.join(map(str, row))) + yield delimiter.join(map(str, row)) for script in scripts: db.bind.execute(file(script).read()) + yield 'executed %s' % script if __name__ == '__main__': - plac.call(main) + for output in plac.call(main): + print(output) diff --git a/plac/doc/example10.py b/plac/doc/example10.py index 5da3a22..ac64db9 100644 --- a/plac/doc/example10.py +++ b/plac/doc/example10.py @@ -10,7 +10,7 @@ def main(operator, *numbers): result = dict(add=0.0, mul=1.0)[operator] for n in numbers: result = op(result, n) - print(result) + return result if __name__ == '__main__': - plac.call(main) + print(plac.call(main)[0]) diff --git a/plac/doc/example12.help b/plac/doc/example12.help index cceb8be..fec1715 100644 --- a/plac/doc/example12.help +++ b/plac/doc/example12.help @@ -1,9 +1,9 @@ -usage: example12.py [-h] [-o OPT] [args [args ...]] [kw [kw ...]] +usage: example12.py [-h] [-opt OPT] [args [args ...]] [kw [kw ...]] positional arguments: - args default arguments - kw keyword arguments + args default arguments + kw keyword arguments optional arguments: - -h, --help show this help message and exit - -o, --opt OPT some option + -h, --help show this help message and exit + -opt OPT some option diff --git a/plac/doc/example12.py b/plac/doc/example12.py index 81a6c8d..56c65a9 100644 --- a/plac/doc/example12.py +++ b/plac/doc/example12.py @@ -6,7 +6,13 @@ import plac args='default arguments', kw='keyword arguments') def main(opt, *args, **kw): - print(opt, args, kw) + if opt: + yield 'opt=%s' % opt + if args: + yield 'args=%s' % str(args) + if kw: + yield 'kw=%s' % kw if __name__ == '__main__': - plac.call(main) + for output in plac.call(main): + print(output) diff --git a/plac/doc/example13.help b/plac/doc/example13.help new file mode 100644 index 0000000..91216a3 --- /dev/null +++ b/plac/doc/example13.help @@ -0,0 +1,10 @@ +usage: example13.py [-h] {status,commit,checkout,help} ... + +A Fake Version Control System + +optional arguments: + -h, --help show this help message and exit + +subcommands: + {status,commit,checkout,help} + -h to get additional help diff --git a/plac/doc/example13.py b/plac/doc/example13.py new file mode 100644 index 0000000..3c91f07 --- /dev/null +++ b/plac/doc/example13.py @@ -0,0 +1,27 @@ +import plac + +class FVCS(object): + "A Fake Version Control System" + commands = 'checkout', 'commit', 'status', 'help' + + @plac.annotations( + name=('a recognized command', 'positional', None, str, commands)) + def help(self, name): + self.p.subp[name].print_help() + + @plac.annotations( + url=('url of the source code', 'positional')) + def checkout(self, url): + print('checkout', url) + + def commit(self): + print('commit') + + @plac.annotations(quiet=('summary information', 'flag')) + def status(self, quiet): + print('status', quiet) + +main = FVCS() + +if __name__ == '__main__': + plac.call(main) diff --git a/plac/doc/example14.help b/plac/doc/example14.help new file mode 100644 index 0000000..50c600b --- /dev/null +++ b/plac/doc/example14.help @@ -0,0 +1,8 @@ +usage: example14.py [-h] {status,commit,checkout,help} ... + +optional arguments: + -h, --help show this help message and exit + +subcommands: + {status,commit,checkout,help} + -h to get additional help diff --git a/plac/doc/example14.py b/plac/doc/example14.py new file mode 100644 index 0000000..18a2931 --- /dev/null +++ b/plac/doc/example14.py @@ -0,0 +1,15 @@ +import plac +from example13 import FVCS + +class VCS_with_help(FVCS): + commands = FVCS.commands + ('help',) + + @plac.annotations( + name=('a recognized command', 'positional', None, str, commands)) + def help(self, name): + self.p.subp[name].print_help() + +main = VCS_with_help() + +if __name__ == '__main__': + plac.call(main) diff --git a/plac/doc/example3.help b/plac/doc/example3.help new file mode 100644 index 0000000..dd4d96a --- /dev/null +++ b/plac/doc/example3.help @@ -0,0 +1,9 @@ +usage: example3.py [-h] dsn + +Do something with the database + +positional arguments: + dsn + +optional arguments: + -h, --help show this help message and exit diff --git a/plac/doc/example6.help b/plac/doc/example6.help new file mode 100644 index 0000000..c950e78 --- /dev/null +++ b/plac/doc/example6.help @@ -0,0 +1,8 @@ +usage: example6.py [-h] [-command COMMAND] dsn + +positional arguments: + dsn + +optional arguments: + -h, --help show this help message and exit + -command COMMAND SQL query diff --git a/plac/doc/example6.py b/plac/doc/example6.py index 5586c03..045839b 100644 --- a/plac/doc/example6.py +++ b/plac/doc/example6.py @@ -1,14 +1,6 @@ # example6.py -from datetime import datetime - -def main(dsn, *scripts): - "Run the given scripts on the database" - for script in scripts: - print('executing %s' % script) - # ... +def main(dsn, command: ("SQL query", 'option')): + print('executing %r on %s' % (command, dsn)) if __name__ == '__main__': - import sys - if len(sys.argv) < 2: - sys.exit('usage: python %s dsn script.sql ...' % sys.argv[0]) - main(sys.argv[1:]) + import plac; plac.call(main) diff --git a/plac/doc/example7_.help b/plac/doc/example7_.help new file mode 100644 index 0000000..6f0f4b8 --- /dev/null +++ b/plac/doc/example7_.help @@ -0,0 +1,10 @@ +usage: example7_.py [-h] dsn [scripts [scripts ...]] + +Run the given scripts on the database + +positional arguments: + dsn Database dsn + scripts SQL scripts + +optional arguments: + -h, --help show this help message and exit diff --git a/plac/doc/example7_.py b/plac/doc/example7_.py new file mode 100644 index 0000000..550a524 --- /dev/null +++ b/plac/doc/example7_.py @@ -0,0 +1,11 @@ +# example7_.py +from datetime import datetime + +def main(dsn: "Database dsn", *scripts: "SQL scripts"): + "Run the given scripts on the database" + for script in scripts: + print('executing %s' % script) + # ... + +if __name__ == '__main__': + import plac; plac.call(main) diff --git a/plac/doc/example8.help b/plac/doc/example8.help index c7fa3a5..a566258 100644 --- a/plac/doc/example8.help +++ b/plac/doc/example8.help @@ -5,5 +5,5 @@ positional arguments: optional arguments: -h, --help show this help message and exit - -c, --command COMMAND + -c COMMAND, --command COMMAND SQL query diff --git a/plac/doc/example8_.help b/plac/doc/example8_.help new file mode 100644 index 0000000..872b05a --- /dev/null +++ b/plac/doc/example8_.help @@ -0,0 +1,9 @@ +usage: example8_.py [-h] [-command select * from table] dsn + +positional arguments: + dsn + +optional arguments: + -h, --help show this help message and exit + -command select * from table + SQL query diff --git a/plac/doc/example8_.py b/plac/doc/example8_.py index 20f6df0..cdea364 100644 --- a/plac/doc/example8_.py +++ b/plac/doc/example8_.py @@ -1,5 +1,5 @@ # example8_.py -def main(dsn, command: ("SQL query", 'option', 'c')='select * from table'): +def main(dsn, command: ("SQL query", 'option')='select * from table'): print('executing %r on %s' % (command, dsn)) if __name__ == '__main__': diff --git a/plac/doc/plac.html b/plac/doc/plac.html index a973666..6339cc3 100644 --- a/plac/doc/plac.html +++ b/plac/doc/plac.html @@ -3,7 +3,7 @@ - + Plac: Parsing the Command Line the Easy Way + + +
+

Testing and scripting your applications with plac

+ +
+

Introduction

+

plac has been designed to be simple to use for simple stuff, but in +truth it is a quite advanced tool with a field of applicability +which far outreaches the specific domain of command-line arguments parsers. +In reality plac is a generic tool to write domain specific +languages (DSL). +This document explains how you can use plac to test your application, and +how you can use it to provide a scripting interface to your application. +Notice that your application does not need to be a command-line +application: you can use plac whenever you have an API with strings +in input and strings in output.

+
+
+

Testing applications with plac

+

In the standard usage, plac.call is called only once on the main +function; however in the tests it quite natural to invoke plac.call +multiple times on the same function with different arguments. +For instance, suppose you want to store the configuration of +your application into a Python shelve; then, you may want to write +a command-line tool to edit your configuration, i.e. a shelve +interface. A possible implementation could be the following:

+
+import shelve
+import plac
+
+@plac.annotations(
+    help=('show help', 'flag'),
+    all=('show all parameters in the shelve', 'flag'),
+    clear=('clear the shelve', 'flag'),
+    delete=('delete an element', 'option'),
+    filename=('filename of the shelve', 'option'),
+    params='names of the parameters in the shelve',
+    setters='setters param=value')
+def ishelve(help, all, clear, delete, filename='conf.shelve',
+            *params, **setters):
+    sh = shelve.open(filename)
+    try:
+        if help:
+            yield 'Special commands:'
+            yield 'help, all, clear, delete'
+        elif all:
+            for param, name in sh.items():
+                yield '%s=%s' % (param, name)
+        elif clear:
+            sh.clear()
+            yield 'cleared the shelve'
+        elif delete:
+            try:
+                del sh[delete]
+            except KeyError:
+                yield '%s: not found' % delete
+            else:
+                yield 'deleted %s' % delete
+        for param in params:
+            try:
+                yield sh[param]
+            except KeyError:
+                yield '%s: not found' % param           
+        for param, value in setters.items():
+            sh[param] = value
+            yield 'setting %s=%s' % (param, value)
+    finally:
+        sh.close()
+
+ishelve.add_help = False # there is a custom help
+
+if __name__ == '__main__':
+    for output in plac.call(ishelve):
+        print(output)
+
+
+

You can write the tests for such implementation as follows:

+
+import plac
+from ishelve import ishelve
+
+def test():
+    assert plac.call(ishelve, []) == []
+    assert plac.call(ishelve, ['--clear']) == ['cleared the shelve']
+    assert plac.call(ishelve, ['a=1']) == ['setting a=1']
+    assert plac.call(ishelve, ['a']) == ['1']
+    assert plac.call(ishelve, ['--delete=a']) == ['deleted a']
+    assert plac.call(ishelve, ['a']) == ['a: not found']
+
+
+
+

There is a small optimization here: once plac.call(func) +has been called, a .p attribute is attached to func, containing +the parser associated to the function annotations. The second time +plac.call(func) is invoked, the parser is re-used.

+
+
+

Writing command-line interpreters with plac

+

Apart from testing, there is another typical use case where plac.call +is invoked multiple times, in the implementation of command interpreters. +For instance, you could define an interative interpreter on top +of ishelve as follows:

+
+import plac
+from ishelve import ishelve
+
+ishelve.prefix_chars = '.'
+ishelve.add_help = False
+
+@plac.annotations(
+    interactive=('start interactive interface', 'flag'))
+def main(interactive, *args):
+    if interactive:
+        import shlex
+        while True:
+            try:
+                line = raw_input('i> ')
+            except EOFError:
+                break
+            cmd = shlex.split(line)
+            for out in plac.call(ishelve, cmd):
+                print(out)
+    else:
+        plac.call(ishelve, args)
+
+if __name__ == '__main__':
+    plac.call(main)
+
+
+

Here is an usage session, usinng rlwrap to enable readline features:

+
+$ rlwrap python shelve_interpreter.py -i
+
+i> ..clear
+cleared the shelve
+i> a=1
+setting a=1
+i> a
+1
+i> b=2
+setting b=2
+i> a b
+1
+2
+i> ..delete a
+deleted a
+i> a
+a: not found
+i> ..all
+b=2
+i> [CTRL-D]
+
+

As you see, it is possibly to write command interpreters directly on top of +plac.call and it is not particularly difficult. However, the devil +is in the details (I mean error management) and my recommendation, if +you want to implement an interpreter of commands, is to use the +class plac.Interpreter which is especially suited for this +task. plac.Interpreter is available only if you are using a recent +version of Python (>= 2.5), because it is a context manager object +to be used with the with statement. The only important method +of plac.Interpreter is the .send method, which takes a +string in input and returns a string in output. Internally the input string +is splitted with shlex.split and passed to plac.call, +with some trick to manage exceptions correctly. Moreover long options +are managed with a single prefix character.

+
+"""Call this script with rlwrap and you will be happy"""
+from __future__ import with_statement
+from plac_shell import Interpreter
+from shelve_interface import interpreter
+
+if __name__ == '__main__':
+    with Interpreter(interpreter) as i:
+        while True:
+            try:
+                line = raw_input('i> ')
+            except EOFError:
+                break
+            print(i.send(line))
+
+
+
+
+

Multi-parsers

+

As we saw, plac is able to infer an arguments parser from the +signature of a function. In addition, plac is also able to infer a +multi-parser from a container of commands, by inferring the subparsers +from the commands. That is useful if you want to implement +subcommands (a familiar example of a command-line application +featuring subcommands is subversion).

+

A container of commands is any object with a .commands attribute +listing a set of functions or methods which are valid commands. In +particular, a Python module is a perfect container of commands. As an +example, consider the following module implementing a fake Version +Control System:

+
+"A Fake Version Control System"
+
+import plac
+
+commands = 'checkout', 'commit', 'status'
+
+@plac.annotations(
+    url=('url of the source code', 'positional'))
+def checkout(url):
+    return ('checkout ', url)
+
+@plac.annotations(
+    message=('commit message', 'option'))
+def commit(message):
+    return ('commit ', message)
+
+@plac.annotations(quiet=('summary information', 'flag'))
+def status(quiet):
+    return ('status ', quiet)
+
+if __name__ == '__main__':
+    import __main__
+    print(plac.call(__main__))
+
+
+

Here is the usage message:

+
+usage: vcs.py [-h] {status,commit,checkout} ...
+
+A Fake Version Control System
+
+optional arguments:
+  -h, --help            show this help message and exit
+
+subcommands:
+  {status,commit,checkout}
+                        -h to get additional help
+
+
+

If the commands are completely independent, a module is a good fit for +a method container. In other situations, it is best to use a custom +class. For instance, suppose you want to store the configuration of +your application into a Python shelve; then, you may want to write +a command-line tool to edit your configuration, i.e. a shelve +interface:

+
+import shelve
+import plac
+
+# error checking is missing: this is left to the reader
+class ShelveInterface(object):
+    "A minimal interface over a shelve object"
+    commands = 'set', 'show', 'show_all', 'delete'
+    def __init__(self, fname):
+        self.fname = fname
+        self.sh = shelve.open(fname)
+    def set(self, name, value):
+        "set name value"
+        yield 'setting %s=%s' % (name, value)
+        self.sh[name] = value
+    def show(self, *names):
+        "show given parameters"
+        for name in names:
+            yield '%s = %s\n' % (name, self.sh[name])
+    def show_all(self):
+        "show all parameters"
+        for name in self.sh:
+            yield '%s = %s\n' % (name, self.sh[name])
+    def delete(self, name=None):
+        "delete given parameter (or everything)"
+        if name is None:
+            yield 'deleting everything'
+            self.sh.clear()
+        else:
+            yield 'deleting %s' % name
+            del self.sh[name]
+
+if __name__ == '__main__':
+    interface = ShelveInterface('conf.shelve')
+    try:
+        for output in plac.call(interface):
+            print(output)
+    finally:
+        interface.sh.close()
+
+
+

Here is a session of usage on an Unix-like operating system:

+
+$ alias conf="python shelve_interface.py"
+$ conf set a pippo
+setting a=pippo
+$ conf set b lippo
+setting b=lippo
+$ conf show_all
+b = lippo
+a = pippo
+$ conf show a b
+a = pippo
+b = lippo
+$ conf delete a
+deleting a
+$ conf show_all
+b = lippo
+
+

Technically a multi-parser is a parser object with an attribute .subp +which is a dictionary of subparsers; each of the methods listed in +the attribute .commands corresponds to a subparser inferred from +the method signature. The original object gets a .p attribute +containing the main parser which is associated to an internal function +which dispatches on the right method depending on the method name.

+

Here is the usage message:

+
+import plac
+
+class FVCS(object):
+    "A Fake Version Control System"
+    commands = 'checkout', 'commit', 'status', 'help'
+
+    @plac.annotations(
+        name=('a recognized command', 'positional', None, str, commands))
+    def help(self, name):
+        self.p.subp[name].print_help()
+
+    @plac.annotations(
+        url=('url of the source code', 'positional'))
+    def checkout(self, url):
+        print('checkout', url)
+
+    def commit(self):
+        print('commit')
+
+    @plac.annotations(quiet=('summary information', 'flag'))
+    def status(self, quiet):
+        print('status', quiet)
+
+main = FVCS()
+
+if __name__ == '__main__':
+    plac.call(main)
+
+
+
+usage: example13.py [-h] {status,commit,checkout,help} ...
+
+A Fake Version Control System
+
+optional arguments:
+  -h, --help            show this help message and exit
+
+subcommands:
+  {status,commit,checkout,help}
+                        -h to get additional help
+
+
+
+
+

Advanced usage

+

plac relies on a argparse for all of the heavy lifting work and it is +possible to leverage on argparse features directly or indirectly.

+

For instance, you can make invisible an argument in the usage message +simply by using '==SUPPRESS==' as help string (or +argparse.SUPPRESS). Similarly, you can use argparse.FileType +directly.

+

It is also possible to pass options to the underlying +argparse.ArgumentParser object (currently it accepts the default +arguments description, epilog, prog, usage, +add_help, argument_default, parents, prefix_chars, +fromfile_prefix_chars, conflict_handler, formatter_class). +It is enough to set such attributes on the main function. For +instance

+
+def main(...):
+    pass
+
+main.add_help = False
+
+

disable the recognition of the help flag -h, --help. This is not +particularly elegant, but I assume the typical user of plac will be +happy with the defaults and would not want to change them; still it is +possible if she wants to. For instance, by setting the description +attribute, it is possible to add a comment to the usage message (by +default the docstring of the main function is used as +description). It is also possible to change the option prefix; for +instance if your script must run under Windows and you want to use "/" +as option prefix you can add the line:

+
+main.prefix_chars='/-'
+
+

prefix_chars is an argparse feature. The first prefix char (/) is used +as the default in the construction of both short and long options; +the second prefix char (-) is kept to keep the -h/--help option +working: however you can disable it and reimplement it if you like. +For instance, here is how you could reimplement the help command +in the Fake VCS example:

+
+import plac
+from example13 import FVCS
+
+class VCS_with_help(FVCS):
+    commands = FVCS.commands + ('help',)
+
+    @plac.annotations(
+        name=('a recognized command', 'positional', None, str, commands))
+    def help(self, name):
+        self.p.subp[name].print_help()
+
+main = VCS_with_help()
+
+if __name__ == '__main__':
+    plac.call(main)
+
+
+

Internally plac.call uses +plac.parser_from and adds the parser as an attribute .p. +This also happers for multiparsers and you can take advantage of +the .p attribute to invoke argparse.ArgumentParser methods.

+

Interested readers should read the documentation of argparse to +understand the meaning of the other options. If there is a set of +options that you use very often, you may consider writing a decorator +adding such options to the main function for you. For simplicity, +plac does not perform any magic of that kind.

+

It is possible to access directly the underlying ArgumentParser object, by +invoking the plac.parser_from utility function:

+
+>>> import plac
+>>> def main(arg):
+...     pass
+...
+>>> print plac.parser_from(main)
+ArgumentParser(prog='', usage=None, description=None, version=None,
+formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error',
+add_help=True)
+
+

I use plac.parser_from in the unit tests of the module, but regular +users should never need to use it, since the parser is also available +as an attribute of the main function.

+
+
+

Custom annotation objects

+

Internally plac uses an Annotation class to convert the tuples +in the function signature into annotation objects, i.e. objects with +six attributes help, kind, short, type, choices, metavar.

+

Advanced users can implement their own annotation objects. +For instance, here is an example of how you could implement annotations for +positional arguments:

+
+# annotations.py
+class Positional(object):
+    def __init__(self, help='', type=None, choices=None, metavar=None):
+        self.help = help
+        self.kind = 'positional'
+        self.abbrev = None
+        self.type = type
+        self.choices = choices
+        self.metavar = metavar
+
+
+

You can use such annotations objects as follows:

+
+# example11.py
+import plac
+from annotations import Positional
+
+@plac.annotations(
+    i=Positional("This is an int", int),
+    n=Positional("This is a float", float),
+    rest=Positional("Other arguments"))
+def main(i, n, *rest):
+    print(i, n, rest)
+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+

Here is the usage message you get:

+
+usage: example11.py [-h] i n [rest [rest ...]]
+
+positional arguments:
+  i           This is an int
+  n           This is a float
+  rest        Other arguments
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+

You can go on and define Option and Flag classes, if you like. +Using custom annotation objects you could do advanced things like extracting the +annotations from a configuration file or from a database, but I expect such +use cases to be quite rare: the default mechanism should work +pretty well for most users.

+
+
+ + diff --git a/plac/doc/plac_adv.pdf b/plac/doc/plac_adv.pdf new file mode 100644 index 0000000..d0b207f --- /dev/null +++ b/plac/doc/plac_adv.pdf @@ -0,0 +1,1543 @@ +%PDF-1.3 +%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com +% 'BasicFonts': class PDFDictionary +1 0 obj +% The standard fonts dictionary +<< /F1 2 0 R + /F2 3 0 R + /F3 7 0 R + /F4 13 0 R >> +endobj +% 'F1': class PDFType1Font +2 0 obj +% Font Helvetica +<< /BaseFont /Helvetica + /Encoding /WinAnsiEncoding + /Name /F1 + /Subtype /Type1 + /Type /Font >> +endobj +% 'F2': class PDFType1Font +3 0 obj +% Font Helvetica-Bold +<< /BaseFont /Helvetica-Bold + /Encoding /WinAnsiEncoding + /Name /F2 + /Subtype /Type1 + /Type /Font >> +endobj +% 'Annot.NUMBER1': class PDFDictionary +4 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 693.5936 + 84.3779 + 705.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER2': class PDFDictionary +5 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 93.0279 + 669.5936 + 115.0329 + 681.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER3': class PDFDictionary +6 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 633.5936 + 83.81291 + 645.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'F3': class PDFType1Font +7 0 obj +% Font Courier +<< /BaseFont /Courier + /Encoding /WinAnsiEncoding + /Name /F3 + /Subtype /Type1 + /Type /Font >> +endobj +% 'Page1': class PDFPage +8 0 obj +% Page dictionary +<< /Annots [ 4 0 R + 5 0 R + 6 0 R ] + /Contents 40 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 39 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page2': class PDFPage +9 0 obj +% Page dictionary +<< /Contents 41 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 39 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page3': class PDFPage +10 0 obj +% Page dictionary +<< /Contents 42 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 39 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER4': class PDFDictionary +11 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 117.8279 + 729.5936 + 139.5429 + 741.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER5': class PDFDictionary +12 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 503.6477 + 729.5936 + 525.3627 + 741.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'F4': class PDFType1Font +13 0 obj +% Font Helvetica-Oblique +<< /BaseFont /Helvetica-Oblique + /Encoding /WinAnsiEncoding + /Name /F4 + /Subtype /Type1 + /Type /Font >> +endobj +% 'Page4': class PDFPage +14 0 obj +% Page dictionary +<< /Annots [ 11 0 R + 12 0 R ] + /Contents 43 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 39 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page5': class PDFPage +15 0 obj +% Page dictionary +<< /Contents 44 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 39 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER6': class PDFDictionary +16 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 119.9936 + 83.9079 + 131.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER7': class PDFDictionary +17 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 133.1029 + 119.9936 + 175.4379 + 131.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER8': class PDFDictionary +18 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 454.1177 + 119.9936 + 496.4527 + 131.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page6': class PDFPage +19 0 obj +% Page dictionary +<< /Annots [ 16 0 R + 17 0 R + 18 0 R ] + /Contents 45 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 39 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER9': class PDFDictionary +20 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com/svn/tags/r11/doc/other-utilities.html?highlight=filetype#FileType) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 455.2227 + 744.5936 + 534.3667 + 756.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER10': class PDFDictionary +21 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 127.99 + 577.3936 + 149.3857 + 589.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER11': class PDFDictionary +22 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 166.3664 + 476.1936 + 209.1976 + 488.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER12': class PDFDictionary +23 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 307.9178 + 176.9936 + 351.5999 + 188.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER13': class PDFDictionary +24 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 317.2329 + 152.9936 + 338.3529 + 164.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER14': class PDFDictionary +25 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com/svn/tags/r11/doc/ArgumentParser.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 327.2261 + 134.9936 + 410.5152 + 146.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page7': class PDFPage +26 0 obj +% Page dictionary +<< /Annots [ 20 0 R + 21 0 R + 22 0 R + 23 0 R + 24 0 R + 25 0 R ] + /Contents 46 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 39 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER15': class PDFDictionary +27 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 106.6216 + 576.3936 + 128.3202 + 588.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page8': class PDFPage +28 0 obj +% Page dictionary +<< /Annots [ 27 0 R ] + /Contents 47 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 39 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page9': class PDFPage +29 0 obj +% Page dictionary +<< /Contents 48 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 39 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'R30': class PDFCatalog +30 0 obj +% Document Root +<< /Outlines 32 0 R + /PageLabels 49 0 R + /PageMode /UseNone + /Pages 39 0 R + /Type /Catalog >> +endobj +% 'R31': class PDFInfo +31 0 obj +<< /Author () + /CreationDate (D:20100611140831-01'00') + /Keywords () + /Producer (ReportLab http://www.reportlab.com) + /Subject (\(unspecified\)) + /Title (Testing and scripting your applications with plac) >> +endobj +% 'R32': class PDFOutlines +32 0 obj +<< /Count 6 + /First 33 0 R + /Last 38 0 R + /Type /Outlines >> +endobj +% 'Outline.0': class OutlineEntryObject +33 0 obj +<< /Dest [ 8 0 R + /XYZ + 62.69291 + 729.0236 + 0 ] + /Next 34 0 R + /Parent 32 0 R + /Title (Introduction) >> +endobj +% 'Outline.1': class OutlineEntryObject +34 0 obj +<< /Dest [ 8 0 R + /XYZ + 62.69291 + 618.0236 + 0 ] + /Next 35 0 R + /Parent 32 0 R + /Prev 33 0 R + /Title (Testing applications with plac) >> +endobj +% 'Outline.2': class OutlineEntryObject +35 0 obj +<< /Dest [ 9 0 R + /XYZ + 62.69291 + 390.6236 + 0 ] + /Next 36 0 R + /Parent 32 0 R + /Prev 34 0 R + /Title (Writing command-line interpreters with plac) >> +endobj +% 'Outline.3': class OutlineEntryObject +36 0 obj +<< /Dest [ 14 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Next 37 0 R + /Parent 32 0 R + /Prev 35 0 R + /Title (Multi-parsers) >> +endobj +% 'Outline.4': class OutlineEntryObject +37 0 obj +<< /Dest [ 19 0 R + /XYZ + 62.69291 + 155.4236 + 0 ] + /Next 38 0 R + /Parent 32 0 R + /Prev 36 0 R + /Title (Advanced usage) >> +endobj +% 'Outline.5': class OutlineEntryObject +38 0 obj +<< /Dest [ 28 0 R + /XYZ + 62.69291 + 611.8236 + 0 ] + /Parent 32 0 R + /Prev 37 0 R + /Title (Custom annotation objects) >> +endobj +% 'R39': class PDFPages +39 0 obj +% page tree +<< /Count 9 + /Kids [ 8 0 R + 9 0 R + 10 0 R + 14 0 R + 15 0 R + 19 0 R + 26 0 R + 28 0 R + 29 0 R ] + /Type /Pages >> +endobj +% 'R40': class PDFStream +40 0 obj +% page stream +<< /Length 3854 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 741.0236 cm +q +BT 1 0 0 1 0 9.64 Tm 2.664882 0 Td 24 TL /F2 20 Tf 0 0 0 rg (Testing and scripting your applications with plac) Tj T* -2.664882 0 Td ET +Q +Q +q +1 0 0 1 62.69291 708.0236 cm +q +BT 1 0 0 1 0 8.435 Tm 21 TL /F2 17.5 Tf 0 0 0 rg (Introduction) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 630.0236 cm +q +BT 1 0 0 1 0 64.82 Tm .564989 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (has been designed to be simple to use for simple stuff, but in truth it is a quite advanced tool with a) Tj T* 0 Tw 1.986905 Tw (field of applicability which far outreaches the specific domain of command-line arguments parsers. In) Tj T* 0 Tw .884985 Tw (reality ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is a generic tool to write domain specific languages \(DSL\). This document explains how you) Tj T* 0 Tw 1.766303 Tw (can use plac to test your application, and how you can use it to provide a scripting interface to your) Tj T* 0 Tw 1.384651 Tw (application. Notice that your application does not need to be a command-line application: you can use) Tj T* 0 Tw 0 0 .501961 rg (plac ) Tj 0 0 0 rg (whenever you have an API with strings in input and strings in output.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 597.0236 cm +q +BT 1 0 0 1 0 8.435 Tm 21 TL /F2 17.5 Tf 0 0 0 rg (Testing applications with plac) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 531.0236 cm +q +BT 1 0 0 1 0 52.82 Tm .352209 Tw 12 TL /F1 10 Tf 0 0 0 rg (In the standard usage, ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (is called only once on the main function; however in the tests it quite) Tj T* 0 Tw .297126 Tw (natural to invoke ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (multiple times on the same function with different arguments. For instance,) Tj T* 0 Tw .436457 Tw (suppose you want to store the configuration of your application into a Python shelve; then, you may want) Tj T* 0 Tw .683318 Tw (to write a command-line tool to edit your configuration, i.e. a shelve interface. A possible implementation) Tj T* 0 Tw (could be the following:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 89.82362 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 432 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 413.71 Tm /F3 10 Tf 12 TL (import shelve) Tj T* (import plac) Tj T* T* (@plac.annotations\() Tj T* ( help=\('show help', 'flag'\),) Tj T* ( all=\('show all parameters in the shelve', 'flag'\),) Tj T* ( clear=\('clear the shelve', 'flag'\),) Tj T* ( delete=\('delete an element', 'option'\),) Tj T* ( filename=\('filename of the shelve', 'option'\),) Tj T* ( params='names of the parameters in the shelve',) Tj T* ( setters='setters param=value'\)) Tj T* (def ishelve\(help, all, clear, delete, filename='conf.shelve',) Tj T* ( *params, **setters\):) Tj T* ( sh = shelve.open\(filename\)) Tj T* ( try:) Tj T* ( if help:) Tj T* ( yield 'Special commands:') Tj T* ( yield 'help, all, clear, delete') Tj T* ( elif all:) Tj T* ( for param, name in sh.items\(\):) Tj T* ( yield '%s=%s' % \(param, name\)) Tj T* ( elif clear:) Tj T* ( sh.clear\(\)) Tj T* ( yield 'cleared the shelve') Tj T* ( elif delete:) Tj T* ( try:) Tj T* ( del sh[delete]) Tj T* ( except KeyError:) Tj T* ( yield '%s: not found' % delete) Tj T* ( else:) Tj T* ( yield 'deleted %s' % delete) Tj T* ( for param in params:) Tj T* ( try:) Tj T* ( yield sh[param]) Tj T* ( except KeyError:) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 56.69291 56.69291 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 238.1649 0 Td (1) Tj T* -238.1649 0 Td ET +Q +Q + +endstream + +endobj +% 'R41': class PDFStream +41 0 obj +% page stream +<< /Length 3706 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 607.8236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 156 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 137.71 Tm /F3 10 Tf 12 TL ( yield '%s: not found' % param ) Tj T* ( for param, value in setters.items\(\):) Tj T* ( sh[param] = value) Tj T* ( yield 'setting %s=%s' % \(param, value\)) Tj T* ( finally:) Tj T* ( sh.close\(\)) Tj T* T* (ishelve.add_help = False # there is a custom help) Tj T* T* (if __name__ == '__main__':) Tj T* ( for output in plac.call\(ishelve\):) Tj T* ( print\(output\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 587.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (You can write the tests for such implementation as follows:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 446.6236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 132 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 113.71 Tm /F3 10 Tf 12 TL (import plac) Tj T* (from ishelve import ishelve) Tj T* T* (def test\(\):) Tj T* ( assert plac.call\(ishelve, []\) == []) Tj T* ( assert plac.call\(ishelve, ['--clear']\) == ['cleared the shelve']) Tj T* ( assert plac.call\(ishelve, ['a=1']\) == ['setting a=1']) Tj T* ( assert plac.call\(ishelve, ['a']\) == ['1']) Tj T* ( assert plac.call\(ishelve, ['--delete=a']\) == ['deleted a']) Tj T* ( assert plac.call\(ishelve, ['a']\) == ['a: not found']) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 402.6236 cm +q +BT 1 0 0 1 0 28.82 Tm .344651 Tw 12 TL /F1 10 Tf 0 0 0 rg (There is a small optimization here: once ) Tj /F3 10 Tf (plac.call\(func\) ) Tj /F1 10 Tf (has been called, a ) Tj /F3 10 Tf (.p ) Tj /F1 10 Tf (attribute is attached) Tj T* 0 Tw 7.140814 Tw (to ) Tj /F3 10 Tf (func) Tj /F1 10 Tf (, containing the parser associated to the function annotations. The second time) Tj T* 0 Tw /F3 10 Tf (plac.call\(func\) ) Tj /F1 10 Tf (is invoked, the parser is re-used.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 369.6236 cm +q +BT 1 0 0 1 0 8.435 Tm 21 TL /F2 17.5 Tf 0 0 0 rg (Writing command-line interpreters with plac) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 327.6236 cm +q +BT 1 0 0 1 0 28.82 Tm .677485 Tw 12 TL /F1 10 Tf 0 0 0 rg (Apart from testing, there is another typical use case where ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (is invoked multiple times, in the) Tj T* 0 Tw .334269 Tw (implementation of command interpreters. For instance, you could define an interative interpreter on top of) Tj T* 0 Tw /F3 10 Tf (ishelve ) Tj /F1 10 Tf (as follows:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 90.42362 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 228 re B* +Q +q +BT 1 0 0 1 0 209.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (import plac) Tj T* (from ishelve import ishelve) Tj T* T* (ishelve.prefix_chars = '.') Tj T* (ishelve.add_help = False) Tj T* T* (@plac.annotations\() Tj T* ( interactive=\('start interactive interface', 'flag'\)\)) Tj T* (def main\(interactive, *args\):) Tj T* ( if interactive:) Tj T* ( import shlex) Tj T* ( while True:) Tj T* ( try:) Tj T* ( line = raw_input\('i) Tj (>) Tj ( '\)) Tj T* ( except EOFError:) Tj T* ( break) Tj T* ( cmd = shlex.split\(line\)) Tj T* ( for out in plac.call\(ishelve, cmd\):) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 56.69291 56.69291 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 238.1649 0 Td (2) Tj T* -238.1649 0 Td ET +Q +Q + +endstream + +endobj +% 'R42': class PDFStream +42 0 obj +% page stream +<< /Length 3636 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 679.8236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 84 re B* +Q +q +BT 1 0 0 1 0 65.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ( print\(out\)) Tj T* ( else:) Tj T* ( plac.call\(ishelve, args\)) Tj T* T* (if __name__ == '__main__':) Tj T* ( plac.call\(main\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 659.8236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (Here is an usage session, usinng ) Tj /F3 10 Tf (rlwrap ) Tj /F1 10 Tf (to enable readline features:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 398.6236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 252 re B* +Q +q +BT 1 0 0 1 0 233.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ($ rlwrap python shelve_interpreter.py -i) Tj T* T* (i) Tj (>) Tj ( ..clear) Tj T* (cleared the shelve) Tj T* (i) Tj (>) Tj ( a=1) Tj T* (setting a=1) Tj T* (i) Tj (>) Tj ( a) Tj T* (1) Tj T* (i) Tj (>) Tj ( b=2) Tj T* (setting b=2) Tj T* (i) Tj (>) Tj ( a b) Tj T* (1) Tj T* (2) Tj T* (i) Tj (>) Tj ( ..delete a) Tj T* (deleted a) Tj T* (i) Tj (>) Tj ( a) Tj T* (a: not found) Tj T* (i) Tj (>) Tj ( ..all) Tj T* (b=2) Tj T* (i) Tj (>) Tj ( [CTRL-D]) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 282.6236 cm +q +BT 1 0 0 1 0 100.82 Tm 1.991654 Tw 12 TL /F1 10 Tf 0 0 0 rg (As you see, it is possibly to write command interpreters directly on top of ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (and it is not) Tj T* 0 Tw 5.022126 Tw (particularly difficult. However, the devil is in the details \(I mean error management\) and my) Tj T* 0 Tw 5.139269 Tw (recommendation, if you want to implement an interpreter of commands, is to use the class) Tj T* 0 Tw .05229 Tw /F3 10 Tf (plac.Interpreter ) Tj /F1 10 Tf (which is especially suited for this task. ) Tj /F3 10 Tf (plac.Interpreter ) Tj /F1 10 Tf (is available only if you) Tj T* 0 Tw .454488 Tw (are using a recent version of Python \() Tj (>) Tj (= 2.5\), because it is a context manager object to be used with the) Tj T* 0 Tw .38229 Tw /F3 10 Tf (with ) Tj /F1 10 Tf (statement. The only important method of ) Tj /F3 10 Tf (plac.Interpreter ) Tj /F1 10 Tf (is the ) Tj /F3 10 Tf (.send ) Tj /F1 10 Tf (method, which takes) Tj T* 0 Tw .057209 Tw (a string in input and returns a string in output. Internally the input string is splitted with ) Tj /F3 10 Tf (shlex.split ) Tj /F1 10 Tf (and) Tj T* 0 Tw 2.706136 Tw (passed to ) Tj /F3 10 Tf (plac.call) Tj /F1 10 Tf (, with some trick to manage exceptions correctly. Moreover long options are) Tj T* 0 Tw (managed with a single prefix character.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 105.4236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 168 re B* +Q +q +BT 1 0 0 1 0 149.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ("""Call this script with rlwrap and you will be happy""") Tj T* (from __future__ import with_statement) Tj T* (from plac_shell import Interpreter) Tj T* (from shelve_interface import interpreter) Tj T* T* (if __name__ == '__main__':) Tj T* ( with Interpreter\(interpreter\) as i:) Tj T* ( while True:) Tj T* ( try:) Tj T* ( line = raw_input\('i) Tj (>) Tj ( '\)) Tj T* ( except EOFError:) Tj T* ( break) Tj T* ( print\(i.send\(line\)\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 56.69291 56.69291 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 238.1649 0 Td (3) Tj T* -238.1649 0 Td ET +Q +Q + +endstream + +endobj +% 'R43': class PDFStream +43 0 obj +% page stream +<< /Length 3539 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 744.0236 cm +q +BT 1 0 0 1 0 8.435 Tm 21 TL /F2 17.5 Tf 0 0 0 rg (Multi-parsers) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 690.0236 cm +q +BT 1 0 0 1 0 40.82 Tm .594988 Tw 12 TL /F1 10 Tf 0 0 0 rg (As we saw, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is able to infer an arguments parser from the signature of a function. In addition, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is) Tj T* 0 Tw 1.96186 Tw (also able to infer a multi-parser from a container of commands, by inferring the subparsers from the) Tj T* 0 Tw .352651 Tw (commands. That is useful if you want to implement ) Tj /F4 10 Tf (subcommands ) Tj /F1 10 Tf (\(a familiar example of a command-line) Tj T* 0 Tw (application featuring subcommands is subversion\).) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 648.0236 cm +q +BT 1 0 0 1 0 28.82 Tm .636457 Tw 12 TL /F1 10 Tf 0 0 0 rg (A container of commands is any object with a ) Tj /F3 10 Tf (.commands ) Tj /F1 10 Tf (attribute listing a set of functions or methods) Tj T* 0 Tw 1.50936 Tw (which are valid commands. In particular, a Python module is a perfect container of commands. As an) Tj T* 0 Tw (example, consider the following module implementing a fake Version Control System:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 350.8236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 288 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 269.71 Tm /F3 10 Tf 12 TL ("A Fake Version Control System") Tj T* T* (import plac) Tj T* T* (commands = 'checkout', 'commit', 'status') Tj T* T* (@plac.annotations\() Tj T* ( url=\('url of the source code', 'positional'\)\)) Tj T* (def checkout\(url\):) Tj T* ( return \('checkout ', url\)) Tj T* T* (@plac.annotations\() Tj T* ( message=\('commit message', 'option'\)\)) Tj T* (def commit\(message\):) Tj T* ( return \('commit ', message\)) Tj T* T* (@plac.annotations\(quiet=\('summary information', 'flag'\)\)) Tj T* (def status\(quiet\):) Tj T* ( return \('status ', quiet\)) Tj T* T* (if __name__ == '__main__':) Tj T* ( import __main__) Tj T* ( print\(plac.call\(__main__\)\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 330.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here is the usage message:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 189.6236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 132 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 113.71 Tm /F3 10 Tf 12 TL (usage: vcs.py [-h] {status,commit,checkout} ...) Tj T* T* (A Fake Version Control System) Tj T* T* (optional arguments:) Tj T* ( -h, --help show this help message and exit) Tj T* T* (subcommands:) Tj T* ( {status,commit,checkout}) Tj T* ( -h to get additional help) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 133.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 40.82 Tm /F1 10 Tf 12 TL 1.614104 Tw (If the commands are completely independent, a module is a good fit for a method container. In other) Tj T* 0 Tw .876654 Tw (situations, it is best to use a custom class. For instance, suppose you want to store the configuration of) Tj T* 0 Tw 2.040574 Tw (your application into a Python shelve; then, you may want to write a command-line tool to edit your) Tj T* 0 Tw (configuration, i.e. a shelve interface:) Tj T* ET +Q +Q +q +1 0 0 1 56.69291 56.69291 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 238.1649 0 Td (4) Tj T* -238.1649 0 Td ET +Q +Q + +endstream + +endobj +% 'R44': class PDFStream +44 0 obj +% page stream +<< /Length 2642 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 295.8236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 468 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 449.71 Tm /F3 10 Tf 12 TL (import shelve) Tj T* (import plac) Tj T* T* (# error checking is missing: this is left to the reader) Tj T* (class ShelveInterface\(object\):) Tj T* ( "A minimal interface over a shelve object") Tj T* ( commands = 'set', 'show', 'show_all', 'delete') Tj T* ( def __init__\(self, fname\):) Tj T* ( self.fname = fname) Tj T* ( self.sh = shelve.open\(fname\)) Tj T* ( def set\(self, name, value\):) Tj T* ( "set name value") Tj T* ( yield 'setting %s=%s' % \(name, value\)) Tj T* ( self.sh[name] = value) Tj T* ( def show\(self, *names\):) Tj T* ( "show given parameters") Tj T* ( for name in names:) Tj T* ( yield '%s = %s\\n' % \(name, self.sh[name]\)) Tj T* ( def show_all\(self\):) Tj T* ( "show all parameters") Tj T* ( for name in self.sh:) Tj T* ( yield '%s = %s\\n' % \(name, self.sh[name]\)) Tj T* ( def delete\(self, name=None\):) Tj T* ( "delete given parameter \(or everything\)") Tj T* ( if name is None:) Tj T* ( yield 'deleting everything') Tj T* ( self.sh.clear\(\)) Tj T* ( else:) Tj T* ( yield 'deleting %s' % name) Tj T* ( del self.sh[name]) Tj T* T* (if __name__ == '__main__':) Tj T* ( interface = ShelveInterface\('conf.shelve'\)) Tj T* ( try:) Tj T* ( for output in plac.call\(interface\):) Tj T* ( print\(output\)) Tj T* ( finally:) Tj T* ( interface.sh.close\(\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 275.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here is a session of usage on an Unix-like operating system:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 98.62362 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 168 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 149.71 Tm /F3 10 Tf 12 TL ($ alias conf="python shelve_interface.py") Tj T* ($ conf set a pippo) Tj T* (setting a=pippo) Tj T* ($ conf set b lippo) Tj T* (setting b=lippo) Tj T* ($ conf show_all) Tj T* (b = lippo) Tj T* (a = pippo) Tj T* ($ conf show a b) Tj T* (a = pippo) Tj T* (b = lippo) Tj T* ($ conf delete a) Tj T* (deleting a) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 56.69291 56.69291 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 238.1649 0 Td (5) Tj T* -238.1649 0 Td ET +Q +Q + +endstream + +endobj +% 'R45': class PDFStream +45 0 obj +% page stream +<< /Length 3421 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 727.8236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 36 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 17.71 Tm /F3 10 Tf 12 TL ($ conf show_all) Tj T* (b = lippo) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 671.8236 cm +q +BT 1 0 0 1 0 40.82 Tm .581235 Tw 12 TL /F1 10 Tf 0 0 0 rg (Technically a multi-parser is a parser object with an attribute ) Tj /F3 10 Tf (.subp ) Tj /F1 10 Tf (which is a dictionary of subparsers;) Tj T* 0 Tw 1.757984 Tw (each of the methods listed in the attribute ) Tj /F3 10 Tf (.commands ) Tj /F1 10 Tf (corresponds to a subparser inferred from the) Tj T* 0 Tw .593984 Tw (method signature. The original object gets a ) Tj /F3 10 Tf (.p ) Tj /F1 10 Tf (attribute containing the main parser which is associated) Tj T* 0 Tw (to an internal function which dispatches on the right method depending on the method name.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 653.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here is the usage message:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 308.6236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 336 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 317.71 Tm /F3 10 Tf 12 TL (import plac) Tj T* T* (class FVCS\(object\):) Tj T* ( "A Fake Version Control System") Tj T* ( commands = 'checkout', 'commit', 'status', 'help') Tj T* T* ( @plac.annotations\() Tj T* ( name=\('a recognized command', 'positional', None, str, commands\)\)) Tj T* ( def help\(self, name\):) Tj T* ( self.p.subp[name].print_help\(\)) Tj T* T* ( @plac.annotations\() Tj T* ( url=\('url of the source code', 'positional'\)\)) Tj T* ( def checkout\(self, url\):) Tj T* ( print\('checkout', url\)) Tj T* T* ( def commit\(self\):) Tj T* ( print\('commit'\)) Tj T* T* ( @plac.annotations\(quiet=\('summary information', 'flag'\)\)) Tj T* ( def status\(self, quiet\):) Tj T* ( print\('status', quiet\)) Tj T* T* (main = FVCS\(\)) Tj T* T* (if __name__ == '__main__':) Tj T* ( plac.call\(main\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 167.4236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 132 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 113.71 Tm /F3 10 Tf 12 TL (usage: example13.py [-h] {status,commit,checkout,help} ...) Tj T* T* (A Fake Version Control System) Tj T* T* (optional arguments:) Tj T* ( -h, --help show this help message and exit) Tj T* T* (subcommands:) Tj T* ( {status,commit,checkout,help}) Tj T* ( -h to get additional help) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 134.4236 cm +q +BT 1 0 0 1 0 8.435 Tm 21 TL /F2 17.5 Tf 0 0 0 rg (Advanced usage) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 104.4236 cm +q +BT 1 0 0 1 0 16.82 Tm .094988 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (relies on a ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (for all of the heavy lifting work and it is possible to leverage on ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (features) Tj T* 0 Tw (directly or indirectly.) Tj T* ET +Q +Q +q +1 0 0 1 56.69291 56.69291 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 238.1649 0 Td (6) Tj T* -238.1649 0 Td ET +Q +Q + +endstream + +endobj +% 'R46': class PDFStream +46 0 obj +% page stream +<< /Length 5867 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 729.0236 cm +q +BT 1 0 0 1 0 28.82 Tm 5.575697 Tw 12 TL /F1 10 Tf 0 0 0 rg (For instance, you can make invisible an argument in the usage message simply by using) Tj T* 0 Tw 1.435976 Tw /F3 10 Tf ('==SUPPRESS==' ) Tj /F1 10 Tf (as help string \(or ) Tj /F3 10 Tf (argparse.SUPPRESS) Tj /F1 10 Tf (\). Similarly, you can use ) Tj 0 0 .501961 rg (argparse.FileType) Tj T* 0 Tw 0 0 0 rg (directly.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 675.0236 cm +q +BT 1 0 0 1 0 40.82 Tm 1.639213 Tw 12 TL /F1 10 Tf 0 0 0 rg (It is also possible to pass options to the underlying ) Tj /F3 10 Tf (argparse.ArgumentParser ) Tj /F1 10 Tf (object \(currently it) Tj T* 0 Tw .285529 Tw (accepts the default arguments ) Tj /F3 10 Tf (description) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (epilog) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (prog) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (usage) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (add_help) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (argument_default) Tj /F1 10 Tf (,) Tj T* 0 Tw 1.439953 Tw /F3 10 Tf (parents) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (prefix_chars) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (fromfile_prefix_chars) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (conflict_handler) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (formatter_class) Tj /F1 10 Tf (\). It) Tj T* 0 Tw (is enough to set such attributes on the ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (function. For instance) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 605.8236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 60 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 41.71 Tm /F3 10 Tf 12 TL (def main\(...\):) Tj T* ( pass) Tj T* T* (main.add_help = False) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 525.8236 cm +q +BT 1 0 0 1 0 64.82 Tm 1.256457 Tw 12 TL /F1 10 Tf 0 0 0 rg (disable the recognition of the help flag ) Tj /F3 10 Tf (-h, --help) Tj /F1 10 Tf (. This is not particularly elegant, but I assume the) Tj T* 0 Tw .275703 Tw (typical user of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (will be happy with the defaults and would not want to change them; still it is possible if) Tj T* 0 Tw .365542 Tw (she wants to. For instance, by setting the ) Tj /F3 10 Tf (description ) Tj /F1 10 Tf (attribute, it is possible to add a comment to the) Tj T* 0 Tw .602339 Tw (usage message \(by default the docstring of the ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (function is used as description\). It is also possible) Tj T* 0 Tw .322988 Tw (to change the option prefix; for instance if your script must run under Windows and you want to use "/" as) Tj T* 0 Tw (option prefix you can add the line:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 492.6236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 24 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL (main.prefix_chars='/-') Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 436.6236 cm +q +BT 1 0 0 1 0 40.82 Tm .591163 Tw 12 TL /F3 10 Tf 0 0 0 rg (prefix_chars ) Tj /F1 10 Tf (is an ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (feature. The first prefix char \() Tj /F3 10 Tf (/) Tj /F1 10 Tf (\) is used as the default in the construction) Tj T* 0 Tw .266098 Tw (of both short and long options; the second prefix char \() Tj /F3 10 Tf (-) Tj /F1 10 Tf (\) is kept to keep the ) Tj /F3 10 Tf (-h/--help ) Tj /F1 10 Tf (option working:) Tj T* 0 Tw .107209 Tw (however you can disable it and reimplement it if you like. For instance, here is how you could reimplement) Tj T* 0 Tw (the ) Tj /F3 10 Tf (help ) Tj /F1 10 Tf (command in the Fake VCS example:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 235.4236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 192 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 173.71 Tm /F3 10 Tf 12 TL (import plac) Tj T* (from example13 import FVCS) Tj T* T* (class VCS_with_help\(FVCS\):) Tj T* ( commands = FVCS.commands + \('help',\)) Tj T* T* ( @plac.annotations\() Tj T* ( name=\('a recognized command', 'positional', None, str, commands\)\)) Tj T* ( def help\(self, name\):) Tj T* ( self.p.subp[name].print_help\(\)) Tj T* T* (main = VCS_with_help\(\)) Tj T* T* (if __name__ == '__main__':) Tj T* ( plac.call\(main\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 191.4236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.938443 Tw 12 TL /F1 10 Tf 0 0 0 rg (Internally ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (uses ) Tj /F3 10 Tf (plac.parser_from ) Tj /F1 10 Tf (and adds the parser as an attribute ) Tj /F3 10 Tf (.p) Tj /F1 10 Tf (. This also) Tj T* 0 Tw 8.165366 Tw (happers for multiparsers and you can take advantage of the ) Tj /F3 10 Tf (.p ) Tj /F1 10 Tf (attribute to invoke) Tj T* 0 Tw /F3 10 Tf (argparse.ArgumentParser ) Tj /F1 10 Tf (methods.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 149.4236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.442126 Tw 12 TL /F1 10 Tf 0 0 0 rg (Interested readers should read the documentation of ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (to understand the meaning of the other) Tj T* 0 Tw .771567 Tw (options. If there is a set of options that you use very often, you may consider writing a decorator adding) Tj T* 0 Tw (such options to the ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (function for you. For simplicity, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (does not perform any magic of that kind.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 119.4236 cm +q +BT 1 0 0 1 0 16.82 Tm 7.709147 Tw 12 TL /F1 10 Tf 0 0 0 rg (It is possible to access directly the underlying ) Tj 0 0 .501961 rg (ArgumentParser ) Tj 0 0 0 rg (object, by invoking the) Tj T* 0 Tw /F3 10 Tf (plac.parser_from ) Tj /F1 10 Tf (utility function:) Tj T* ET +Q +Q +q +1 0 0 1 56.69291 56.69291 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 238.1649 0 Td (7) Tj T* -238.1649 0 Td ET +Q +Q + +endstream + +endobj +% 'R47': class PDFStream +47 0 obj +% page stream +<< /Length 3975 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 655.8236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 108 re B* +Q +q +BT 1 0 0 1 0 89.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (>) Tj (>) Tj (>) Tj ( import plac) Tj T* (>) Tj (>) Tj (>) Tj ( def main\(arg\):) Tj T* (... pass) Tj T* (...) Tj T* (>) Tj (>) Tj (>) Tj ( print plac.parser_from\(main\)) Tj T* (ArgumentParser\(prog='', usage=None, description=None, version=None,) Tj T* (formatter_class=) Tj (<) Tj (class 'argparse.HelpFormatter') Tj (>) Tj (, conflict_handler='error',) Tj T* (add_help=True\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 623.8236 cm +q +BT 1 0 0 1 0 16.82 Tm .365542 Tw 12 TL /F1 10 Tf 0 0 0 rg (I use ) Tj /F3 10 Tf (plac.parser_from ) Tj /F1 10 Tf (in the unit tests of the module, but regular users should never need to use it,) Tj T* 0 Tw (since the parser is also available as an attribute of the main function.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 590.8236 cm +q +BT 1 0 0 1 0 8.435 Tm 21 TL /F2 17.5 Tf 0 0 0 rg (Custom annotation objects) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 560.8236 cm +q +BT 1 0 0 1 0 16.82 Tm .578651 Tw 12 TL /F1 10 Tf 0 0 0 rg (Internally ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (uses an ) Tj /F3 10 Tf (Annotation ) Tj /F1 10 Tf (class to convert the tuples in the function signature into annotation) Tj T* 0 Tw (objects, i.e. objects with six attributes ) Tj /F3 10 Tf (help, kind, short, type, choices, metavar) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 530.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL .083735 Tw (Advanced users can implement their own annotation objects. For instance, here is an example of how you) Tj T* 0 Tw (could implement annotations for positional arguments:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 401.6236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 120 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 101.71 Tm /F3 10 Tf 12 TL (# annotations.py) Tj T* (class Positional\(object\):) Tj T* ( def __init__\(self, help='', type=None, choices=None, metavar=None\):) Tj T* ( self.help = help) Tj T* ( self.kind = 'positional') Tj T* ( self.abbrev = None) Tj T* ( self.type = type) Tj T* ( self.choices = choices) Tj T* ( self.metavar = metavar) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 381.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (You can use such annotations objects as follows:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 204.4236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 168 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 149.71 Tm /F3 10 Tf 12 TL (# example11.py) Tj T* (import plac) Tj T* (from annotations import Positional) Tj T* T* (@plac.annotations\() Tj T* ( i=Positional\("This is an int", int\),) Tj T* ( n=Positional\("This is a float", float\),) Tj T* ( rest=Positional\("Other arguments"\)\)) Tj T* (def main\(i, n, *rest\):) Tj T* ( print\(i, n, rest\)) Tj T* T* (if __name__ == '__main__':) Tj T* ( import plac; plac.call\(main\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 184.4236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here is the usage message you get:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 91.22362 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 84 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 65.71 Tm /F3 10 Tf 12 TL (usage: example11.py [-h] i n [rest [rest ...]]) Tj T* T* (positional arguments:) Tj T* ( i This is an int) Tj T* ( n This is a float) Tj T* ( rest Other arguments) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 56.69291 56.69291 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 238.1649 0 Td (8) Tj T* -238.1649 0 Td ET +Q +Q + +endstream + +endobj +% 'R48': class PDFStream +48 0 obj +% page stream +<< /Length 1026 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 715.8236 cm +q +q +1 0 0 1 0 0 cm +q +1 0 0 1 6.6 6.6 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 468.6898 48 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 29.71 Tm /F3 10 Tf 12 TL T* (optional arguments:) Tj T* ( -h, --help show this help message and exit) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 671.8236 cm +q +BT 1 0 0 1 0 28.82 Tm .713516 Tw 12 TL /F1 10 Tf 0 0 0 rg (You can go on and define ) Tj /F3 10 Tf (Option ) Tj /F1 10 Tf (and ) Tj /F3 10 Tf (Flag ) Tj /F1 10 Tf (classes, if you like. Using custom annotation objects you) Tj T* 0 Tw .17528 Tw (could do advanced things like extracting the annotations from a configuration file or from a database, but I) Tj T* 0 Tw (expect such use cases to be quite rare: the default mechanism should work pretty well for most users.) Tj T* ET +Q +Q +q +1 0 0 1 56.69291 56.69291 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 238.1649 0 Td (9) Tj T* -238.1649 0 Td ET +Q +Q + +endstream + +endobj +% 'R49': class PDFPageLabels +49 0 obj +% Document Root +<< /Nums [ 0 + 50 0 R + 1 + 51 0 R + 2 + 52 0 R + 3 + 53 0 R + 4 + 54 0 R + 5 + 55 0 R + 6 + 56 0 R + 7 + 57 0 R + 8 + 58 0 R ] >> +endobj +% 'R50': class PDFPageLabel +50 0 obj +% None +<< /S /D + /St 1 >> +endobj +% 'R51': class PDFPageLabel +51 0 obj +% None +<< /S /D + /St 2 >> +endobj +% 'R52': class PDFPageLabel +52 0 obj +% None +<< /S /D + /St 3 >> +endobj +% 'R53': class PDFPageLabel +53 0 obj +% None +<< /S /D + /St 4 >> +endobj +% 'R54': class PDFPageLabel +54 0 obj +% None +<< /S /D + /St 5 >> +endobj +% 'R55': class PDFPageLabel +55 0 obj +% None +<< /S /D + /St 6 >> +endobj +% 'R56': class PDFPageLabel +56 0 obj +% None +<< /S /D + /St 7 >> +endobj +% 'R57': class PDFPageLabel +57 0 obj +% None +<< /S /D + /St 8 >> +endobj +% 'R58': class PDFPageLabel +58 0 obj +% None +<< /S /D + /St 9 >> +endobj +xref +0 59 +0000000000 65535 f +0000000113 00000 n +0000000246 00000 n +0000000411 00000 n +0000000598 00000 n +0000000847 00000 n +0000001096 00000 n +0000001334 00000 n +0000001493 00000 n +0000001808 00000 n +0000002087 00000 n +0000002381 00000 n +0000002632 00000 n +0000002871 00000 n +0000003051 00000 n +0000003361 00000 n +0000003655 00000 n +0000003905 00000 n +0000004154 00000 n +0000004389 00000 n +0000004722 00000 n +0000005038 00000 n +0000005288 00000 n +0000005538 00000 n +0000005788 00000 n +0000006040 00000 n +0000006312 00000 n +0000006673 00000 n +0000006910 00000 n +0000007211 00000 n +0000007492 00000 n +0000007651 00000 n +0000007912 00000 n +0000008037 00000 n +0000008209 00000 n +0000008414 00000 n +0000008632 00000 n +0000008821 00000 n +0000009011 00000 n +0000009181 00000 n +0000009359 00000 n +0000013314 00000 n +0000017121 00000 n +0000020858 00000 n +0000024498 00000 n +0000027241 00000 n +0000030763 00000 n +0000036731 00000 n +0000040807 00000 n +0000041938 00000 n +0000042135 00000 n +0000042212 00000 n +0000042289 00000 n +0000042366 00000 n +0000042443 00000 n +0000042520 00000 n +0000042597 00000 n +0000042674 00000 n +0000042751 00000 n +trailer +<< /ID + % ReportLab generated PDF document -- digest (http://www.reportlab.com) + [(\030N\026;L\241\300\254\011\265\273\363\247\201 N) (\030N\026;L\241\300\254\011\265\273\363\247\201 N)] + + /Info 31 0 R + /Root 30 0 R + /Size 59 >> +startxref +42798 +%%EOF diff --git a/plac/doc/plac_adv.txt b/plac/doc/plac_adv.txt new file mode 100644 index 0000000..16a974b --- /dev/null +++ b/plac/doc/plac_adv.txt @@ -0,0 +1,279 @@ +Testing and scripting your applications with plac +========================================================= + +Introduction +----------------------------------------------------- + +plac_ has been designed to be simple to use for simple stuff, but in +truth it is a quite advanced tool with a field of applicability +which far outreaches the specific domain of command-line arguments parsers. +In reality plac_ is a generic tool to write domain specific +languages (DSL). +This document explains how you can use plac to test your application, and +how you can use it to provide a scripting interface to your application. +Notice that your application does not need to be a command-line +application: you can use plac_ whenever you have an API with strings +in input and strings in output. + +Testing applications with plac +------------------------------------------- + +In the standard usage, ``plac.call`` is called only once on the main +function; however in the tests it quite natural to invoke ``plac.call`` +multiple times on the same function with different arguments. +For instance, suppose you want to store the configuration of +your application into a Python shelve; then, you may want to write +a command-line tool to edit your configuration, i.e. a shelve +interface. A possible implementation could be the following: + +.. include:: ishelve.py + :literal: + +You can write the tests for such implementation as follows: + +.. include:: test_ishelve.py + :literal: + +There is a small optimization here: once ``plac.call(func)`` +has been called, a ``.p`` attribute is attached to ``func``, containing +the parser associated to the function annotations. The second time +``plac.call(func)`` is invoked, the parser is re-used. + +Writing command-line interpreters with plac +----------------------------------------------- + +Apart from testing, there is another typical use case where ``plac.call`` +is invoked multiple times, in the implementation of command interpreters. +For instance, you could define an interative interpreter on top +of ``ishelve`` as follows: + +.. include:: shelve_interpreter.py + :literal: + +Here is an usage session, usinng ``rlwrap`` to enable readline features:: + + $ rlwrap python shelve_interpreter.py -i + + i> ..clear + cleared the shelve + i> a=1 + setting a=1 + i> a + 1 + i> b=2 + setting b=2 + i> a b + 1 + 2 + i> ..delete a + deleted a + i> a + a: not found + i> ..all + b=2 + i> [CTRL-D] + +As you see, it is possibly to write command interpreters directly on top of +``plac.call`` and it is not particularly difficult. However, the devil +is in the details (I mean error management) and my recommendation, if +you want to implement an interpreter of commands, is to use the +class ``plac.Interpreter`` which is especially suited for this +task. ``plac.Interpreter`` is available only if you are using a recent +version of Python (>= 2.5), because it is a context manager object +to be used with the ``with`` statement. The only important method +of ``plac.Interpreter`` is the ``.send`` method, which takes a +string in input and returns a string in output. Internally the input string +is splitted with ``shlex.split`` and passed to ``plac.call``, +with some trick to manage exceptions correctly. Moreover long options +are managed with a single prefix character. + +.. include:: shelve_cli.py + :literal: + +Multi-parsers +---------------------------------------- + +As we saw, plac_ is able to infer an arguments parser from the +signature of a function. In addition, plac_ is also able to infer a +multi-parser from a container of commands, by inferring the subparsers +from the commands. That is useful if you want to implement +*subcommands* (a familiar example of a command-line application +featuring subcommands is subversion). + +A container of commands is any object with a ``.commands`` attribute +listing a set of functions or methods which are valid commands. In +particular, a Python module is a perfect container of commands. As an +example, consider the following module implementing a fake Version +Control System: + +.. include:: vcs.py + :literal: + +Here is the usage message: + +.. include:: vcs.help + :literal: + +If the commands are completely independent, a module is a good fit for +a method container. In other situations, it is best to use a custom +class. For instance, suppose you want to store the configuration of +your application into a Python shelve; then, you may want to write +a command-line tool to edit your configuration, i.e. a shelve +interface: + +.. include:: shelve_interface2.py + :literal: + +Here is a session of usage on an Unix-like operating system:: + + $ alias conf="python shelve_interface.py" + $ conf set a pippo + setting a=pippo + $ conf set b lippo + setting b=lippo + $ conf show_all + b = lippo + a = pippo + $ conf show a b + a = pippo + b = lippo + $ conf delete a + deleting a + $ conf show_all + b = lippo + +Technically a multi-parser is a parser object with an attribute ``.subp`` +which is a dictionary of subparsers; each of the methods listed in +the attribute ``.commands`` corresponds to a subparser inferred from +the method signature. The original object gets a ``.p`` attribute +containing the main parser which is associated to an internal function +which dispatches on the right method depending on the method name. + +Here is the usage message: + +.. include:: example13.py + :literal: + +.. include:: example13.help + :literal: + +Advanced usage +---------------------------------------------------- + +plac_ relies on a argparse_ for all of the heavy lifting work and it is +possible to leverage on argparse_ features directly or indirectly. + +For instance, you can make invisible an argument in the usage message +simply by using ``'==SUPPRESS=='`` as help string (or +``argparse.SUPPRESS``). Similarly, you can use argparse.FileType_ +directly. + +It is also possible to pass options to the underlying +``argparse.ArgumentParser`` object (currently it accepts the default +arguments ``description``, ``epilog``, ``prog``, ``usage``, +``add_help``, ``argument_default``, ``parents``, ``prefix_chars``, +``fromfile_prefix_chars``, ``conflict_handler``, ``formatter_class``). +It is enough to set such attributes on the ``main`` function. For +instance + +:: + + def main(...): + pass + + main.add_help = False + +disable the recognition of the help flag ``-h, --help``. This is not +particularly elegant, but I assume the typical user of plac_ will be +happy with the defaults and would not want to change them; still it is +possible if she wants to. For instance, by setting the ``description`` +attribute, it is possible to add a comment to the usage message (by +default the docstring of the ``main`` function is used as +description). It is also possible to change the option prefix; for +instance if your script must run under Windows and you want to use "/" +as option prefix you can add the line:: + + main.prefix_chars='/-' + +``prefix_chars`` is an argparse_ feature. The first prefix char (``/``) is used +as the default in the construction of both short and long options; +the second prefix char (``-``) is kept to keep the ``-h/--help`` option +working: however you can disable it and reimplement it if you like. +For instance, here is how you could reimplement the ``help`` command +in the Fake VCS example: + +.. include:: example14.py + :literal: + +Internally ``plac.call`` uses +``plac.parser_from`` and adds the parser as an attribute ``.p``. +This also happers for multiparsers and you can take advantage of +the ``.p`` attribute to invoke ``argparse.ArgumentParser`` methods. + +Interested readers should read the documentation of argparse_ to +understand the meaning of the other options. If there is a set of +options that you use very often, you may consider writing a decorator +adding such options to the ``main`` function for you. For simplicity, +plac_ does not perform any magic of that kind. + +It is possible to access directly the underlying ArgumentParser_ object, by +invoking the ``plac.parser_from`` utility function: + +>>> import plac +>>> def main(arg): +... pass +... +>>> print plac.parser_from(main) +ArgumentParser(prog='', usage=None, description=None, version=None, +formatter_class=, conflict_handler='error', +add_help=True) + +I use ``plac.parser_from`` in the unit tests of the module, but regular +users should never need to use it, since the parser is also available +as an attribute of the main function. + +Custom annotation objects +------------------------------------------------------ + +Internally plac_ uses an ``Annotation`` class to convert the tuples +in the function signature into annotation objects, i.e. objects with +six attributes ``help, kind, short, type, choices, metavar``. + +Advanced users can implement their own annotation objects. +For instance, here is an example of how you could implement annotations for +positional arguments: + +.. include:: annotations.py + :literal: + +You can use such annotations objects as follows: + +.. include:: example11.py + :literal: + +Here is the usage message you get: + +.. include:: example11.help + :literal: + +You can go on and define ``Option`` and ``Flag`` classes, if you like. +Using custom annotation objects you could do advanced things like extracting the +annotations from a configuration file or from a database, but I expect such +use cases to be quite rare: the default mechanism should work +pretty well for most users. + +.. _argparse: http://argparse.googlecode.com +.. _optparse: http://docs.python.org/library/optparse.html +.. _getopt: http://docs.python.org/library/getopt.html +.. _optionparse: http://code.activestate.com/recipes/278844-parsing-the-command-line/ +.. _plac: http://pypi.python.org/pypi/plac +.. _scaling down: http://www.welton.it/articles/scalable_systems +.. _ArgumentParser: http://argparse.googlecode.com/svn/tags/r11/doc/ArgumentParser.html +.. _argparse.FileType: http://argparse.googlecode.com/svn/tags/r11/doc/other-utilities.html?highlight=filetype#FileType +.. _Clap: http://pypi.python.org/pypi/Clap/0.7 +.. _OptionParser: http://docs.python.org/library/optparse.html?highlight=optionparser#optparse.OptionParser +.. _SQLAlchemy: http://www.sqlalchemy.org/ +.. _SqlSoup: http://www.sqlalchemy.org/docs/reference/ext/sqlsoup.html +.. _CLIArgs: http://pypi.python.org/pypi/CLIArgs +.. _opterator: http://pypi.python.org/pypi/opterator +.. _advanced usage document: in-writing diff --git a/plac/doc/vcs.help b/plac/doc/vcs.help new file mode 100644 index 0000000..5d7ac77 --- /dev/null +++ b/plac/doc/vcs.help @@ -0,0 +1,10 @@ +usage: vcs.py [-h] {status,commit,checkout} ... + +A Fake Version Control System + +optional arguments: + -h, --help show this help message and exit + +subcommands: + {status,commit,checkout} + -h to get additional help diff --git a/plac/doc/vcs.py b/plac/doc/vcs.py new file mode 100644 index 0000000..a893fd9 --- /dev/null +++ b/plac/doc/vcs.py @@ -0,0 +1,23 @@ +"A Fake Version Control System" + +import plac + +commands = 'checkout', 'commit', 'status' + +@plac.annotations( + url=('url of the source code', 'positional')) +def checkout(url): + return ('checkout ', url) + +@plac.annotations( + message=('commit message', 'option')) +def commit(message): + return ('commit ', message) + +@plac.annotations(quiet=('summary information', 'flag')) +def status(quiet): + return ('status ', quiet) + +if __name__ == '__main__': + import __main__ + print(plac.call(__main__)) diff --git a/plac/plac.py b/plac/plac.py index 458e128..31bc89b 100644 --- a/plac/plac.py +++ b/plac/plac.py @@ -25,227 +25,13 @@ """ plac, the easiest Command Line Arguments Parser in the world. -See plac/doc.html for the documentation. +See doc/plac.pdf for the documentation. """ -# this module should be kept Python 2.3 compatible __version__ = '0.5.0' -import re, sys, inspect, argparse +import sys +from plac_core import * -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.varkw, self.defaults = \ - inspect.getargspec(f) - self.annotations = getattr(f, '__annotations__', {}) -try: - set -except NameError: # Python 2.3 - from sets import Set as set - -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) - if fas.varkw: - args.append(fas.varkw) - 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=None, - choices=None, metavar=None): - assert kind in ('positional', 'option', 'flag'), kind - if kind == "positional": - assert abbrev is None, 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 - -PARSER_CFG = getfullargspec(argparse.ArgumentParser.__init__).args[1:] -# the default arguments accepted by an ArgumentParser object - -class PlacHelpFormatter(argparse.HelpFormatter): - "Custom HelpFormatter which does not displau the default value twice" - - def _format_action_invocation(self, action): - if not action.option_strings: - return self._metavar_formatter(action, action.dest)(1)[0] - long_short = tuple(action.option_strings) - if action.nargs == 0: # format is -s, --long - return '%s, %s' % long_short - else: # format is -s, --long ARGS - default = action.dest.upper() - args_string = self._format_args(action, default) - return '%s, %s %s' % (long_short + (args_string,)) - -def _parser_from(func, baseparser=None, **cfg): - """ - Extract the arguments from the attributes of the passed function - (or bound method) and return an ArgumentParser instance. As a side - effect, adds a .p attribute to func. - """ - cfg.setdefault('description', func.__doc__) - cfg.setdefault('formatter_class', PlacHelpFormatter) - for n, v in vars(func).items(): - if n in PARSER_CFG: # arguments of ArgumentParser - cfg[n] = v - p = baseparser or argparse.ArgumentParser(**cfg) - p.func = func - f = p.argspec = getfullargspec(func) - if inspect.ismethod(func): - del f.args[0] # remove self - try: - func.im_func.p = p # Python 2.X - except AttributeError: - func.__func__.p = p # Python 2.3 - else: - func.p = p - defaults = f.defaults or () - n_args = len(f.args) - n_defaults = len(defaults) - alldefaults = (NONE,) * (n_args - n_defaults) + defaults - short_prefix = getattr(func, 'short_prefix', '-') - long_prefix = getattr(func, 'long_prefix', '--') - for name, default in zip(f.args, alldefaults): - ann = f.annotations.get(name, ()) - a = Annotation.from_(ann) - metavar = a.metavar - if default is NONE: - dflt = None - else: - dflt = default - if a.kind in ('option', 'flag'): - short = short_prefix + (a.abbrev or name[0]) - 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': - if default is not NONE: - metavar = metavar or str(default) - 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 and default is not False: - raise TypeError('Flag %r wants default False, got %r' % - (name, default)) - 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) - if f.varkw: - a = Annotation.from_(f.annotations.get(f.varkw, ())) - p.add_argument(f.varkw, nargs='*', help=a.help, default={}, - type=a.type, metavar=a.metavar) - return p - -def parser_from(obj, baseparser=None, **cfg): - """ - obj can be a function, a bound method, or a generic object with a - .commands attribute. Returns an ArgumentParser with attributes - .func and .argspec, or a multi-parser with attribute .sub. - """ - if hasattr(obj, 'p'): # the underlying parser has been generated already - return obj.p - elif hasattr(obj, 'commands'): # an object with commands - commands = obj.commands - elif inspect.isfunction(obj) or inspect.ismethod(obj): # error if not func - return _parser_from(obj, baseparser, **cfg) - p = obj.p = baseparser or argparse.ArgumentParser(**cfg) - subparsers = p.add_subparsers( - title='subcommands', help='-h to get additional help') - p.subp = {} - for cmd in commands: - method = getattr(obj, cmd) - p.subp[cmd] = _parser_from(method, subparsers.add_parser(cmd), **cfg) - return p - -def _extract_kwargs(args): - "Returns two lists: regular args and name=value args" - arglist = [] - kwargs = {} - for arg in args: - match = re.match(r'([a-zA-Z_]\w*)=', arg) - if match: - name = match.group(1) - kwargs[name] = arg[len(name)+1:] - else: - arglist.append(arg) - return arglist, kwargs - -def parser_call(p, arglist): - """ - Given a parser, calls its underlying callable with the arglist. - Works also for multiparsers by dispatching to the underlying parser. - """ - subp = getattr(p, 'subp', None) - if subp: # subparsers - p.parse_args(arglist) # argument checking - return parser_call(subp[arglist[0]], arglist[1:]) - # regular parser - if p.argspec.varkw: - arglist, kwargs = _extract_kwargs(arglist) - else: - kwargs = {} - argdict = vars(p.parse_args(arglist)) - args = [argdict[a] for a in p.argspec.args] - varargs = argdict.get(p.argspec.varargs, []) - collision = set(p.argspec.args) & set(kwargs) - if collision: - p.error('colliding keyword arguments: %s' % ' '.join(collision)) - return p.func(*(args + varargs), **kwargs) - -def call(obj, arglist=sys.argv[1:], **cfg): - """ - If obj is a function or a bound method, parses the given arglist - by using an argument parser inferred from the annotations of obj - and then calls obj with the parsed arguments. - If obj is an object with attribute .commands, builds a multiparser - and dispatches to the associated subparsers. - The user can provide a custom parse_annotation hook or replace - the default one. - """ - return parser_call(parser_from(obj, **cfg), arglist) +if sys.version >= '2.5': + from plac_ext import Interpreter diff --git a/plac/plac_core.py b/plac/plac_core.py new file mode 100644 index 0000000..8df1d03 --- /dev/null +++ b/plac/plac_core.py @@ -0,0 +1,218 @@ +# this module should be kept Python 2.3 compatible +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.varkw, self.defaults = \ + inspect.getargspec(f) + self.annotations = getattr(f, '__annotations__', {}) +try: + set +except NameError: # Python 2.3 + from sets import Set as set + +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) + if fas.varkw: + args.append(fas.varkw) + 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=None, + choices=None, metavar=None): + assert kind in ('positional', 'option', 'flag'), kind + if kind == "positional": + assert abbrev is None, 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 + +PARSER_CFG = getfullargspec(argparse.ArgumentParser.__init__).args[1:] +# the default arguments accepted by an ArgumentParser object + +def pconf(obj): + "Extracts the configuration of the underlying ArgumentParser from obj" + cfg = dict(description=obj.__doc__) + for name in dir(obj): + if name in PARSER_CFG: # argument of ArgumentParser + cfg[name] = getattr(obj, name) + return cfg + +def _parser_from(func, baseparser=None): + """ + Extract the arguments from the attributes of the passed function + (or bound method) and return an ArgumentParser instance. As a side + effect, adds a .p attribute to func. + """ + p = baseparser or argparse.ArgumentParser(**pconf(func)) + p.func = func + p.argspec = f = getfullargspec(func) + p.parselist = _parser_call.__get__(p) + if inspect.ismethod(func): + del f.args[0] # remove self + try: + func.im_func.p = p # Python 2.X + except AttributeError: + func.__func__.p = p # Python 2.3 + else: + func.p = p + defaults = f.defaults or () + n_args = len(f.args) + n_defaults = len(defaults) + alldefaults = (NONE,) * (n_args - n_defaults) + defaults + prefix = p.prefix = getattr(func, 'prefix_chars', '-')[0] + for name, default in zip(f.args, alldefaults): + ann = f.annotations.get(name, ()) + a = Annotation.from_(ann) + metavar = a.metavar + if default is NONE: + dflt = None + else: + dflt = default + if a.kind in ('option', 'flag'): + if a.abbrev: + shortlong = (prefix + a.abbrev, prefix*2 + name) + else: + shortlong = (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': + if default is not NONE: + metavar = metavar or str(default) + p.add_argument(help=a.help, default=dflt, type=a.type, + choices=a.choices, metavar=metavar, *shortlong) + elif a.kind == 'flag': + if default is not NONE and default is not False: + raise TypeError('Flag %r wants default False, got %r' % + (name, default)) + p.add_argument(action='store_true', help=a.help, *shortlong) + 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) + if f.varkw: + a = Annotation.from_(f.annotations.get(f.varkw, ())) + p.add_argument(f.varkw, nargs='*', help=a.help, default={}, + type=a.type, metavar=a.metavar) + return p + +def parser_from(obj, baseparser=None): + """ + obj can be a function, a bound method, or a generic object with a + .commands attribute. Returns an ArgumentParser with attributes + .func and .argspec, or a multi-parser with attribute .sub. + """ + if hasattr(obj, 'p'): # the underlying parser has been generated already + return obj.p + elif hasattr(obj, 'commands'): # a command container + p = obj.p = baseparser or argparse.ArgumentParser(**pconf(obj)) + subparsers = p.add_subparsers( + title='subcommands', help='-h to get additional help') + p.subp = {} + for cmd in obj.commands: + method = getattr(obj, cmd) + baseparser = subparsers.add_parser(cmd, **pconf(method)) + p.subp[cmd] = _parser_from(method, baseparser) + p.parselist = _parser_call.__get__(p) + return p + elif inspect.isfunction(obj) or inspect.ismethod(obj): # error if not func + return _parser_from(obj, baseparser) + else: + raise TypeError('%r could not be converted into a parser' % obj) + +def _extract_kwargs(args): + "Returns two lists: regular args and name=value args" + arglist = [] + kwargs = {} + for arg in args: + match = re.match(r'([a-zA-Z_]\w*)=', arg) + if match: + name = match.group(1) + kwargs[name] = arg[len(name)+1:] + else: + arglist.append(arg) + return arglist, kwargs + +def _parser_call(p, arglist): + """ + Given a parser, calls its underlying callable with the arglist. + Works also for multiparsers by dispatching to the underlying parser. + """ + subp = getattr(p, 'subp', None) + if subp: # subparsers + p.parse_args(arglist) # argument checking + return _parser_call(subp[arglist[0]], arglist[1:]) + # regular parser + if p.argspec.varkw: + arglist, kwargs = _extract_kwargs(arglist) + else: + kwargs = {} + argdict = vars(p.parse_args(arglist)) + args = [argdict[a] for a in p.argspec.args] + varargs = argdict.get(p.argspec.varargs, []) + collision = set(p.argspec.args) & set(kwargs) + if collision: + p.error('colliding keyword arguments: %s' % ' '.join(collision)) + result = p.func(*(args + varargs), **kwargs) + if result is None: + return [] + elif hasattr(result, '__iter__') and not isinstance(result, str): + return [str(x) for x in result] + else: + return [str(result)] + +def call(obj, arglist=sys.argv[1:]): + """ + If obj is a function or a bound method, parses the given arglist + by using an argument parser inferred from the annotations of obj + and then calls obj with the parsed arguments. + If obj is an object with attribute .commands, builds a multiparser + and dispatches to the associated subparsers. + Return a list of strings. + """ + return parser_from(obj).parselist(arglist) diff --git a/plac/plac_ext.py b/plac/plac_ext.py new file mode 100644 index 0000000..6c904f2 --- /dev/null +++ b/plac/plac_ext.py @@ -0,0 +1,156 @@ +import os, sys, cmd, shlex +import plac + +def cmd_interface(obj): + "Returns a cmd.Cmd wrapper over the command container" + dic = {} + for command in obj.commands: + method = getattr(obj, command) + def do_func(self, line, command=command): + args = [command] + shlex.split(line) + try: + for output in plac.call(obj, args): + print(output) + except SystemExit: + print(e) + except Exception: + print('%s: %s' % (e.__class__.__name__, e)) + do_func.__doc__ = method.__doc__ + if sys.version >= '2.4': + do_func.__name__ = method.__name__ + dic['do_' + command] = do_func + clsname = '_%s_' % obj.__class__.__name__ + cls = type(clsname, (cmd.Cmd, object), dic) + return cls() + +# requires Python 2.5+ +class Interpreter(object): + """ + The safety_net is a function taking a parsing function and a list of + arguments and applying the first to the second by managing some class + of exceptions. + """ + def __init__(self, obj, safety_net=lambda parse, arglist: parse(arglist), + commentchar='#'): + self.obj = obj + self.safety_net = safety_net + self.commentchar = commentchar + self.interpreter = None + self.p = plac.parser_from(obj) + self.p.error = lambda msg: sys.exit(msg) # patch the parser + + def __enter__(self): + self.interpreter = self._make_interpreter() + self.interpreter.send(None) + return self + + def send(self, line): + """ + Send a line to the underlying interpreter. + Return a string or None for comment lines. + The line should end with a newline. + """ + return self.interpreter.send(line) + + def close(self): + self.interpreter.close() + + def __exit__(self, *exc): + self.close() + + def _make_interpreter(self): + enter = getattr(self.obj, '__enter__', lambda : None) + exit = getattr(self.obj, '__exit__', lambda a1, a2, a3: None) + enter() + result = None + prefix = self.p.short_prefix + try: + while True: + line = yield result + if not line: + break + elif line.startswith(self.commentchar): + yield; continue + arglist = shlex.split(line) + for i, long_opt in enumerate(arglist): + # avoid double prefix in long options + if len(long_opt) > 2 and long_opt[0] == prefix: + arglist[i] = prefix + long_opt + try: + output = self.safety_net(self.p.parselist, arglist) + except SystemExit, e: + output = [str(e)] + result = os.linesep.join(output) + except: + exit(*sys.exc_info()) + raise + else: + exit(None, None, None) + +## from cmd.py +# def complete(self, text, state): +# """Return the next possible completion for 'text'. + +# If a command has not been entered, then complete against command list. +# Otherwise try to call complete_ to get list of completions. +# """ +# if state == 0: +# import readline +# origline = readline.get_line_buffer() +# line = origline.lstrip() +# stripped = len(origline) - len(line) +# begidx = readline.get_begidx() - stripped +# endidx = readline.get_endidx() - stripped +# if begidx>0: +# cmd, args, foo = self.parseline(line) +# if cmd == '': +# compfunc = self.completedefault +# else: +# try: +# compfunc = getattr(self, 'complete_' + cmd) +# except AttributeError: +# compfunc = self.completedefault +# else: +# compfunc = self.completenames +# self.completion_matches = compfunc(text, line, begidx, endidx) +# try: +# return self.completion_matches[state] +# except IndexError: +# return None + +# def readlines(completekey='tab'): +# if readline: +# old_completer = readline.get_completer() +# readline.set_completer(self.complete) +# readline.parse_and_bind(self.completekey + ": complete") +# try: +# while True: +# try: +# yield raw_input('cli> ') +# except EOFError: +# break +# finally: +# if readline: +# readline.set_completer(old_completer) + +class Cmds(object): + commands = 'checkout', 'commit', 'status', 'help' + quit = False + + @plac.annotations( + name=('a recognized command', 'positional', None, str, commands)) + def help(self, name): + return self.p.subp[name].format_help() + + def checkout(self, url): + return ('checkout', url) + + def commit(self): + return ('commit') + + @plac.annotations(quiet=('summary information', 'flag')) + def status(self, quiet): + return ('status', quiet) + +if __name__ == '__main__': + cmdloop(Cmds()) diff --git a/plac/setup.py b/plac/setup.py index ca34c5b..0a7ee88 100644 --- a/plac/setup.py +++ b/plac/setup.py @@ -3,18 +3,23 @@ try: except ImportError: from distutils.core import setup import os.path -import plac + +def getversion(fname): + "Get the __version__ without importing plac" + for line in open(fname): + if line.startswith('__version__'): + return eval(line[13:]) if __name__ == '__main__': - setup(name=plac.__name__, - version=plac.__version__, + setup(name='plac', + version=getversion(os.path.join(os.path.dirname(__file__),'plac.py')), 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'], + py_modules = ['plac_core', 'plac_ext', 'plac'], install_requires=['argparse>=1.1'], keywords="command line arguments parser", platforms=["All"], diff --git a/plac/test_plac.py b/plac/test_plac.py index c20a275..3cfd936 100644 --- a/plac/test_plac.py +++ b/plac/test_plac.py @@ -25,12 +25,13 @@ def parser_from(f, **kw): def check_help(name): sys.argv[0] = name + '.py' dic = {} - try: - execfile(os.path.join('doc', name + '.py'), dic) - except NameError: # Python 3 + if sys.version >= '3': exec(open(os.path.join('doc', name + '.py')).read(), dic) - except SyntaxError: # raised by some tests when using Python 2 - return + else: # Python 2.X + try: + execfile(os.path.join('doc', name + '.py'), dic) + except SyntaxError: # raised by some tests when using Python 2 + return p = plac.parser_from(dic['main']) expected = open(os.path.join('doc', name + '.help')).read().strip() got = p.format_help().strip() @@ -105,16 +106,16 @@ def test_kwargs(): return args, kw main.__annotations__ = dict(opt=('Option', 'option')) argskw = plac.call(main, ['arg1', 'arg2', 'a=1', 'b=2']) - assert argskw == (('arg2',), dict(a='1', b='2')), argskw + assert argskw == ["('arg2',)", "{'a': '1', 'b': '2'}"], argskw - argskw = plac.call(main, ['arg1', 'arg2', 'a=1', '-o2']) - assert argskw == (('arg2',), dict(a='1')), argskw + argskw = plac.call(main, ['arg1', 'arg2', 'a=1', '-o', '2']) + assert argskw == ["('arg2',)", "{'a': '1'}"], argskw expect(SystemExit, plac.call, main, ['arg1', 'arg2', 'a=1', 'opt=2']) def test_expected_help(): for fname in os.listdir('doc'): - if fname.endswith('.help'): + if fname.endswith('.help') and fname != 'vcs.help': yield check_help, fname[:-5] class Cmds(object): @@ -126,10 +127,16 @@ class Cmds(object): def test_cmds(): cmds = Cmds() - assert 'commit' == plac.call(cmds, ['commit']) - assert 'help', 'foo' == plac.call(cmds, ['help', 'foo']) + assert ['commit'] == plac.call(cmds, ['commit']) + assert ['help', 'foo'] == plac.call(cmds, ['help', 'foo']) expect(SystemExit, plac.call, cmds, []) +def test_yield(): + def main(): + for i in (1, 2, 3): + yield i + assert plac.call(main, []) == ['1', '2', '3'] + if __name__ == '__main__': n = 0 for name, test in sorted(globals().items()): -- cgit v1.2.1