commit ce84e509c06ebd45953cf7948d888fcada50e990 Author: Kenneth Reitz Date: Mon Sep 6 01:32:36 2010 -0400 Plac mirror update to Plac 0.7.4. diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..0853f30 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,31 @@ +HISTORY +---------- + +0.7.4 Fixed the plac_runner switches -i and -s; fixed a bug with multiline output + and issue with nosetest (2010-09-04) +0.7.3 Put the documentation in a single document; added runp (2010-08-31) +0.7.2 Interpreter.call does not start an interpreter automagically anymore; + better documented and added tests for the metavar concept (2010-08-31) +0.7.1 A few bug fixes (2010-08-11) +0.7.0 Improved and documented the support for parallel programming; + added an asynchronous server; added plac.Interpreter.call (2010-08-07) +0.6.1 Fixed the history file location; added the ability to pass a split + function; added two forgotten files; added a reference to cmd2 by + Catherine Devlin (2010-07-12) +0.6.0 Improved the interactive experience with full readline support and + custom help. Added support for long running command, via threads and + processes (2010-07-11) +0.5.0 Gigantic release. Introduced smart options, added an Interpreter class + and the command container concept. Made the split plac/plac_core/plac_ext + and added a plac runner, able to run scripts, batch files and doctests. + Removed the default formatter class (2010-06-20) +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 + the output of the main function. (2010-06-03) +0.3.0 Initial version. (2010-06-02) diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..0f93421 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include *.txt doc/*.py doc/*.help doc/*.txt doc/*.html doc/*.pdf diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..86361da --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,77 @@ +Metadata-Version: 1.0 +Name: plac +Version: 0.7.4 +Summary: The smartest command line arguments parser in the world +Home-page: http://pypi.python.org/pypi/plac +Author: Michele Simionato +Author-email: michele.simionato@gmail.com +License: BSD License +Description: Installation + ------------- + + If you are lazy, just perform + + :: + + $ easy_install -U plac + + 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 + + :: + + $ python setup.py install + + in the main directory, possibly as superuser. + + .. _tarball: http://pypi.python.org/pypi/plac + .. _distribute: http://packages.python.org/distribute/ + + Testing + -------- + + Run + + :: + + $ python doc/test_plac.py + + or + + :: + + $ nosetests doc + + or + + :: + + $ py.test doc + + Some tests will fail if sqlalchemy is not installed. + Run an ``easy_install -U sqlalchemy`` or just ignore them. + + Documentation + -------------- + + The source code and the documentation are hosted on Google code. + Here is the full documentation in HTML and PDF form: + + http://micheles.googlecode.com/hg/plac/doc/plac.html + + http://micheles.googlecode.com/hg/plac/doc/plac.pdf + +Keywords: command line arguments parser +Platform: All +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..b99db3b --- /dev/null +++ b/README.txt @@ -0,0 +1,58 @@ +Installation +------------- + +If you are lazy, just perform + +:: + + $ easy_install -U plac + +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 + +:: + + $ python setup.py install + +in the main directory, possibly as superuser. + +.. _tarball: http://pypi.python.org/pypi/plac +.. _distribute: http://packages.python.org/distribute/ + +Testing +-------- + +Run + +:: + + $ python doc/test_plac.py + +or + +:: + + $ nosetests doc + +or + +:: + + $ py.test doc + +Some tests will fail if sqlalchemy is not installed. +Run an ``easy_install -U sqlalchemy`` or just ignore them. + +Documentation +-------------- + +The source code and the documentation are hosted on Google code. +Here is the full documentation in HTML and PDF form: + +http://micheles.googlecode.com/hg/plac/doc/plac.html + +http://micheles.googlecode.com/hg/plac/doc/plac.pdf diff --git a/doc/annotations.py b/doc/annotations.py new file mode 100644 index 0000000..0de2140 --- /dev/null +++ b/doc/annotations.py @@ -0,0 +1,9 @@ +# 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 diff --git a/doc/cmd_ex.py b/doc/cmd_ex.py new file mode 100644 index 0000000..4af18c4 --- /dev/null +++ b/doc/cmd_ex.py @@ -0,0 +1,6 @@ +# cmd_ext.py +from plac_ext import cmd_interface +import ishelve2 + +if __name__ == '__main__': + cmd_interface(ishelve2.main()).cmdloop() diff --git a/doc/dbcli.help b/doc/dbcli.help new file mode 100644 index 0000000..8c0c18a --- /dev/null +++ b/doc/dbcli.help @@ -0,0 +1,13 @@ +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 + +optional arguments: + -h, --help show this help message and exit + -H, --header Header + -c SQL, --sqlcmd SQL SQL command + -d |, --delimiter | Column separator diff --git a/doc/dbcli.py b/doc/dbcli.py new file mode 100644 index 0000000..d5db392 --- /dev/null +++ b/doc/dbcli.py @@ -0,0 +1,29 @@ +# dbcli.py +import plac +from sqlalchemy.ext.sqlsoup import SqlSoup + +@plac.annotations( + db=("Connection string", 'positional', None, SqlSoup), + header=("Header", 'flag', 'H'), + sqlcmd=("SQL command", 'option', 'c', str, None, "SQL"), + delimiter=("Column separator", 'option', 'd'), + scripts="SQL scripts", + ) +def main(db, header, sqlcmd, delimiter="|", *scripts): + "A script to run queries and SQL scripts on a database" + yield 'Working on %s' % db.bind.url + + if sqlcmd: + result = db.bind.execute(sqlcmd) + if header: # print the header + yield delimiter.join(result.keys()) + for row in result: # print the rows + yield delimiter.join(map(str, row)) + + for script in scripts: + db.bind.execute(file(script).read()) + yield 'executed %s' % script + +if __name__ == '__main__': + for output in plac.call(main): + print(output) diff --git a/doc/emacs-integration.txt b/doc/emacs-integration.txt new file mode 100644 index 0000000..38ffd9c --- /dev/null +++ b/doc/emacs-integration.txt @@ -0,0 +1,47 @@ + +.. + + Multiline support and Emacs integration + + plac_ is optimized for the simplest use case and by default it provide + support for simple command-line languages where a command take + a single line. This is the simplest case: it is easy to keep + track of the line number and to print it in the error message, if + there is some error in a plac_ script. Starting from release 0.7 + plac_ is beginning to support multiline input: it is now possible + to define command-line languages with commands spanning multiple + lines. The topical use case is the implementation of a tool + to interact with a relational database: the tool must be able to send + complex SQL queries spanning multiple lines to the backend. + To support multiline input the ``Interpreter`` class provides + a method ``multiline(stdin=sys.stdin, terminator=';', verbose=False)`` + which reads input from ``stdin`` until the terminator character + (by default a semicolon) is reached. + + Since the Python readline module does not expose the + multiline functionality of the underlying C library (which is there), + plac_ multiline mode does not have readline functionality. This is + not a big deal really, because if you are writing multiple line + commands you don't really want to type them at the command-line. It is + much better to use a real editor to type them, and to call plac_ from + the editor. Since I use Emacs I will give the recipe to integrate + Emacs with plac_: something equivalent can be done for vi and for + other editors/IDEs. + + The multiline mode can be enabled by invoking the plac_ runner with + the ``-m`` option. Since the multiline mode is intended for use with + Emacs in inferior mode, it does not print any prompt. Here is an example + of usage:: + + $ plac -m ishelve2.py + set a 1; + setting a=1 + show a; + a = 1 + + To integrate plac_ with Emacs, enters the following lines in your + .emacs: + + .. include:: plac.el + :literal: + diff --git a/doc/ex.py b/doc/ex.py new file mode 100644 index 0000000..21e875f --- /dev/null +++ b/doc/ex.py @@ -0,0 +1,10 @@ +import os, plac, ishelve2 + +try: + fifo = os.mkfifo('/tmp/x') +except OSError: # fifo exists + pass + +if __name__ == '__main__': + i = plac.Interpreter(ishelve2.main()) + i.execute(iter(open('/tmp/x').readline, ''), verbose=True) diff --git a/doc/example0.py b/doc/example0.py new file mode 100644 index 0000000..af345c3 --- /dev/null +++ b/doc/example0.py @@ -0,0 +1,6 @@ +def main(arg: "required argument"): + "do something with arg" + print('Got %s' % arg) + +if __name__ == '__main__': + import plac; plac.call(main) # passes sys.argv[1:] to main diff --git a/doc/example1.py b/doc/example1.py new file mode 100644 index 0000000..a5071bb --- /dev/null +++ b/doc/example1.py @@ -0,0 +1,15 @@ +# example1.py +def main(dsn): + "Do something with the database" + print(dsn) + # ... + +if __name__ == '__main__': + import sys + n = len(sys.argv[1:]) + if n == 0: + sys.exit('usage: python %s dsn' % sys.argv[0]) + elif n == 1: + main(sys.argv[1]) + else: + sys.exit('Unrecognized arguments: %s' % ' '.join(sys.argv[2:])) diff --git a/doc/example10.help b/doc/example10.help new file mode 100644 index 0000000..8ac3e9f --- /dev/null +++ b/doc/example10.help @@ -0,0 +1,10 @@ +usage: example10.py [-h] {add,mul} [n [n ...]] + +A script to add and multiply numbers + +positional arguments: + {add,mul} The name of an operator + n A number + +optional arguments: + -h, --help show this help message and exit diff --git a/doc/example10.py b/doc/example10.py new file mode 100644 index 0000000..02bbdc6 --- /dev/null +++ b/doc/example10.py @@ -0,0 +1,20 @@ +# example10.py +import plac + +@plac.annotations( +operator=("The name of an operator", 'positional', None, str, ['add', 'mul']), +numbers=("A number", 'positional', None, float, None, "n")) +def main(operator, *numbers): + "A script to add and multiply numbers" + if operator == 'mul': + op = float.__mul__ + result = 1.0 + else: # operator == 'add' + op = float.__add__ + result = 0.0 + for n in numbers: + result = op(result, n) + return result + +if __name__ == '__main__': + print(plac.call(main)) diff --git a/doc/example11.help b/doc/example11.help new file mode 100644 index 0000000..4d36924 --- /dev/null +++ b/doc/example11.help @@ -0,0 +1,9 @@ +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 diff --git a/doc/example11.py b/doc/example11.py new file mode 100644 index 0000000..07fbd40 --- /dev/null +++ b/doc/example11.py @@ -0,0 +1,13 @@ +# 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) diff --git a/doc/example12.help b/doc/example12.help new file mode 100644 index 0000000..fec1715 --- /dev/null +++ b/doc/example12.help @@ -0,0 +1,9 @@ +usage: example12.py [-h] [-opt OPT] [args [args ...]] [kw [kw ...]] + +positional arguments: + args default arguments + kw keyword arguments + +optional arguments: + -h, --help show this help message and exit + -opt OPT some option diff --git a/doc/example12.py b/doc/example12.py new file mode 100644 index 0000000..56c65a9 --- /dev/null +++ b/doc/example12.py @@ -0,0 +1,18 @@ +# example12.py +import plac + +@plac.annotations( + opt=('some option', 'option'), + args='default arguments', + kw='keyword arguments') +def main(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__': + for output in plac.call(main): + print(output) diff --git a/doc/example13.help b/doc/example13.help new file mode 100644 index 0000000..94401ad --- /dev/null +++ b/doc/example13.help @@ -0,0 +1,9 @@ +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} diff --git a/doc/example13.py b/doc/example13.py new file mode 100644 index 0000000..715f2e1 --- /dev/null +++ b/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): + print(plac.parser_from(self).help_cmd(name)) + + @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/doc/example2.py b/doc/example2.py new file mode 100644 index 0000000..56aa23c --- /dev/null +++ b/doc/example2.py @@ -0,0 +1,12 @@ +# example2.py +def main(dsn): + "Do something on the database" + print(dsn) + # ... + +if __name__ == '__main__': + import argparse + p = argparse.ArgumentParser() + p.add_argument('dsn') + arg = p.parse_args() + main(arg.dsn) diff --git a/doc/example3.help b/doc/example3.help new file mode 100644 index 0000000..dd4d96a --- /dev/null +++ b/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/doc/example3.py b/doc/example3.py new file mode 100644 index 0000000..979b1a4 --- /dev/null +++ b/doc/example3.py @@ -0,0 +1,8 @@ +# example3.py +def main(dsn): + "Do something with the database" + print(dsn) + # ... + +if __name__ == '__main__': + import plac; plac.call(main) diff --git a/doc/example4.py b/doc/example4.py new file mode 100644 index 0000000..2e76b65 --- /dev/null +++ b/doc/example4.py @@ -0,0 +1,15 @@ +# example4.py +from datetime import datetime + +def main(dsn, table='product', today=datetime.today()): + "Do something on the database" + print(dsn, table, today) + +if __name__ == '__main__': + import sys + args = sys.argv[1:] + if not args: + sys.exit('usage: python %s dsn' % sys.argv[0]) + elif len(args) > 2: + sys.exit('Unrecognized arguments: %s' % ' '.join(argv[2:])) + main(*args) diff --git a/doc/example5.help b/doc/example5.help new file mode 100644 index 0000000..0d6a630 --- /dev/null +++ b/doc/example5.help @@ -0,0 +1,11 @@ +usage: example5.py [-h] dsn [table] [today] + +Do something on the database + +positional arguments: + dsn + table + today + +optional arguments: + -h, --help show this help message and exit diff --git a/doc/example5.py b/doc/example5.py new file mode 100644 index 0000000..30c1d87 --- /dev/null +++ b/doc/example5.py @@ -0,0 +1,9 @@ +# example5.py +from datetime import datetime + +def main(dsn, table='product', today=datetime.today()): + "Do something on the database" + print(dsn, table, today) + +if __name__ == '__main__': + import plac; plac.call(main) diff --git a/doc/example6.help b/doc/example6.help new file mode 100644 index 0000000..c950e78 --- /dev/null +++ b/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/doc/example6.py b/doc/example6.py new file mode 100644 index 0000000..045839b --- /dev/null +++ b/doc/example6.py @@ -0,0 +1,6 @@ +# example6.py +def main(dsn, command: ("SQL query", 'option')): + print('executing %r on %s' % (command, dsn)) + +if __name__ == '__main__': + import plac; plac.call(main) diff --git a/doc/example7.help b/doc/example7.help new file mode 100644 index 0000000..0834858 --- /dev/null +++ b/doc/example7.help @@ -0,0 +1,10 @@ +usage: example7.py [-h] dsn [scripts [scripts ...]] + +Run the given scripts on the database + +positional arguments: + dsn + scripts + +optional arguments: + -h, --help show this help message and exit diff --git a/doc/example7.py b/doc/example7.py new file mode 100644 index 0000000..475799d --- /dev/null +++ b/doc/example7.py @@ -0,0 +1,11 @@ +# example7.py +from datetime import datetime + +def main(dsn, *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/doc/example7_.help b/doc/example7_.help new file mode 100644 index 0000000..6f0f4b8 --- /dev/null +++ b/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/doc/example7_.py b/doc/example7_.py new file mode 100644 index 0000000..550a524 --- /dev/null +++ b/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/doc/example8.help b/doc/example8.help new file mode 100644 index 0000000..a566258 --- /dev/null +++ b/doc/example8.help @@ -0,0 +1,9 @@ +usage: example8.py [-h] [-c COMMAND] dsn + +positional arguments: + dsn + +optional arguments: + -h, --help show this help message and exit + -c COMMAND, --command COMMAND + SQL query diff --git a/doc/example8.py b/doc/example8.py new file mode 100644 index 0000000..1dad399 --- /dev/null +++ b/doc/example8.py @@ -0,0 +1,8 @@ +# example8.py +def main(command: ("SQL query", 'option', 'c'), dsn): + if command: + print('executing %s on %s' % (command, dsn)) + # ... + +if __name__ == '__main__': + import plac; plac.call(main) diff --git a/doc/example8_.help b/doc/example8_.help new file mode 100644 index 0000000..872b05a --- /dev/null +++ b/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/doc/example8_.py b/doc/example8_.py new file mode 100644 index 0000000..cdea364 --- /dev/null +++ b/doc/example8_.py @@ -0,0 +1,6 @@ +# example8_.py +def main(dsn, command: ("SQL query", 'option')='select * from table'): + print('executing %r on %s' % (command, dsn)) + +if __name__ == '__main__': + import plac; plac.call(main) diff --git a/doc/example9.help b/doc/example9.help new file mode 100644 index 0000000..cda79b3 --- /dev/null +++ b/doc/example9.help @@ -0,0 +1,8 @@ +usage: example9.py [-h] [-v] dsn + +positional arguments: + dsn connection string + +optional arguments: + -h, --help show this help message and exit + -v, --verbose prints more info diff --git a/doc/example9.py b/doc/example9.py new file mode 100644 index 0000000..8e39eff --- /dev/null +++ b/doc/example9.py @@ -0,0 +1,9 @@ +# example9.py + +def main(verbose: ('prints more info', 'flag', 'v'), dsn: 'connection string'): + if verbose: + print('connecting to %s' % dsn) + # ... + +if __name__ == '__main__': + import plac; plac.call(main) diff --git a/doc/execute.py b/doc/execute.py new file mode 100644 index 0000000..eabf398 --- /dev/null +++ b/doc/execute.py @@ -0,0 +1,5 @@ +import plac +from ishelve import ishelve + +if __name__ == '__main__': + plac.Interpreter(ishelve).execute(file('ishelve2.bat')) diff --git a/doc/importer1.py b/doc/importer1.py new file mode 100644 index 0000000..233f910 --- /dev/null +++ b/doc/importer1.py @@ -0,0 +1,20 @@ +import time +import plac + +class FakeImporter(object): + "A fake importer with an import_file command" + commands = ['import_file'] + def __init__(self, dsn): + self.dsn = dsn + def import_file(self, fname): + "Import a file into the database" + try: + for n in range(10000): + time.sleep(.01) + if n % 100 == 99: + yield 'Imported %d lines' % (n+1) + finally: + print('closing the file') + +if __name__ == '__main__': + plac.Interpreter.call(FakeImporter) diff --git a/doc/importer2.py b/doc/importer2.py new file mode 100644 index 0000000..61a99b1 --- /dev/null +++ b/doc/importer2.py @@ -0,0 +1,22 @@ +import time +import plac + +class FakeImporter(object): + "A fake importer with an import_file command" + thcommands = ['import_file'] + def __init__(self, dsn): + self.dsn = dsn + def import_file(self, fname): + "Import a file into the database" + try: + for n in range(10000): + time.sleep(.02) + if n % 100 == 99: # every two seconds + yield 'Imported %d lines' % (n+1) + if n % 10 == 9: # every 0.2 seconds + yield # go back and check the TOBEKILLED status + finally: + print('closing the file') + +if __name__ == '__main__': + plac.Interpreter.call(FakeImporter) diff --git a/doc/importer3.py b/doc/importer3.py new file mode 100644 index 0000000..2a47e99 --- /dev/null +++ b/doc/importer3.py @@ -0,0 +1,20 @@ +import time +import plac + +class FakeImporter(object): + "A fake importer with an import_file command" + mpcommands = ['import_file'] + def __init__(self, dsn): + self.dsn = dsn + def import_file(self, fname): + "Import a file into the database" + try: + for n in range(10000): + time.sleep(.02) + if n % 100 == 99: + yield 'Imported %d lines' % (n+1) + finally: + print('closing the file') + +if __name__ == '__main__': + plac.Interpreter.call(FakeImporter) diff --git a/doc/importer4.py b/doc/importer4.py new file mode 100644 index 0000000..95ce6f0 --- /dev/null +++ b/doc/importer4.py @@ -0,0 +1,18 @@ +import time +import plac + +class Importer(object): + "A fake importer with an import_file command" + thcommands = ['import_file'] + def __init__(self, dsn): + self.dsn = dsn + def import_file(self, fname): + "Import a file into the database" + for n in range(10000): + time.sleep(.01) + if n % 100 == 99: + yield 'Imported %d lines' % (n+1) + yield + +if __name__ == '__main__': + plac.Interpreter.plac.call(Importer) diff --git a/doc/importer_ui.py b/doc/importer_ui.py new file mode 100644 index 0000000..a4592b0 --- /dev/null +++ b/doc/importer_ui.py @@ -0,0 +1,29 @@ +from Tkinter import * +from importer3 import FakeImporter + +def taskwidget(root, task, tick=500): + "A Label widget showing the output of a task every 500 ms" + sv = StringVar(root) + lb = Label(root, textvariable=sv) + def show_outlist(): + try: + out = task.outlist[-1] + except IndexError: # no output yet + out = '' + sv.set('%s %s' % (task, out)) + root.after(tick, show_outlist) + root.after(0, show_outlist) + return lb + +def monitor(tasks): + root = Tk() + for task in tasks: + task.run() + taskwidget(root, task).pack() + root.mainloop() + +if __name__ == '__main__': + import plac + with plac.Interpreter(plac.call(FakeImporter)) as i: + tasks = [i.submit('import_file f1'), i.submit('import_file f2')] + monitor(tasks) diff --git a/doc/ishelve.help b/doc/ishelve.help new file mode 100644 index 0000000..d68a099 --- /dev/null +++ b/doc/ishelve.help @@ -0,0 +1,18 @@ +usage: ishelve.py [.help] [.showall] [.clear] [.delete DELETE] + [.filename /home/micheles/conf.shelve] + [params [params ...]] [setters [setters ...]] + +Simple interface to a shelve + +positional arguments: + params names of the parameters in the shelve + setters setters param=value + +optional arguments: + .help show help + .showall show all parameters in the shelve + .clear clear the shelve + .delete DELETE delete an element + .filename /home/micheles/conf.shelve + filename of the shelve + diff --git a/doc/ishelve.py b/doc/ishelve.py new file mode 100644 index 0000000..4f22ba2 --- /dev/null +++ b/doc/ishelve.py @@ -0,0 +1,54 @@ +# ishelve.py +import os, shelve, plac + +DEFAULT_SHELVE = os.path.expanduser('~/conf.shelve') + +@plac.annotations( + help=('show help', 'flag'), + showall=('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 main(help, showall, clear, delete, filename=DEFAULT_SHELVE, + *params, **setters): + "A simple interface to a shelve. Use .help to see the available commands." + sh = shelve.open(filename) + try: + if not any([help, showall, clear, delete, params, setters]): + yield 'no arguments passed, use .help to see the available commands' + elif help: # custom help + yield 'Commands: .help, .showall, .clear, .delete' + yield ' ...' + yield ' ...' + elif showall: + 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() + +main.add_help = False # there is a custom help, remove the default one +main.prefix_chars = '.' # use dot-prefixed commands + +if __name__ == '__main__': + for output in plac.call(main): + print(output) diff --git a/doc/ishelve2.help b/doc/ishelve2.help new file mode 100644 index 0000000..ade74c0 --- /dev/null +++ b/doc/ishelve2.help @@ -0,0 +1,8 @@ +usage: ishelve2.py [-h] [-configfile CONFIGFILE] + +A minimal interface over a shelve object. + +optional arguments: + -h, --help show this help message and exit + -configfile CONFIGFILE + path name of the shelve diff --git a/doc/ishelve2.py b/doc/ishelve2.py new file mode 100644 index 0000000..b055b98 --- /dev/null +++ b/doc/ishelve2.py @@ -0,0 +1,43 @@ +# ishelve2.py +import shelve, os, sys, plac + +class ShelveInterface(object): + "A minimal interface over a shelve object." + commands = 'set', 'show', 'showall', 'delete' + @plac.annotations( + configfile=('path name of the shelve', 'option')) + def __init__(self, configfile): + self.configfile = configfile or '~/conf.shelve' + self.fname = os.path.expanduser(self.configfile) + self.__doc__ += '\nOperating on %s.\n.help to see '\ + 'the available commands.\n' % self.fname + def __enter__(self): + self.sh = shelve.open(self.fname) + return self + def __exit__(self, etype, exc, tb): + self.sh.close() + 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' % (name, self.sh[name]) # no error checking + def showall(self): + "show all parameters" + for name in self.sh: + yield '%s = %s' % (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] # no error checking + +main = ShelveInterface # useful for the tests + +if __name__ == '__main__': + plac.Interpreter.call(ShelveInterface) \ No newline at end of file diff --git a/doc/ishelve3.py b/doc/ishelve3.py new file mode 100644 index 0000000..c37178b --- /dev/null +++ b/doc/ishelve3.py @@ -0,0 +1,5 @@ +# ishelve3.py +from ishelve2 import ShelveInterface as main + +if __name__ == '__main__': + import plac; plac.Interpreter.call(main) diff --git a/doc/multiline.py b/doc/multiline.py new file mode 100644 index 0000000..1556437 --- /dev/null +++ b/doc/multiline.py @@ -0,0 +1,10 @@ +import sys +from plac_ext import Process + +if __name__ == '__main__': + proc = Process(['ishelve2.py', '-c', '~/conf.shelve']) + while True: + multiline = ' '.join(iter(sys.stdin.readline, 'END\n')) + if not multiline: + break + print proc.send(multiline) diff --git a/doc/pi_ui.py b/doc/pi_ui.py new file mode 100644 index 0000000..701a846 --- /dev/null +++ b/doc/pi_ui.py @@ -0,0 +1,32 @@ +from Tkinter import * +from picalculator import PiCalculator + +def taskwidget(root, task, tick=500): + "A Label widget showing the output of a task every 500 ms" + sv = StringVar(root) + lb = Label(root, textvariable=sv) + def show_outlist(): + try: + out = task.outlist[-1] + except IndexError: # no output yet + out = '' + sv.set('%s %s' % (task, out)) + root.after(tick, show_outlist) + root.after(0, show_outlist) + return lb + +def monitor(calculator): + tasks = calculator.submit_tasks() + root = Tk() + try: + for task in tasks: + task.run() + taskwidget(root, task).pack() + root.mainloop() + except KeyboardInterrupt: + root.quit() + finally: + calculator.close() + +if __name__ == '__main__': + import plac; monitor(plac.call(PiCalculator)) diff --git a/doc/picalc_ui.py b/doc/picalc_ui.py new file mode 100644 index 0000000..1eb8402 --- /dev/null +++ b/doc/picalc_ui.py @@ -0,0 +1,36 @@ +import time +from Tkinter import * +from threading import Thread +from picalculator import PiCalculator + +def taskwidget(root, task, tick=500): + "A Label widget showing the output of a task every 500 ms" + sv = StringVar(root) + lb = Label(root, textvariable=sv) + def show_outlist(): + try: + out = task.outlist[-1] + except IndexError: # no output yet + out = '' + sv.set('%s %s' % (task, out)) + root.after(tick, show_outlist) + root.after(0, show_outlist) + return lb + +def monitor(calculator): + calculator.submit_tasks() + th = Thread(target=calculator.run) + th.start() + root = Tk() + try: + for task in calculator.i.tasks(): + taskwidget(root, task).pack() + root.mainloop() + except KeyboardInterrupt: + root.quit() + finally: + calculator.close() + th.join() + +if __name__ == '__main__': + import plac; monitor(plac.call(PiCalculator)) diff --git a/doc/picalculator.py b/doc/picalculator.py new file mode 100644 index 0000000..75df2b2 --- /dev/null +++ b/doc/picalculator.py @@ -0,0 +1,63 @@ +from __future__ import with_statement +from random import random +import multiprocessing +import plac + +class PiCalculator(object): + """Compute pi in parallel with threads or processes""" + + @plac.annotations( + npoints=('number of integration points', 'positional', None, int), + mode=('sequential|parallel|threaded', 'option', 'm', str, 'SPT')) + def __init__(self, npoints, mode='S'): + self.npoints = npoints + if mode == 'P': + self.mpcommands = ['calc_pi'] + elif mode == 'T': + self.thcommands = ['calc_pi'] + elif mode == 'S': + self.commands = ['calc_pi'] + self.n_cpu = multiprocessing.cpu_count() + + def submit_tasks(self): + self.i = plac.Interpreter(self).__enter__() + return [self.i.submit('calc_pi %d' % (self.npoints / self.n_cpu)) + for _ in range(self.n_cpu)] + + def close(self): + self.i.close() + + @plac.annotations( + npoints=('npoints', 'positional', None, int)) + def calc_pi(self, npoints): + counts = 0 + for j in xrange(npoints): + n, r = divmod(j, 1000000) + if r == 0: + yield '%dM iterations' % n + x, y = random(), random() + if x*x + y*y < 1: + counts += 1 + yield (4.0 * counts)/npoints + + def run(self): + tasks = self.i.tasks() + for t in tasks: + t.run() + try: + total = 0 + for task in tasks: + total += task.result + except: # the task was killed + print tasks + return + return total / self.n_cpu + +if __name__ == '__main__': + pc = plac.call(PiCalculator) + pc.submit_tasks() + try: + import time; t0 = time.time() + print '%f in %f seconds ' % (pc.run(), time.time() - t0) + finally: + pc.close() diff --git a/doc/plac.html b/doc/plac.html new file mode 100644 index 0000000..06cfceb --- /dev/null +++ b/doc/plac.html @@ -0,0 +1,2967 @@ + + + + + + + + + + +
+ + +
+

Plac: Parsing the Command Line the Easy Way

+ +++ + + + + + + + + + + + + + + + + + +
Author:Michele Simionato
E-mail:michele.simionato@gmail.com
Date:August 2010
Download page:http://pypi.python.org/pypi/plac
Project page:http://micheles.googlecode.com/hg/plac/doc/plac.html
Requires:Python 2.3+
Installation:easy_install -U plac
License:BSD license
+ +
+

The importance of scaling down

+

There is no want of command line arguments parsers in the Python +world. The standard library alone contains three different modules: +getopt (from the stone age), +optparse (from Python 2.3) and argparse (from Python 2.7). All of +them are quite powerful and especially argparse is an industrial +strength solution; unfortunately, all of them feature a non-zero learning +curve and a certain verbosity. They do not scale down well, at +least in my opinion.

+

It should not be necessary to stress the importance scaling down; +nevertheless, a lot of people are obsessed with features and concerned with +the possibility of scaling up, forgetting the equally important +issue of scaling down. This is an old meme in +the computing world: programs should address the common cases simply and +simple things should be kept simple, while at the same keeping +difficult things possible. plac adhere as much as possible to this +philosophy and it is designed to handle well the simple cases, while +retaining the ability to handle complex cases by relying on the +underlying power of argparse.

+

Technically plac is just a simple wrapper over argparse which hides +most of its complexity by using a declarative interface: the argument +parser is inferred rather than written down by imperatively. Still, plac is +surprisingly scalable upwards, even without using the underlying +argparse. I have been using Python for 8 years and in my experience +it is extremely unlikely that you will ever need to go beyond the +features provided by the declarative interface of plac: they should +be more than enough for 99.9% of the use cases.

+

plac is targetting especially unsophisticated users, +programmers, sys-admins, scientists and in general people writing +throw-away scripts for themselves, choosing the command line +interface because it is the quick and simple. Such users are not +interested in features, they are interested in a small learning curve: +they just want to be able to write a simple command line tool from a +simple specification, not to build a command-line parser by +hand. Unfortunately, the modules in the standard library forces them +to go the hard way. They are designed to implement power user tools +and they have a non-trivial learning curve. On the contrary, plac +is designed to be simple to use and extremely concise, as the examples +below will show.

+
+
+

Scripts with required arguments

+

Let me start with the simplest possible thing: a script that takes a +single argument and does something to it. It cannot get simpler +than that, unless you consider a script without command-line +arguments, where there is nothing to parse. Still, it is a use +case extremely common: I need to write scripts like that nearly +every day, I wrote hundreds of them in the last few years and I have +never been happy. Here is a typical example of code I have been +writing by hand for years:

+
+# example1.py
+def main(dsn):
+    "Do something with the database"
+    print(dsn)
+    # ...
+
+if __name__ == '__main__':
+    import sys
+    n = len(sys.argv[1:])
+    if n == 0:
+        sys.exit('usage: python %s dsn' % sys.argv[0])
+    elif n == 1:
+        main(sys.argv[1])
+    else:
+        sys.exit('Unrecognized arguments: %s' % ' '.join(sys.argv[2:]))
+
+
+

As you see the whole if __name__ == '__main__' block (nine lines) +is essentially boilerplate that should not exist. Actually I think +the language should recognize the main function and pass to it the +command-line arguments automatically; unfortunaly this is unlikely to +happen. I have been writing boilerplate like this in hundreds of +scripts for years, and every time I hate it. The purpose of using a +scripting language is convenience and trivial things should be +trivial. Unfortunately the standard library does not help for this +incredibly common use case. Using getopt and optparse does not help, +since they are intended to manage options and not positional +arguments; the argparse module helps a bit and it is able to reduce +the boilerplate from nine lines to six lines:

+
+# example2.py
+def main(dsn):
+    "Do something on the database"
+    print(dsn)
+    # ...
+
+if __name__ == '__main__':
+    import argparse
+    p = argparse.ArgumentParser()
+    p.add_argument('dsn')
+    arg = p.parse_args()
+    main(arg.dsn)
+
+
+

However saving three lines does not justify introducing the external +dependency: most people will not switch to Python 2.7, which at the time of +this writing is just about to be released, for many years. +Moreover, it just feels too complex to instantiate a class and to +define a parser by hand for such a trivial task.

+

The plac module is designed to manage well such use cases, and it is able +to reduce the original nine lines of boiler plate to two lines. With the +plac module all you need to write is

+
+# example3.py
+def main(dsn):
+    "Do something with the database"
+    print(dsn)
+    # ...
+ 
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+

The plac module provides for free (actually the work is done by the +underlying argparse module) a nice usage message:

+
+$ python example3.py -h
+
+
+usage: example3.py [-h] dsn
+
+Do something with the database
+
+positional arguments:
+  dsn
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+

Moreover plac manages the case of missing arguments and of too many arguments. +This is only the tip of the iceberg: plac is able to do much more than that.

+
+
+

Scripts with default arguments

+

The need to have suitable defaults for command-line scripts is quite +common. For instance I have encountered this use case at work hundreds +of times:

+
+# example4.py
+from datetime import datetime
+
+def main(dsn, table='product', today=datetime.today()):
+    "Do something on the database"
+    print(dsn, table, today)
+
+if __name__ == '__main__':
+    import sys
+    args = sys.argv[1:]
+    if not args:
+        sys.exit('usage: python %s dsn' % sys.argv[0])
+    elif len(args) > 2:
+        sys.exit('Unrecognized arguments: %s' % ' '.join(argv[2:]))
+    main(*args)
+
+
+

Here I want to perform a query on a database table, by extracting the +most recent data: it makes sense for today to be a default argument. +If there is a most used table (in this example a table called 'product') +it also makes sense to make it a default argument. Performing the parsing +of the command-line arguments by hand takes 8 ugly lines of boilerplate +(using argparse would require about the same number of lines). +With plac the entire __main__ block reduces to the usual two lines:

+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+

In other words, six lines of boilerplate have been removed, and we get +the usage message for free:

+
+usage: example5.py [-h] dsn [table] [today]
+
+Do something on the database
+
+positional arguments:
+  dsn
+  table
+  today
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+

plac manages transparently even the case when you want to pass a +variable number of arguments. Here is an example, a script running +on a database a series of SQL scripts:

+
+# example7.py
+from datetime import datetime
+
+def main(dsn, *scripts):
+    "Run the given scripts on the database"
+    for script in scripts:
+        print('executing %s' % script)
+        # ...
+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+

Here is the usage message:

+
+usage: example7.py [-h] dsn [scripts [scripts ...]]
+
+Run the given scripts on the database
+
+positional arguments:
+  dsn
+  scripts
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+

The examples here should have made clear that plac is able to figure out +the command-line arguments parser to use from the signature of the main +function. This is the whole idea behind plac: if the intent is clear, +let's the machine take care of the details.

+

plac is inspired to an old Python Cookbook recipe (optionparse), in +the sense that it delivers the programmer from the burden of writing +the parser, but is less of a hack: instead of extracting the parser +from the docstring of the module, it extracts it from the signature of +the main function.

+

The idea comes from the function annotations concept, a new +feature of Python 3. An example is worth a thousand words, so here +it is:

+
+# 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)
+
+
+

Here the arguments of the main function have been annotated with +strings which are intented to be used in the help message:

+
+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
+
+
+

plac is able to recognize much more complex annotations, as +I will show in the next paragraphs.

+
+
+

Scripts with options (and smart options)

+

It is surprising how few command-line scripts with options I have +written over the years (probably less than a hundred), compared to the +number of scripts with positional arguments I wrote (certainly more +than a thousand of them). Still, this use case cannot be neglected. +The standard library modules (all of them) are quite verbose when it +comes to specifying the options and frankly I have never used them +directly. Instead, I have always relied on the +optionparse recipe, which provides a convenient wrapper over +optionparse. Alternatively, in the simplest cases, I have just +performed the parsing by hand. In plac the parser is inferred by the +function annotations. Here is an example:

+
+# example8.py
+def main(command: ("SQL query", 'option', 'c'), dsn):
+    if command:
+        print('executing %s on %s' % (command, dsn))
+        # ...
+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+

Here the argument command has been annotated with the tuple +("SQL query", 'option', 'c'): the first string is the help string +which will appear in the usage message, the second string tells plac +that command is an option and the third string that there is also +a short form of the option -c, the long form being --command. +The usage message is the following:

+
+usage: example8.py [-h] [-c COMMAND] dsn
+
+positional arguments:
+  dsn
+
+optional arguments:
+  -h, --help            show this help message and exit
+  -c COMMAND, --command COMMAND
+                        SQL query
+
+
+

Here are two examples of usage:

+
+$ python3 example8.py -c"select * from table" dsn
+executing select * from table on dsn
+
+$ python3 example8.py --command="select * from table" dsn
+executing select * from table on dsn
+
+

The third argument in the function annotation can be omitted: in such +case it will be assumed to be None. The consequence is that +the usual dichotomy between long and short options (GNU-style options) +disappears: we get smart options, which have the single character prefix +of short options and behave like both long and short options, since +they can be abbreviated. Here is an example featuring smart options:

+
+# example6.py
+def main(dsn, command: ("SQL query", 'option')):
+    print('executing %r on %s' % (command, dsn))
+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+
+usage: example6.py [-h] [-command COMMAND] dsn
+
+positional arguments:
+  dsn
+
+optional arguments:
+  -h, --help        show this help message and exit
+  -command COMMAND  SQL query
+
+
+

The following are all valid invocations ot the script:

+
+$ python3 example6.py -c "select" dsn
+executing 'select' on dsn
+$ python3 example6.py -com "select" dsn
+executing 'select' on dsn
+$ python3 example6.py -command="select" dsn
+executing 'select' on dsn
+
+

Notice that the form -command=SQL is recognized only for the full +option, not for its abbreviations:

+
+$ python3 example6.py -com="select" dsn
+usage: example6.py [-h] [-command COMMAND] dsn
+example6.py: error: unrecognized arguments: -com=select
+
+

If the option is not passed, the variable command +will get the value None. However, it is possible to specify a non-trivial +default. Here is an example:

+
+# example8_.py
+def main(dsn, command: ("SQL query", 'option')='select * from table'):
+    print('executing %r on %s' % (command, dsn))
+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+

Notice that the default value appears in the help message:

+
+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
+
+
+

When you run the script and you do not pass the -command option, the +default query will be executed:

+
+$ python3 example8_.py dsn
+executing 'select * from table' on dsn
+
+
+
+

Scripts with flags

+

plac is able to recognize flags, i.e. boolean options which are +True if they are passed to the command line and False +if they are absent. Here is an example:

+
+# example9.py
+
+def main(verbose: ('prints more info', 'flag', 'v'), dsn: 'connection string'):
+    if verbose:
+        print('connecting to %s' % dsn)
+    # ...
+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+
+usage: example9.py [-h] [-v] dsn
+
+positional arguments:
+  dsn            connection string
+
+optional arguments:
+  -h, --help     show this help message and exit
+  -v, --verbose  prints more info
+
+
+
+$ python3 example9.py -v dsn
+connecting to dsn
+
+

Notice that it is an error trying to specify a default for flags: the +default value for a flag is always False. If you feel the need to +implement non-boolean flags, you should use an option with two +choices, as explained in the "more features" section.

+

For consistency with the way the usage message is printed, I suggest +you to follow the Flag-Option-Required-Default (FORD) convention: in +the main function write first the flag arguments, then the option +arguments, then the required arguments and finally the default +arguments. This is just a convention and you are not forced to use it, +except for the default arguments (including the varargs) which must +stay at the end as it is required by the Python syntax.

+

I also suggests to specify a one-character abbreviation for flags: in +this way you can use the GNU-style composition of flags (i.e. -zxvf +is an abbreviation of -z -x -v -f). I usually do not provide +the one-character abbreviation for options, since it does not make sense +to compose them.

+
+
+

plac for Python 2.X users

+

I do not use Python 3. At work we are just starting to think about +migrating to Python 2.6. It will take years before we +think to migrate to Python 3. I am pretty much sure most Pythonistas +are in the same situation. Therefore plac provides a way to work +with function annotations even in Python 2.X (including Python 2.3). +There is no magic involved; you just need to add the annotations +by hand. For instance the annotated function declaration

+
+def main(dsn: "Database dsn", *scripts: "SQL scripts"):
+    ...
+
+

is equivalent to the following code:

+
+def main(dsn, *scripts):
+    ...
+main.__annotations__ = dict(
+    dsn="Database dsn",
+    scripts="SQL scripts")
+
+

One should be careful to match the keys of the annotation dictionary +with the names of the arguments in the annotated function; for lazy +people with Python 2.4 available the simplest way is to use the +plac.annotations decorator that performs the check for you:

+
+@plac.annotations(
+    dsn="Database dsn",
+    scripts="SQL scripts")
+def main(dsn, *scripts):
+    ...
+
+

In the rest of this article I will assume that you are using Python 2.X with +X >= 4 and I will use the plac.annotations decorator. Notice however +that the core features of plac run even on Python 2.3.

+
+
+

More features

+

One of the goals of plac is to have a learning curve of minutes for +its core features, compared to the learning curve of hours of +argparse. In order to reach this goal, I have not sacrificed all +the features of argparse. Actually a lot of argparse power persists +in plac. Until now, I have only showed simple annotations, but in +general an annotation is a 6-tuple of the form

+
+(help, kind, abbrev, type, choices, metavar)
+

where help is the help message, kind is a string in the set { +"flag", "option", "positional"}, abbrev is a +one-character string or None, type is a callable taking a +string in input, +choices is a discrete sequence of values and metavar is a string.

+

type is used to automagically convert the command line arguments +from the string type to any Python type; by default there is no +conversion and type=None.

+

choices is used to restrict the number of the valid +options; by default there is no restriction i.e. choices=None.

+

metavar has two meanings. For a positional argument it is used to +change the argument name in the usage message (and only there). By +default the metavar is None and the name in the usage message is +the same as the argument name. For an option +the metavar is used differently in the usage message, which has +now the form [--option-name METAVAR]. If the metavar is None, +then it is equal to the uppercased name of the argument, unless the +argument has a default and in such a case is equal to the stringified +form of the default.

+

Here is an example showing many of the features (copied from the +argparse documentation):

+
+# example10.py
+import plac
+
+@plac.annotations(
+operator=("The name of an operator", 'positional', None, str, ['add', 'mul']),
+numbers=("A number", 'positional', None, float, None, "n"))
+def main(operator, *numbers):
+    "A script to add and multiply numbers"
+    if operator == 'mul':
+        op = float.__mul__
+        result = 1.0
+    else: # operator == 'add'
+        op = float.__add__
+        result = 0.0
+    for n in numbers:
+        result = op(result, n)
+    return result
+
+if __name__ == '__main__':
+    print(plac.call(main))
+
+
+

Here is the usage:

+
+usage: example10.py [-h] {add,mul} [n [n ...]]
+
+A script to add and multiply numbers
+
+positional arguments:
+  {add,mul}   The name of an operator
+  n           A number
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+

Notice that the docstring of the main function has been automatically added +to the usage message. Here are a couple of examples of use:

+
+$ python example10.py add 1 2 3 4
+10.0
+$ python example10.py mul 1 2 3 4
+24.0
+$ python example10.py ad 1 2 3 4 # a mispelling error
+usage: example10.py [-h] {add,mul} [n [n ...]]
+example10.py: error: argument operator: invalid choice: 'ad' (choose from 'add', 'mul')
+
+

plac.call can also be used in doctests like this:

+
+>>> import plac, example10
+>>> plac.call(example10.main, ['add', '1', '2'])
+3.0
+
+

plac.call works for generators too:

+
+>>> def main(n):
+...     for i in range(int(n)):
+...         yield i
+>>> plac.call(main, ['3'])
+[0, 1, 2]
+
+

Internally plac.call tries to convert the output of the main function +into a list, if possible. If the output is not iterable or it is a +string, it is left unchanged, but if it is iterable it is converted. +In particular, generator objects are exhausted by plac.call.

+

This behavior avoids mistakes like forgetting of applying +list(result) to the result of plac.call; moreover it makes +errors visible early, and avoids mistakes in code like the following:

+
+try:
+    result = plac.call(main, args)
+except:
+   # do something
+
+

Without the "listify" functionality, a main function returning a +generator object would not raise any exception until the generator +is iterated over.

+

If you are a fan of lazyness, you can still have it by setting the eager +flag to False, as in the following example:

+
+for line in plac.call(main, args, eager=False):
+    print(line)
+
+

If main returns a generator object this example will print each +line as soon as available, whereas the default behaviour is to print +all the lines together and the end of the computation.

+
+
+

A realistic example

+

Here is a more realistic script using most of the features of plac to +run SQL queries on a database by relying on SQLAlchemy. Notice the usage +of the type feature to automagically convert a SQLAlchemy connection +string into a SqlSoup object:

+
+# dbcli.py
+import plac
+from sqlalchemy.ext.sqlsoup import SqlSoup
+
+@plac.annotations(
+    db=("Connection string", 'positional', None, SqlSoup),
+    header=("Header", 'flag', 'H'),
+    sqlcmd=("SQL command", 'option', 'c', str, None, "SQL"),
+    delimiter=("Column separator", 'option', 'd'),
+    scripts="SQL scripts",
+    )
+def main(db, header, sqlcmd, delimiter="|", *scripts):
+    "A script to run queries and SQL scripts on a database"
+    yield 'Working on %s' % db.bind.url
+
+    if sqlcmd:
+        result = db.bind.execute(sqlcmd)
+        if header: # print the header
+            yield delimiter.join(result.keys())
+        for row in result: # print the rows
+            yield delimiter.join(map(str, row))
+
+    for script in scripts:
+        db.bind.execute(file(script).read())
+        yield 'executed %s' % script
+
+if __name__ == '__main__':
+    for output in plac.call(main):
+        print(output)
+
+
+

You can see the yield-is-print pattern here: instead of using +print in the main function, I use yield, and I perform the +print in the __main__ block. The advantage of the pattern is that +tests invoking plac.call and checking the result become trivial: +had I performed the printing in the main function, the test would have +involved an ugly hack like redirecting sys.stdout to a +StringIO object.

+

Here is the usage message:

+
+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
+
+optional arguments:
+  -h, --help            show this help message and exit
+  -H, --header          Header
+  -c SQL, --sqlcmd SQL  SQL command
+  -d |, --delimiter |   Column separator
+
+
+

You can check for yourself that the script works.

+
+
+

Keyword arguments

+

Starting from release 0.4, plac supports keyword arguments. In +practice that means that if your main function has keyword arguments, +plac treats specially arguments of the form "name=value" in the +command line. Here is an example:

+
+# example12.py
+import plac
+
+@plac.annotations(
+   opt=('some option', 'option'),
+   args='default arguments',
+   kw='keyword arguments')
+def main(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__':
+    for output in plac.call(main):
+       print(output)
+
+
+

Here is the generated usage message:

+
+usage: example12.py [-h] [-opt OPT] [args [args ...]] [kw [kw ...]]
+
+positional arguments:
+  args        default arguments
+  kw          keyword arguments
+
+optional arguments:
+  -h, --help  show this help message and exit
+  -opt OPT    some option
+
+
+

Here is how you call the script:

+
+$ python example12.py -o X a1 a2 name=value
+opt=X
+args=('a1', 'a2')
+kw={'name': 'value'}
+
+

When using keyword arguments, one must be careful to use names which +are not alreay taken; for instance in this examples the name opt +is taken:

+
+$ python example12.py 1 2 kw1=1 kw2=2 opt=0
+usage: example12.py [-h] [-o OPT] [args [args ...]] [kw [kw ...]]
+example12.py: error: colliding keyword arguments: opt
+
+

The names taken are the names of the flags, of the options, and of the +positional arguments, excepted varargs and keywords. This limitation +is a consequence of the way the argument names are managed in function calls +by the Python language.

+
+
+

Final example: a shelve interface

+

Here is a less trivial example for the keyword arguments feature. +The use case is the following: suppose we have stored the +configuration parameters of a given application into a Python shelve +and we need a command-line tool to edit the shelve. +A possible implementation using plac could be the following:

+
+# ishelve.py
+import os, shelve, plac
+
+DEFAULT_SHELVE = os.path.expanduser('~/conf.shelve')
+
+@plac.annotations(
+    help=('show help', 'flag'),
+    showall=('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 main(help, showall, clear, delete, filename=DEFAULT_SHELVE,
+         *params, **setters):
+    "A simple interface to a shelve. Use .help to see the available commands."
+    sh = shelve.open(filename)
+    try:
+        if not any([help, showall, clear, delete, params, setters]):
+            yield 'no arguments passed, use .help to see the available commands'
+        elif help: # custom help
+            yield 'Commands: .help, .showall, .clear, .delete'
+            yield '<param> ...'
+            yield '<param=value> ...'
+        elif showall:
+            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()
+
+main.add_help = False # there is a custom help, remove the default one
+main.prefix_chars = '.' # use dot-prefixed commands
+
+if __name__ == '__main__':
+    for output in plac.call(main):
+        print(output)
+
+
+

A few notes are in order:

+
    +
  1. I have disabled the ordinary help provided by argparse and I have +implemented a custom help command.
  2. +
  3. I have changed the prefix character used to recognize the options +to a dot.
  4. +
  5. Keyword arguments recognition (in the **setters) is used to make it +possible to store a value in the shelve with the syntax +param_name=param_value.
  6. +
  7. *params are used to retrieve parameters from the shelve and some +error checking is performed in the case of missing parameters
  8. +
  9. A command to clear the shelve is implemented as a flag (.clear).
  10. +
  11. A command to delete a given parameter is implemented as an option +(.delete).
  12. +
  13. There is an option with default (.filename=conf.shelve) to store +the filename of the shelve.
  14. +
  15. All things considered, the code looks like a poor man object oriented +interface implemented with a chain of elifs instead of methods. Of course, +plac can do better than that, but let me start from a low-level approach +first.
  16. +
+

If you run ishelve.py without arguments you get the following +message:

+
+$ python ishelve.py
+no arguments passed, use .help to see the available commands
+
+

If you run ishelve.py with the option .h (or any abbreviation +of .help) you get:

+
+$ python ishelve.py .h
+Commands: .help, .showall, .clear, .delete
+<param> ...
+<param=value> ...
+
+

You can check by hand that the tool work:

+
+$ python ishelve.py .clear # start from an empty shelve
+cleared the shelve
+$ python ishelve.py a=1 b=2
+setting a=1
+setting b=2
+$ python ishelve.py .showall
+b=2
+a=1
+$ python ishelve.py .del b # abbreviation for .delete
+deleted b
+$ python ishelve.py a
+1
+$ python ishelve.py b
+b: not found
+$ python ishelve.py .cler # mispelled command
+usage: ishelve.py [.help] [.showall] [.clear] [.delete DELETE]
+                  [.filename /home/micheles/conf.shelve]
+                  [params [params ...]] [setters [setters ...]]
+ishelve.py: error: unrecognized arguments: .cler
+
+
+
+

plac vs argparse

+

plac is opinionated and by design it does not try to make available +all of the features of argparse in an easy way. In particular you +should be aware of the following limitations/differences (the +following assumes knowledge of argparse):

+
    +
  • plac does not support the destination concept: the destination +coincides with the name of the argument, always. This restriction +has some drawbacks. For instance, suppose you want to define a long +option called --yield. In this case the destination would be yield, +which is a Python keyword, and since you cannot introduce an +argument with that name in a function definition, it is impossible +to implement it. Your choices are to change the name of the long +option, or to use argparse with a suitable destination.
  • +
  • plac does not support "required options". As the argparse +documentation puts it: Required options are generally considered bad +form - normal users expect options to be optional. You should avoid +the use of required options whenever possible. Notice that since +argparse supports them, plac can manage them too, but not directly.
  • +
  • plac supports only regular boolean flags. argparse has the ability to +define generalized two-value flags with values different from True +and False. An earlier version of plac had this feature too, but +since you can use options with two choices instead, and in any case +the conversion from {True, False} to any couple of values +can be trivially implemented with a ternary operator +(value1 if flag else value2), I have removed it (KISS rules!).
  • +
  • plac does not support nargs options directly (it uses them internally, +though, to implement flag recognition). The reason it that all the use +cases of interest to me are covered by plac and did not feel the need +to increase the learning curve by adding direct support for nargs.
  • +
  • plac does support subparsers, but you must read the advanced usage +document to see how it works.
  • +
  • plac does not support actions directly. This also +looks like a feature too advanced for the goals of plac. Notice however +that the ability to define your own annotation objects (again, see +the advanced usage document) may mitigate the need for custom actions.
  • +
+

plac can leverage directly on many argparse features.

+

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
+
+

disables the recognition of the help flag -h, --help. This +mechanism does not look particularly elegant, but it works well +enough. I assume that 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='/-'
+
+

The first prefix char (/) is used +as the default for the recognition of options and flags; +the second prefix char (-) is kept to keep the -h/--help option +working: however you can disable it and reimplement it, if you like, +as seen in the ishelve example.

+

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)) #doctest: +ELLIPSIS
+ArgumentParser(prog=...)
+
+

Internally plac.call uses plac.parser_from and adds the parser +to the main function as an attribute. When plac.call(func) is +invoked multiple time, the parser is re-used and not rebuilt from scratch again.

+

I use plac.parser_from in the unit tests of the module, but regular +users should not need to use it, unless they want to access all +of the features of argparse directly without calling the main function.

+

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 except the addition of the .p +attribute.

+
+
+

plac vs the rest of the world

+

Originally plac boasted about being "the easiest command-line +arguments parser in the world". Since then, people started pointing +out to me various projects which are based on the same idea +(extracting the parser from the main function signature) and are +arguably even easier than plac:

+ +

Luckily for me none of such projects had the idea of using +function annotations and argparse; as a consequence, they are +no match for the capabilities of plac.

+

Of course, there are tons of other libraries to parse the command +line. For instance Clap by Matthew Frazier which appeared on PyPI +just the day before plac; Clap is fine but it is certainly not +easier than plac.

+

plac can also be used as a replacement of the cmd module in the standard +library and as such it shares many features with the module cmd2 by +Catherine Devlin. However, this is completely coincidental, since I became +aware of the cmd2 module only after writing plac.

+
+
+

The future

+

Currently the core of plac is around 200 lines of code, not counting blanks, +comments and docstrings. I do not plan to extend the core much in the +future. The idea is to keep the module short: it is and it should +remain a little wrapper over argparse. Actually I have thought about +contributing the core back to argparse if plac becomes successfull +and gains a reasonable number of users. For the moment it should be +considered in alpha status.

+

Notice that even if plac has been designed to be simple to use for +simple stuff, its power should not be underestimated; it is actually a +quite advanced tool with a domain of applicability which far exceeds +the realm of command-line arguments parsers.

+

Version 0.5 of plac doubled the code base and the documentation: it is +based on the idea of using plac to implement command-line interpreters, +i.e. something akin to the cmd module in the standard library, only better. +The new features of plac are described in the advanced usage document . +They are implemented in a separated module (plac_ext.py), since +they require Python 2.5 to work, whereas plac_core.py only requires +Python 2.3.

+
+
+

Trivia: the story behind the name

+

The plac project started very humbly: I just wanted to make +easy_installable my old optionparse recipe, and to publish it on PyPI. +The original name of plac was optionparser and the idea behind it was +to build an OptionParser object from the docstring of the module. +However, before doing that, I decided to check out the argparse module, +since I knew it was going into Python 2.7 and Python 2.7 was coming out. +Soon enough I realized two things:

+
    +
  1. the single greatest idea of argparse was unifying the positional arguments +and the options in a single namespace object;
  2. +
  3. parsing the docstring was so old-fashioned, considering the existence +of functions annotations in Python 3.
  4. +
+

Putting together these two observations with the original idea of inferring the +parser I decided to build an ArgumentParser object from function +annotations. The optionparser name was ruled out, since I was +now using argparse; a name like argparse_plus was also ruled out, +since the typical usage was completely different from the argparse usage.

+

I made a research on PyPI and the name clap (Command Line Arguments Parser) +was not taken, so I renamed everything to clap. After two days +a Clap module appeared on PyPI <expletives deleted>!

+

Having little imagination, I decided to rename everything again to plac, +an anagram of clap: since it is a non-existing English name, I hope nobody +will steal it from me!

+

That's all, I hope you will enjoy working with plac!

+
+
+
+

Advanced usages of plac

+
+

Introduction

+

One of the design goals of plac is to make it dead easy to write a +scriptable and testable interface for an application. You can use +plac whenever you have an API with strings in input and strings in +output, and that includes a huge domain of applications.

+

A string-oriented interface is a scriptable interface by +construction. That means that you can define a command language for +your application and that it is possible to write scripts which are +interpretable by plac and can be run as batch scripts.

+

Actually, at the most general level, you can see plac as a generic tool to +write domain specific languages (DSL). With plac you +can test your application interactively as well as with batch +scripts, and even with the analogous of Python doctests for your +defined language.

+

You can easily replace the cmd module of the standard library and +you could easily write an application like twill with plac. Or you +could use it to script your building procedure. plac also supports +parallel execution of multiple commands and can be used as +task manager and monitor. It is also quite easy to build a GUI +or a Web application on top of plac. When speaking of things +you can do with plac, your imagination is the only limit!

+
+
+

From scripts to interactive applications

+

Command-line scripts have many advantages, but they are no substitute +for interactive applications. +In particular, if you have a script with a large startup time which must be run +multiple times, it is best to turn it into an interactive application, +so that the startup is performed only once. plac provides an +Interpreter class just for this purpose.

+

The Interpreter class wraps the main function of a script and +provides an .interact method to start an interactive interpreter +reading commands from the console.

+

For instance, you can define an interactive interpreter on top of the +ishelve script introduced in the basic documentation as +follows:

+
+# shelve_interpreter.py
+import plac, ishelve
+
+@plac.annotations(
+    interactive=('start interactive interface', 'flag'),
+    subcommands='the commands of the underlying ishelve interpreter')
+def main(interactive, *subcommands):
+    """
+    This script works both interactively and non-interactively.
+    Use .help to see the internal commands.
+    """
+    if interactive:
+        plac.Interpreter(ishelve.main).interact()
+    else:
+        for out in plac.call(ishelve.main, subcommands):
+            print(out)
+
+if __name__ == '__main__':
+    plac.call(main)
+
+
+

A trick has been used here: the ishelve command-line interface has been +hidden inside an external interface. They are distinct: for instance +the external interface recognizes the -h/--help flag whereas the +internal interface only recognizes the .help command:

+
+$ python shelve_interpreter.py -h
+
+
+usage: shelve_interpreter.py [-h] [-interactive]
+                             [subcommands [subcommands ...]]
+
+    This script works both interactively and non-interactively.
+    Use .help to see the internal commands.
+    
+
+positional arguments:
+  subcommands   the commands of the underlying ishelve interpreter
+
+optional arguments:
+  -h, --help    show this help message and exit
+  -interactive  start interactive interface
+
+
+

Thanks to this ingenuous trick, the script can be run both interactively +and non-interactively:

+
+$ python shelve_interpreter.py .clear # non-interactive use
+cleared the shelve
+
+

Here is an usage session:

+
+$ python shelve_interpreter.py -i # interactive use
+A simple interface to a shelve. Use .help to see the available commands.
+i> .help
+Commands: .help, .showall, .clear, .delete
+<param> ...
+<param=value> ...
+i> a=1
+setting a=1
+i> a
+1
+i> b=2
+setting b=2
+i> a b
+1
+2
+i> .del a
+deleted a
+i> a
+a: not found
+i> .show
+b=2
+i> [CTRL-D]
+
+

The .interact method +reads commands from the console and send them to the +underlying interpreter, until the user send a CTRL-D +command (CTRL-Z in Windows). There is a default +argument prompt='i> ' which +can be used to change the prompt. The text displayed at the beginning +of the interactive session is the docstring of the main function. +plac also understands command abbreviations: in this example +del is an abbreviation for delete. In case of ambiguous +abbreviations plac raises a NameError.

+

Finally I must notice that the plac.Interpreter is available only if you +are using a recent version of Python (>= 2.5), because it is a context +manager object which uses extended generators internally.

+
+
+

Testing a plac application

+

You can conveniently test your application in interactive mode. +However manual testing is a poor substitute for automatic testing.

+

In principle, one could write automatic tests for the +ishelve application by using plac.call directly:

+
+# test_ishelve.py
+import plac, ishelve
+
+def test():
+    assert plac.call(ishelve.main, ['.clear']) == ['cleared the shelve']
+    assert plac.call(ishelve.main, ['a=1']) == ['setting a=1']
+    assert plac.call(ishelve.main, ['a']) == ['1']
+    assert plac.call(ishelve.main, ['.delete=a']) == ['deleted a']
+    assert plac.call(ishelve.main, ['a']) == ['a: not found']
+
+if __name__ == '__main__':
+    test()
+
+
+

However, using plac.call is not especially nice. The big +issue is that plac.call responds to invalid input by printing an +error message on stderr and by raising a SystemExit: this is +certainly not a nice thing to do in a test.

+

As a consequence of this behavior it is impossible to test for invalid +commands, unless you wrap the SystemExit exception by +hand each time (a possibly you do something with the error message in +stderr too). Luckily, plac offers a better testing support through +the check method of Interpreter objects:

+
+# test_ishelve_more.py
+from __future__ import with_statement
+import plac, ishelve
+
+def test():
+    with plac.Interpreter(ishelve.main) as i:
+        i.check('.clear', 'cleared the shelve')
+        i.check('a=1', 'setting a=1')
+        i.check('a', '1')
+        i.check('.delete=a', 'deleted a')
+        i.check('a', 'a: not found')
+
+
+

The method .check(given_input, expected_output) works on strings +and raises an AssertionError if the output produced by the +interpreter is different from the expected output for the given input. +Notice that AssertionError is catched by tools like py.test and +nosetests and actually plac tests are intended to be run with +such tools.

+

Interpreters offer a minor syntactic advantage with respect to calling +plac.call directly, but they offer a major semantic advantage when things +go wrong (read exceptions): an Interpreter object internally invokes +something like plac.call, but it wraps all exceptions, so that i.check +is guaranteed not to raise any exception except AssertionError.

+

Even the SystemExit exception is captured and you can write your test as

+
+i.check('-cler', 'SystemExit: unrecognized arguments: -cler')
+

without risk of exiting from the Python interpreter.

+

There is a second advantage of interpreters: if the main function contains some +initialization code and finalization code +(__enter__ and __exit__ functions) they will be run only +once at the beginning and at the end of the interpreter loop. +plac.call instead ignores the initialization/finalization code.

+
+
+

Plac easy tests

+

Writing your tests in terms of Interpreter.check is certainly an +improvement over writing them in terms of plac.call, but they +are still too low-level for my taste. The Interpreter class provides +support for doctest-style tests, a.k.a. plac easy tests.

+

By using plac easy tests you can cut and paste your interactive session and +turn it into a runnable automatics test. +Consider for instance the following file ishelve.placet (the .placet +extension is a mnemonic for plac easy tests):

+
+#!ishelve.py
+i> .clear # start from a clean state
+cleared the shelve
+i> a=1
+setting a=1
+i> a
+1
+i> .del a
+deleted a
+i> a
+a: not found
+i> .cler # spelling error
+.cler: not found
+
+
+

Notice the precence of the shebang line containing the name of the +plac tool to test (a plac tool is just a Python module with a +function called main). The shebang is ignored by the interpreter +(it looks like a comment to it) but it is there so that external +tools (say a test runner) can infer the plac interpreter +to use to test the file.

+

You can test ishelve.placet file by calling the +.doctest method of the interpreter, as in this example:

+
+$ python -c"import plac, ishelve
+plac.Interpreter(ishelve.main).doctest(open('ishelve.placet'), verbose=True)"
+
+

Internally Interpreter.doctests invokes something like Interpreter.check +multiple times inside the same context and compare the output with the +expected output: if even a check fails, the whole test fail.

+

You should realize tha the easy tests supported by plac are not +unittests: they are functional tests. They model then user interaction and the +order of the operations generally matters. The single subtests in a +.placet file are not independent and it makes sense to exit +immediately at the first failure.

+

The support for doctests in plac comes nearly for free, thanks to the +shlex module in the standard library, which is able to parse simple +languages as the ones you can implement with plac. In particular, +thanks to shlex, plac is able to recognize comments (the default +comment character is #), escape sequences and more. Look at the +shlex documentation if you need to customize how the language is +interpreted. For more flexibility, it is even possible to pass to the +interpreter a custom split function with signature split(line, +commentchar).

+

In addition, I have implemented from scratch some support for line number +recognition, so that if a test fail you get the line number of the +failing command. This is especially useful if your tests are +stored in external files, even if plac easy tests does not need to be in +a file: you can just pass to the .doctest method a list of +strings corresponding to the lines of the file.

+

At the present plac does not use any code from the doctest +module, but the situation may change in the future (it would be nice +if plac could reuse doctests directives like ELLIPSIS).

+

It is straighforward to integrate your .placet tests with standard +testing tools. For instance, you can integrate your doctests with nose +or py.test as follow:

+
+import os, shlex, plac
+
+def test_doct():
+   """
+   Find all the doctests in the current directory and run them with the
+   corresponding plac interpreter (the shebang rules!)
+   """
+   placets = [f for f in os.listdir('.') if f.endswith('.placet')]
+   for placet in placets:
+       lines = list(open(placet))
+       assert lines[0].startswith('#!'), 'Missing or incorrect shebang line!'
+       firstline = lines[0][2:] # strip the shebang
+       main = plac.import_main(*shlex.split(firstline))
+       yield plac.Interpreter(main).doctest, lines[1:]
+
+

Here you should notice that usage of plac.import_main, an utility +which is able to import the main function of the script specified in +the shebang line. You can use both the full path name of the +tool, or a relative path name. In this case the runner look at the +environment variable PLACPATH and it searches +the plac tool in the directories specified there (PLACPATH is just +a string containing directory names separated by colons). If the variable +PLACPATH is not defined, it just looks in the current directory. +If the plac tool is not found, an ImportError is raised.

+
+
+

Plac batch scripts

+

It is pretty easy to realize that an interactive interpreter can +also be used to run batch scripts: instead of reading the commands from +the console, it is enough to read the commands from a file. +plac interpreters provide an .execute method to perform just that.

+

There is just a subtle point to notice: whereas in an interactive loop +one wants to manage all exceptions, a batch script should not in the +background in case of unexpected errors. The implementation of +Interpreter.execute makes sure that any error raised by +plac.call internally is re-raised. In other words, plac +interpreters wrap the errors, but does not eat them: the errors are +always accessible and can be re-raised on demand.

+

The exception is the case of invalid commands, which are skipped. +Consider for instance the following batch file, which contains a +mispelled command (.dl instead of .del):

+
+#!ishelve.py
+.clear 
+a=1 b=2
+.show
+.del a
+.dl b
+.show
+
+
+

If you execute the batch file, the interpreter will print a .dl: not found +at the .dl line and will continue:

+
+$ python -c "import plac, ishelve
+plac.Interpreter(ishelve.main).execute(open('ishelve.plac'), verbose=True)"
+i> .clear
+cleared the shelve
+i> a=1 b=2
+setting a=1
+setting b=2
+i> .show
+b=2
+a=1
+i> .del a
+deleted a
+i> .dl b
+2
+.dl: not found
+i> .show
+b=2
+
+

The verbose flag is there to show the lines which are being interpreted +(prefixed by i>). This is done on purpose, so that you can cut and paste +the output of the batch script and turn it into a .placet test +(cool, isn't it?).

+
+
+

Implementing subcommands

+

When I discussed the ishelve implementation in the basic +documentation, I said that it looked like a poor man implementation +of an object system as a chain of elifs; I also said that plac was +able to do much better than that. Here I will substantiate my claim.

+

plac is actually able to infer a set of subparsers from a +generic container of commands. This is useful if you want to +implement subcommands (a familiar example of a command-line +application featuring subcommands is subversion).

+

Technically a container of commands is any object with a .commands attribute +listing a set of functions or methods which are valid commands. A command +container may have initialization/finalization hooks (__enter__/__exit__) +and dispatch hooks (__missing__, invoked for invalid command names). +Moreover, only when using command containers plac is able to provide +automatic autocompletion of commands.

+

The shelve interface can be rewritten in an object-oriented way as follows:

+
+# ishelve2.py
+import shelve, os, sys, plac
+
+class ShelveInterface(object):
+    "A minimal interface over a shelve object."
+    commands = 'set', 'show', 'showall', 'delete'
+    @plac.annotations(
+        configfile=('path name of the shelve', 'option'))
+    def __init__(self, configfile):
+        self.configfile = configfile or '~/conf.shelve'
+        self.fname = os.path.expanduser(self.configfile)
+        self.__doc__ += '\nOperating on %s.\n.help to see '\
+            'the available commands.\n'  % self.fname
+    def __enter__(self):
+        self.sh = shelve.open(self.fname)
+        return self
+    def __exit__(self, etype, exc, tb):
+        self.sh.close()
+    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' % (name, self.sh[name]) # no error checking
+    def showall(self):
+        "show all parameters"
+        for name in self.sh:
+            yield '%s = %s' % (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] # no error checking
+
+main = ShelveInterface # useful for the tests
+
+if __name__ == '__main__':
+    plac.Interpreter.call(ShelveInterface)
+
+

plac.Interpreter objects wrap context manager objects +consistently. In other words, if you wrap an object with +__enter__ and __exit__ methods, they are invoked in the right +order (__enter__ before the interpreter loop starts and +__exit__ after the interpreter loop ends, both in the regular and +in the exceptional case). In our example, the methods __enter__ +and __exit__ make sure the the shelve is opened and closed +correctly even in the case of exceptions. Notice that I have not +implemented any error checking in the show and delete methods +on purpose, to verify that plac works correctly in the presence of +exceptions.

+

When working with command containers, plac automatically adds two +special commands to the set of provided commands: .help +and .last_tb. The .help command is the easier to understand: +when invoked without arguments it displays the list of available commands +with the same formatting of the cmd module; when invoked with the name of +a command it displays the usage message for that command. +The .last_tb command is useful when debugging: in case of errors, +it allows you to display the traceback of the last executed command.

+

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

+
+$ python ishelve2.py
+A minimal interface over a shelve object.
+Operating on /home/micheles/conf.shelve.
+.help to see the available commands.
+i> .help
+
+special commands
+================
+.help .last_tb
+
+custom commands
+===============
+delete  set  show  showall
+
+i> delete
+deleting everything
+i> set a pippo
+setting a=pippo
+i> set b lippo
+setting b=lippo
+i> showall
+b = lippo
+a = pippo
+i> show a b
+a = pippo
+b = lippo
+i> del a
+deleting a
+i> showall
+b = lippo
+i> delete a
+deleting a
+KeyError: 'a'
+i> .last_tb
+ File "/usr/local/lib/python2.6/dist-packages/plac-0.6.0-py2.6.egg/plac_ext.py", line 190, in _wrap
+    for value in genobj:
+  File "./ishelve2.py", line 37, in delete
+    del self.sh[name] # no error checking
+  File "/usr/lib/python2.6/shelve.py", line 136, in __delitem__
+    del self.dict[key]
+i>
+
+

Notice that in interactive mode the traceback is hidden, unless +you pass the verbose flag to the Interpreter.interact method.

+
+
+

plac.Interpreter.call

+

At the core of plac there is the call function which invokes +a callable with the list of the arguments passed at the command-line +(sys.argv[1:]). Thanks to plac.call you can launch your module +by simply adding the lines:

+
+if __name__ == '__main__':
+    plac.call(main)
+
+

Everything works fine if main is a simple callable performing some +action; however, in many cases, one has a main "function" which +is a actually a factory returning a command container object. For +instance, in my second shelve example the main function is the class +ShelveInterface, and the two lines needed to run the module are +a bit ugly:

+
+if __name__ == '__main__':
+   plac.Interpreter(plac.call(ShelveInterface)).interact()
+
+

Moreover, now the program runs, but only in interactive mode, i.e. +it is not possible to run it as a script. It would be nice instead +to be able to specify the command to execute on the command-line +and have the interpreter start, execute the command and finish +properly (I mean by calling __enter__ and __exit__) +without needing user input. The the script could be called from +a batch shell script working in the background. +In order to provide such functionality plac.Interpreter provides +a classmethod named .call which takes the factory, instantiates +it with the arguments read from the command line, wraps the resulting +container object as an interpreter and runs it with the rest arguments +found in the command line. Here is the code to turn the ShelveInterface +into a script

+
+# ishelve3.py
+from ishelve2 import ShelveInterface as main
+
+if __name__ == '__main__':
+    import plac; plac.Interpreter.call(main)
+
+
+

and here are a few examples of usage:

+
+$ python ishelve3.py -h
+usage: ishelve3.py [-h] [-i] [-configfile CONFIGFILE] [args [args ...]]
+
+positional arguments:
+  args
+
+optional arguments:
+  -h, --help            show this help message and exit
+  -i, --interact        start interactive interpreter
+  -configfile CONFIGFILE
+                        path name of the shelve
+
+$ python ishelve3.py set a 1
+setting a=1
+$ python ishelve3.py show a
+a = 1
+
+

If you pass the -i flag in the command line, then the +script will enter in interactive mode and ask the user +for the commands to execute:

+
+$ python ishelve3.py -i
+A minimal interface over a shelve object.
+Operating on /home/micheles/conf.shelve.
+.help to see the available commands.
+
+i>
+
+

In a sense, I have closed the circle: at the beginning of this +document I discussed how to turn a script into an interactive +application (the shelve_interpreter.py example), whereas here I +have show how to turn an interactive application into a script.

+

The complete signature of plac.Interpreter.call is the following:

+
+call(factory, arglist=sys.argv[1:],
+     commentchar='#', split=shlex.split,
+     stdin=sys.stdin, prompt='i> ', verbose=False)
+
+

The factory must have a fixed number of positional arguments (no +default arguments, no varargs, no kwargs), otherwise a TypeError +is raised: the reason is that we want to be able to distinguish the +command-line arguments needed to instantiate the factory from the rest +arguments that must be sent to the corresponding interpreter object. +It is also possible to specify a list of arguments different from +sys.argv[1:] (useful in tests), the character to be recognized as +a comment, the splitting function, the input source and the prompt to +use while in interactive mode, and a verbose flag.

+
+
+

Readline support

+

Starting from release 0.6 plac offers full readline support. That +means that if your Python was compiled with readline support you get +autocompletion and persistent command history for free. By default +all commands are autocomplete in a case sensitive way. If you want to +add new words to the autocompletion set, or you want to change the +location of the .history file, or to change the case sensitivity, +the way to go is to pass a plac.ReadlineInput object to the +interpreter. Here is an example, assuming you want to build a +database interface understanding SQL commands:

+
+import os, plac
+from sqlalchemy.ext.sqlsoup import SqlSoup
+
+SQLKEYWORDS = set(['select', 'from', 'inner', 'join', 'outer', 'left', 'right']
+                  ) # and many others
+DBTABLES = set(['table1', 'table2']) # you can read them from the db schema
+
+COMPLETIONS = SQLKEYWORDS | DBTABLES
+
+class SqlInterface(object):
+    commands = ['SELECT']
+    def __init__(self, dsn):
+        self.soup = SqlSoup(dsn)
+    def SELECT(self, argstring):
+        sql = 'SELECT ' + argstring
+        for row in self.soup.bind.execute(sql):
+            yield str(row) # the formatting can be much improved
+
+rl_input = plac.ReadlineInput(
+    COMPLETIONS, histfile=os.path.expanduser('~/.sql_interface.history'), 
+    case_sensitive=False)
+
+def split_on_first_space(line, commentchar):
+    return line.strip().split(' ', 1) # ignoring comments
+    
+if __name__ == '__main__':
+    plac.Interpreter.call(SqlInterface, split=split_on_first_space,
+                          stdin=rl_input, prompt='sql> ')
+
+
+

Here is an example of usage:

+
+$ python sql_interface.py <some dsn>
+sql> SELECT a.* FROM TABLE1 AS a INNER JOIN TABLE2 AS b ON a.id = b.id
+...
+
+

You can check that entering just sel and pressing TAB the readline library +completes the SELECT keyword for you and makes it upper case; idem for +FROM, INNER, JOIN and even for the names of the tables. An +obvious improvement is to read the names of the tables by introspecting +the database: actually you can even read the names of the views and of +the columns, and have full autocompletion. All the entered commands +and recorded and saved in the file ~/.sql_interface.history when +exiting from the command-line interface.

+

If the readline library is not available, my suggestion is to use the +rlwrap tool which provides similar features, at least on Unix-like +platforms. plac should also work fine on Windows with the pyreadline +library (I do not use Windows, so this part is very little tested: I +tried it only once and it worked, but your mileage may vary). +For people worried about licenses, I will notice that plac uses the +readline library only if available, it does not include it and it does +not rely on it in any fundamental way, so that the plac licence does +not need to be the GPL (actually it is a BSD +do-whatever-you-want-with-it licence).

+

The interactive mode of plac can be used as a replacement of the +cmd module in the standard library. It is actually better than cmd: +for instance, the .help command is more powerful, since it +provides information about the arguments accepted by the given command:

+
+i> .help set
+usage:  set name value
+
+set name value
+
+positional arguments:
+  name
+  value
+
+i> .help delete
+usage:  delete [name]
+
+delete given parameter (or everything)
+
+positional arguments:
+  name
+
+i> .help show
+usage:  show [names [names ...]]
+
+show given parameters
+
+positional arguments:
+  names
+
+

As you can imagine, the help message is provided by the underlying argparse +subparser (there is a subparser for each command). plac commands accept +options, flags, varargs, keyword arguments, arguments with defaults, +arguments with a fixed number of choices, type conversion and all the +features provided of argparse which should be reimplemented from scratch +using plac.

+

Moreover at the moment plac also understands command abbreviations. +However, this feature may disappear in +future releases. It was meaningful in the past, when plac did not support +readline.

+

Notice that if an abbreviation is ambiguous, plac warns you:

+
+i> sh
+NameError: Ambiguous command 'sh': matching ['showall', 'show']
+
+
+
+

The plac runner

+

The distribution of plac includes a runner script named plac_runner.py, +which will be installed in a suitable directory in your system by distutils +(say in \usr\local\bin\plac_runner.py in a Unix-like operative system). +The runner provides many facilities to run .plac scripts and +.placet files, as well as Python modules containg a main +object, which can be a function, a command container object or +even a command container class.

+

For instance, suppose you want to execute a script containing commands +defined in the ishelve2 module like the following one:

+
+#!ishelve2.py:ShelveInterface -c ~/conf.shelve
+set a 1
+del a
+del a # intentional error
+
+
+

The first line of the .plac script contains the name of the +python module containing the plac interpreter and the arguments +which must be passed to its main function in order to be able +to instantiate an interpreter object. In this case I appended +:ShelveInterface to the name of the module to specify the +object that must be imported: if not specified, by default the +object named 'main' is imported. +The other lines contains commands. +You can run the script as follows:

+
+$ plac_runner.py --batch ishelve2.plac
+setting a=1
+deleting a
+Traceback (most recent call last):
+  ...
+_bsddb.DBNotFoundError: (-30988, 'DB_NOTFOUND: No matching key/data pair found')
+
+

The last command intentionally contained an error, to show that the +plac runner does not eat the traceback.

+

The runner can also be used to run Python modules in interactive +mode and non-interactive mode. If you put this alias in your bashrc

+
+alias plac="plac_runner.py"
+

(or you define a suitable plac.bat script in Windows) you can +run the ishelve2.py script in interactive mode as +follows:

+
+$ plac -i ishelve2.py:ShelveInterface
+A minimal interface over a shelve object.
+Operating on /home/micheles/conf.shelve.
+.help to see the available commands.
+
+i> del
+deleting everything
+i> set a 1
+setting a=1
+i> set b 2
+setting b=2
+i> show b
+b = 2
+
+

Now you can cut and paste the interactive session an turns into into +a .placet file like the following:

+
+#!ishelve2.py:ShelveInterface -configfile=~/test.shelve
+i> del
+deleting everything
+i> set a 1
+setting a=1
+i> set b 2
+setting b=2
+i> show a
+a = 1
+
+
+

Notice that the first line specifies a test database +~/test.shelve, to avoid clobbering your default shelve. If you +mispell the arguments in the first line plac will give you an +argparse error message (just try).

+

You can run placets following the shebang convention directly with +the plac runner:

+
+$ plac --test ishelve2.placet
+run 1 plac test(s)
+
+

If you want to see the output of the tests, pass the -v/--verbose flag. +Notice that he runner ignore the extension, so you can actually use any +extension your like, but it relies on the first line of the file to invoke +the corresponding plac tool with the given arguments.

+

The plac runner does not provide any test discovery facility, +but you can use standard Unix tools to help. For instance, you can +run all the .placet files into a directory and its subdirectories +as follows:

+
+$ find . -name \*.placet | xargs plac_runner.py -t
+
+

The plac runner expects the main function of your script to +return a plac tool, i.e. a function or an object with a .commands +attribute. It this is not the case the runner gracefully exits.

+

It also works in non-interactive mode, if you call it as

+
+$ plac module.py args ...
+

Here is an example:

+
+$ plac ishelve.py a=1
+setting a=1
+$ plac ishelve.py .show
+a=1
+
+

Notice that in non-interactive mode the runner just invokes plac.call +on the main object of the Python module.

+
+
+

A non class-based example

+

plac does not force you to use classes to define command containers. +Even a simple function can be a valid command container, it is +enough to add to it a .commands attribute and possibly +__enter__ and/or __exit__ attributes.

+

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')
+def checkout(url):
+    "A fake checkout command"
+    return ('checkout ', url)
+
+@plac.annotations(message=('commit message', 'option'))
+def commit(message):
+    "A fake commit command"
+    return ('commit ', message)
+
+@plac.annotations(quiet=('summary information', 'flag', 'q'))
+def status(quiet):
+    "A fake status command"
+    return ('status ', quiet)
+
+def __missing__(name):
+    return 'Command %r does not exist' % name
+
+def __exit__(etype, exc, tb):
+    "Will be called automatically at the end of the call/cmdloop"
+    if etype in (None, GeneratorExit): # success
+        print('ok')
+
+main = __import__(__name__) # the module imports itself!
+
+
+

Notice that I have defined both an __exit__ hook and a __missing__ +hook, invoked for non-existing commands. +The real trick here is the line main = __import__(__name__), which +define main to be an alias for the current module.

+

The vcs module does not contain an if __name__ == '__main__' +block, but you can still run it through the plac runner +(try plac vcs.py -h):

+
+usage: plac_runner.py 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
+
+
+

You can get help for the subcommands by postponing -h after the +name of the command:

+
+$ plac vcs.py status -h
+usage: vcs.py status [-h] [-q]
+
+A fake status command
+
+optional arguments:
+  -h, --help   show this help message and exit
+  -q, --quiet  summary information
+
+

Notice how the docstring of the command is automatically shown in +usage message, as well as the documentation for the sub flag -q.

+

Here is an example of a non-interactive session:

+
+$ plac vcs.py check url
+checkout
+url
+$ plac vcs.py st -q
+status
+True
+$ plac vcs.py co
+commit
+None
+
+

and here is an interactive session:

+
+$ plac -i vcs.py
+usage: plac_runner.py vcs.py [-h] {status,commit,checkout} ...
+i> check url
+checkout
+url
+i> st -q
+status
+True
+i> co
+commit
+None
+i> sto
+Command 'sto' does not exist
+i> [CTRL-D]
+ok
+
+

Notice the invocation of the __missing__ hook for non-existing commands. +Notice also that the __exit__ hook gets called only in interactive +mode.

+

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.

+
+
+

Writing your own plac runner

+

The runner included in the plac distribution is intentionally kept +small (around 50 lines of code) so that you can study it and write +your own runner if want to. If you need to go to such level +of detail, you should know that the most important method of +the Interpreter class is the .send method, which takes +strings in input and returns a four-tuple with attributes +.str, .etype, .exc and .tb:

+
    +
  • .str is the output of the command, if successful (a string);
  • +
  • .etype is the class of the exception, if the command fail;
  • +
  • .exc is the exception instance;
  • +
  • .tb is the traceback.
  • +
+

Moreover the __str__ representation of the output object is redefined +to return the output string if the command was successful or the error +message if the command failed (actually it returns the error message +preceded by the name of the exception class).

+

For instance, if you send a mispelled option to +the interpreter a SystemExit will be trapped:

+
+>>> import plac
+>>> from ishelve import ishelve
+>>> with plac.Interpreter(ishelve) as i:
+...     print(i.send('.cler'))
+...
+SystemExit: unrecognized arguments: .cler
+
+

It is important to invoke the .send method inside the context manager, +otherwise you will get a RuntimeError.

+

For instance, suppose you want to implement a graphical runner for a +plac-based interpreter with two text widgets: one to enter the commands +and one to display the results. Suppose you want to display the errors +with tracebacks in red. You will need to code something like that +(pseudocode follows):

+
+input_widget = WidgetReadingInput()
+output_widget = WidgetDisplayingOutput()
+
+def send(interpreter, line):
+    out = interpreter.send(line)
+    if out.tb: # there was an error
+        output_widget.display(out.tb, color='red')
+    else:
+        output_widget.display(out.str)
+
+main = plac.import_main(tool_path) # get the main object
+
+with plac.Interpreter(main) as i:
+   def callback(event):
+      if event.user_pressed_ENTER():
+           send(i, input_widget.last_line)
+   input_widget.addcallback(callback)
+   gui_mainloop.start()
+
+

You can adapt the pseudocode to your GUI toolkit of choice and you can +also change the file associations in such a way that clicking on a +plac tool file the graphical user interface starts.

+

An example of GUI program built on top of plac is given later on, in the +paragraph Managing the output of concurrent commands (using Tkinter +for simplicity and portability).

+

There is a final caveat: since the plac interpreter loop is +implemented via extended generators, plac interpreters are single threaded: you +will get an error if you .send commands from separated threads. +You can circumvent the problem by using a queue. If EXIT is a sentinel +value to signal exiting from the interpreter look, you can write code +like this:

+
+with interpreter:
+    for input_value in iter(input_queue.get, EXIT):
+        output_queue.put(interpreter.send(input_value))
+
+

The same trick also work for processes; you could run the interpreter +loop in a separate process and send commands to it via the Queue +class provided by the multiprocessing module.

+
+
+

Long running commands

+

As we saw, by default a plac interpreter blocks until +the command terminates. This is an issue, in the sense that it makes +the interactive experience quite painful for long running commands. An +example is better than a thousand words, so consider the following +fake importer:

+
+import time
+import plac
+
+class FakeImporter(object):
+    "A fake importer with an import_file command"
+    commands = ['import_file']
+    def __init__(self, dsn):
+        self.dsn = dsn
+    def import_file(self, fname):
+        "Import a file into the database"
+        try:
+            for n in range(10000):
+                time.sleep(.01)
+                if n % 100 == 99:
+                    yield 'Imported %d lines' % (n+1)
+        finally:
+            print('closing the file')
+
+if __name__ == '__main__':
+    plac.Interpreter.call(FakeImporter)
+
+
+

If you run the import_file command, you will have to wait for 200 seconds +before entering a new command:

+
+$ python importer1.py dsn
+A fake importer with an import_file command
+i> import_file file1
+... <wait 3+ minutes>
+Imported 100 lines
+Imported 200 lines
+Imported 300 lines
+...
+Imported 10000 lines
+closing the file
+
+

Being unable to enter any other command is quite annoying: in such situation one +would like to run the long running commands in the background, to keep +the interface responsive. plac provides two ways to reach this goal: threads +and processes.

+
+
+

Threaded commands

+

The most familiar way to execute a task in the background (even if not +necessarily the best way) is to run it into a separated thread. In our +example it is sufficient to replace the line

+
+commands = ['import_file']
+

with

+
+thcommands = ['import_file']
+

to tell to the plac interpreter that the command import_file should be +run into a separated thread. Here is an example session:

+
+i> import_file file1
+<ThreadedTask 1 [import_file file1] RUNNING>
+
+

The import task started in a separated thread. You can see the +progress of the task by using the special command .output:

+
+i> .output 1
+<ThreadedTask 1 [import_file file1] RUNNING>
+Imported 100 lines
+Imported 200 lines
+
+

If you look after a while, you will get more lines of output:

+
+i> .output 1
+<ThreadedTask 1 [import_file file1] RUNNING>
+Imported 100 lines
+Imported 200 lines
+Imported 300 lines
+Imported 400 lines
+
+

If you look after a time long enough, the task will be finished:

+
+i> .output 1
+<ThreadedTask 1 [import_file file1] FINISHED>
+
+

You can even skip the number argument: then .output will the return +the output of the last launched command (the special commands like .output +do not count).

+

You can launch many tasks one after the other:

+
+i> import_file file2
+<ThreadedTask 5 [import_file file2] RUNNING>
+i> import_file file3
+<ThreadedTask 6 [import_file file3] RUNNING>
+
+

The .list command displays all the running tasks:

+
+i> .list
+<ThreadedTask 5 [import_file file2] RUNNING>
+<ThreadedTask 6 [import_file file3] RUNNING>
+
+

It is even possible to kill a task:

+
+i> .kill 5
+<ThreadedTask 5 [import_file file2] TOBEKILLED>
+# wait a bit ...
+closing the file
+i> .output 5
+<ThreadedTask 5 [import_file file2] KILLED>
+
+

You should notice that since at the Python level it is impossible to kill +a thread, the .kill commands works by setting the status of the task to +TOBEKILLED. Internally the generator corresponding to the command +is executed in the thread and the status is checked at each iteration: +when the status become TOBEKILLED a GeneratorExit exception is +raised and the thread terminates (softly, so that the finally clause +is honored). In our example the generator is yielding +back control once every 100 iterations, i.e. every two seconds (not much). +In order to get a responsive interface it is a good idea to yield more +often, for instance every 10 iterations (i.e. 5 times per second), +as in the following code:

+
+import time
+import plac
+
+class FakeImporter(object):
+    "A fake importer with an import_file command"
+    thcommands = ['import_file']
+    def __init__(self, dsn):
+        self.dsn = dsn
+    def import_file(self, fname):
+        "Import a file into the database"
+        try:
+            for n in range(10000):
+                time.sleep(.02)
+                if n % 100 == 99: # every two seconds
+                    yield 'Imported %d lines' % (n+1)
+                if n % 10 == 9: # every 0.2 seconds
+                    yield # go back and check the TOBEKILLED status
+        finally:
+            print('closing the file')
+
+if __name__ == '__main__':
+    plac.Interpreter.call(FakeImporter)
+
+
+
+
+

Running commands as external processes

+

Threads are not loved much in the Python world and actually most people +prefer to use processes instead. For this reason plac provides the +option to execute long running commands as external processes. Unfortunately +the current implementation only works in Unix-like operating systems +(including Mac OS X) because it relies on fork via the multiprocessing +module.

+

In our example, to enable the feature it is sufficient to replace the line

+
+thcommands = ['import_file']
+

with

+
+mpcommands = ['import_file'].
+

The user experience is exactly the same as with threads and you will not see any +difference at the user interface level:

+
+i> import_file file3
+<MPTask 1 [import_file file3] SUBMITTED>
+i> .kill 1
+<MPTask 1 [import_file file3] RUNNING>
+closing the file
+i> .o 1
+<MPTask 1 [import_file file3] KILLED>
+Imported 100 lines
+Imported 200 lines
+i>
+
+

Still, using processes is quite different than using threads: in +particular, when using processes you can only yield pickleable values +and you cannot re-raise an exception first raised in a different +process, because traceback objects are not pickleable. Moreover, +you cannot rely on automatic sharing of your objects.

+

On the plus side, when using processes you do not need to worry about +killing a command: they are killed immediately using a SIGTERM signal, +and there is not a TOBEKILLED mechanism. Moreover, the killing is +guaranteed to be soft: internally a command receiving a SIGTERM raises +a TerminatedProcess exception which is trapped in the generator +loop, so that the command is closed properly.

+

Using processes allows to take full advantage of multicore machines +and it is safer than using threads, so it is the recommended approach +unless you are working on Windows.

+
+
+

Managing the output of concurrent commands

+

plac acts as a command-line task launcher and can be used as the base +to build a GUI-based task launcher and task monitor. To this aim the +interpreter class provides a .submit method which returns a task +object and a .tasks method returning the list of all the tasks +submitted to the interpreter. The submit method does not start the task +and thus it is nonblocking. +Each task has an .outlist attribute which is a list +storing the value yielded by the generator underlying the task (the +None values are skipped though): the .outlist grows as the +task runs and more values are yielded. Accessing the .outlist is +nonblocking and can be done freely. +Finally there is a .result +property which waits for the task to finish and returns the last yielded +value or raises an exception.

+

Here is some example code to visualize the output of the FakeImporter +in Tkinter (I chose Tkinter because it is easy to use and it is +in the standard library, but you can use any GUI):

+
+from Tkinter import *
+from importer3 import FakeImporter
+
+def taskwidget(root, task, tick=500):
+    "A Label widget showing the output of a task every 500 ms"
+    sv = StringVar(root)
+    lb = Label(root, textvariable=sv)
+    def show_outlist():
+        try:
+            out = task.outlist[-1]
+        except IndexError: # no output yet
+            out = ''
+        sv.set('%s %s' % (task, out))
+        root.after(tick, show_outlist)
+    root.after(0, show_outlist)
+    return lb
+
+def monitor(tasks):
+    root = Tk()
+    for task in tasks:
+        task.run()
+        taskwidget(root, task).pack()
+    root.mainloop()
+
+if __name__ == '__main__':
+    import plac
+    with plac.Interpreter(plac.call(FakeImporter)) as i:
+        tasks = [i.submit('import_file f1'), i.submit('import_file f2')]
+        monitor(tasks)
+
+
+
+
+

Parallel computing with plac

+

plac is certainly not intended as a tool for parallel computing, but +still you can use it to launch a set of commands and to collect the +results, similarly to the MapReduce pattern popularized by +Google. In order to give an example, I will consider the "Hello +World" of parallel computing, i.e. the computation of pi with +independent processes. There is a huge number of algorithms to +compute pi; here I will describe a trivial one chosen for simplicity, +not per efficienty. The trick is to consider the first quadrant of a +circle with radius 1 and to extract a number of points (x, y) with +x and y random variables in the interval [0,1]. The +probability of extracting a number inside the quadrant (i.e. with +x^2 + y^2 < 1) is proportional to the area of the quadrant +(i.e. pi/4). The value of pi therefore can be extracted by +multiplying by 4 the ratio between the number of points in the +quadrant versus the total number of points N, for N large:

+
+def calc_pi(N):
+    inside = 0
+    for j in xrange(N):
+        x, y = random(), random()
+        if x*x + y*y < 1:
+            inside += 1
+    return (4.0 * inside) / N
+
+

The algorithm is trivially parallelizable: if you have n CPUs, you can +compute pi n times with N/n iterations, sum the results and divide the total +by n. I have a Macbook with two cores, therefore I would expect a speedup +factor of 2 with respect to a sequential computation. Moreover, I would +expect a threaded computation to be even slower than a sequential +computation, due to the GIL and the scheduling overhead.

+

Here is a script implementing the algorithm and working in three different +modes (parallel mode, threaded mode and sequential mode) depending on a +mode option:

+
+from __future__ import with_statement
+from random import random
+import multiprocessing
+import plac
+
+class PiCalculator(object):
+    """Compute pi in parallel with threads or processes"""
+    
+    @plac.annotations(
+        npoints=('number of integration points', 'positional', None, int),
+        mode=('sequential|parallel|threaded', 'option', 'm', str, 'SPT'))
+    def __init__(self, npoints, mode='S'):
+        self.npoints = npoints
+        if mode == 'P':
+            self.mpcommands = ['calc_pi']
+        elif mode == 'T':
+            self.thcommands = ['calc_pi']
+        elif mode == 'S':
+            self.commands = ['calc_pi']
+        self.n_cpu = multiprocessing.cpu_count()
+    
+    def submit_tasks(self):
+        self.i = plac.Interpreter(self).__enter__()            
+        return [self.i.submit('calc_pi %d' % (self.npoints / self.n_cpu))
+                for _ in range(self.n_cpu)]
+
+    def close(self):
+        self.i.close()
+
+    @plac.annotations(
+        npoints=('npoints', 'positional', None, int))
+    def calc_pi(self, npoints):
+        counts = 0
+        for j in xrange(npoints):
+            n, r = divmod(j, 1000000)
+            if r == 0:
+                yield '%dM iterations' % n
+            x, y = random(), random()
+            if x*x + y*y < 1:
+                counts += 1
+        yield (4.0 * counts)/npoints
+
+    def run(self):
+        tasks = self.i.tasks()
+        for t in tasks:
+            t.run()
+        try:
+            total = 0
+            for task in tasks:
+                total += task.result
+        except: # the task was killed
+            print tasks
+            return
+        return total / self.n_cpu
+
+if __name__ == '__main__':
+    pc = plac.call(PiCalculator)
+    pc.submit_tasks()
+    try:
+        import time; t0 = time.time()
+        print '%f in %f seconds ' % (pc.run(), time.time() - t0)
+    finally:
+        pc.close()
+
+
+

Notice the submit_tasks method, which instantiates and initializes a +plac.Interpreter object and submits a number of commands corresponding +to the number of available CPUs. The calc_pi command yield a log +message every million of interactions, just to monitor the progress of +the computation. The run method starts all the submitted commands +in parallel and sums the results. It returns the average value of pi +after the slowest CPU has finished its job (if the CPUs are equal and +equally busy they should finish more or less at the same time).

+

Here are the results on my old Macbook with Ubuntu 10.04 and Python 2.6, +for 10 million of iterations:

+
+$ python picalculator.py -mP 10000000 # two processes
+3.141904 in 5.744545 seconds
+$ python picalculator.py -mT 10000000 # two threads
+3.141272 in 13.875645 seconds
+$ python picalculator.py -mS 10000000 # sequential
+3.141586 in 11.353841 seconds
+
+

As you see using processes one gets a 2x speedup indeed, where the threaded +mode is some 20% slower than the sequential mode.

+

Since the pattern submit a bunch of tasks, starts them and collect the +results is so common, plac provides an utility function +runp(genseq, mode='p', start=True) to start +a bunch a generators and return a list of task objects. By default +runp use processes, but you can use threads by passing mode='t'. +If you do not wont to start the tasks, you can say so (start=False). +With runp the parallel pi calculation becomes a one-liner:

+
+sum(task.result for task in plac.runp(calc_pi(N) for i in range(ncpus)))/ncpus
+
+
+
+

The plac server

+

A command-line oriented interface can be easily converted into a +socket-based interface. Starting from release 0.7 plac features +a builtin server which is able to accept commands from multiple +clients and to execute them. The server works by instantiating +a separate interpreter for each client, so that if a client interpreter +dies for any reason the other interpreters keep working. +To avoid external dependencies the server is based on the asynchat +module in the standard library, but it would not be difficult to +replace the server with a different one (for instance, a Twisted server). +Since asynchat-based servers are asynchronous, any blocking command +in the interpreter should be run in a separated process or thread. +The default port for the plac server is 2199, and the command to +signal end-of-connection is EOF. +For instance, here is how you could manage remote import on a database +(say a SQLite db):

+
+import plac
+from importer2 import FakeImporter
+
+def main(port=2199):
+    main = FakeImporter('dsn')
+    plac.Interpreter(main).start_server(port)
+    
+if __name__ == '__main__':
+   plac.call(main)
+
+
+

You can connect to the server with telnet on port 2199, as follows:

+
+$ telnet localhost 2199
+Trying ::1...
+Trying 127.0.0.1...
+Connected to localhost.
+Escape character is '^]'.
+i> import_file f1
+i> .list
+<ThreadedTask 1 [import_file f1] RUNNING>
+i> .out
+Imported 100 lines
+Imported 200 lines
+i> EOF
+Connection closed by foreign host.
+
+
+
+

Summary

+

Once plac claimed to be the easiest command-line arguments parser +in the world. Having read this document you may think that it is not +so easy after all. But it is a false impression. Actually the +rules are quite simple:

+
    +
  1. if you want to implement a command-line script, use plac.call;
  2. +
  3. if you want to implement a command interpreter, use plac.Interpreter:
      +
    • for an interactive interpreter, call the .interact method;
    • +
    • for an batch interpreter, call the .execute method;
    • +
    +
  4. +
  5. for testing call the Interpreter.check method in the appropriate context +or use the Interpreter.doctest feature;
  6. +
  7. if you need to go at a lower level, you may need to call the +Interpreter.send method which returns a (finished) Task object.
  8. +
  9. long running command can be executed in the background as threads or +processes: just declare them in the lists thcommands and mpcommands +respectively.
  10. +
  11. the .start_server method starts an asynchronous server on the +given port number (default 2199)
  12. +
+

Moreover, remember that plac_runner.py is your friend.

+
+
+
+

Appendix: 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/doc/plac.pdf b/doc/plac.pdf new file mode 100644 index 0000000..612eb98 --- /dev/null +++ b/doc/plac.pdf @@ -0,0 +1,12911 @@ +%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 94 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 (mailto:michele.simionato@gmail.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 153.7323 + 708.5936 + 526.5827 + 720.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 [ 153.7323 + 678.5936 + 526.5827 + 690.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER3': class PDFDictionary +6 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://micheles.googlecode.com/hg/plac/doc/plac.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 153.7323 + 651.5936 + 526.5827 + 663.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 337 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER4': class LinkAnnotation +9 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 8 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Rect [ 62.69291 + 726.5936 + 286.0929 + 738.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER5': class LinkAnnotation +10 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 8 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Rect [ 527.0227 + 726.5936 + 532.5827 + 738.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER6': class LinkAnnotation +11 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 95 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Rect [ 82.69291 + 708.5936 + 223.8629 + 720.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER7': class LinkAnnotation +12 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 95 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Rect [ 527.0227 + 708.5936 + 532.5827 + 720.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER8': class LinkAnnotation +13 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 95 0 R + /XYZ + 62.69291 + 411.0236 + 0 ] + /Rect [ 82.69291 + 690.5936 + 223.2929 + 702.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER9': class LinkAnnotation +14 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 95 0 R + /XYZ + 62.69291 + 411.0236 + 0 ] + /Rect [ 527.0227 + 690.5936 + 532.5827 + 702.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER10': class LinkAnnotation +15 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 109 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Rect [ 82.69291 + 672.5936 + 216.6329 + 684.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER11': class LinkAnnotation +16 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 109 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Rect [ 527.0227 + 672.5936 + 532.5827 + 684.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER12': class LinkAnnotation +17 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 119 0 R + /XYZ + 62.69291 + 741.0236 + 0 ] + /Rect [ 82.69291 + 654.5936 + 257.7529 + 666.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER13': class LinkAnnotation +18 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 119 0 R + /XYZ + 62.69291 + 741.0236 + 0 ] + /Rect [ 527.0227 + 654.5936 + 532.5827 + 666.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER14': class LinkAnnotation +19 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 123 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Rect [ 82.69291 + 636.5936 + 157.7129 + 648.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER15': class LinkAnnotation +20 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 123 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Rect [ 527.0227 + 636.5936 + 532.5827 + 648.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER16': class LinkAnnotation +21 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 123 0 R + /XYZ + 62.69291 + 265.7299 + 0 ] + /Rect [ 82.69291 + 618.5936 + 194.4129 + 630.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER17': class LinkAnnotation +22 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 123 0 R + /XYZ + 62.69291 + 265.7299 + 0 ] + /Rect [ 527.0227 + 618.5936 + 532.5827 + 630.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER18': class LinkAnnotation +23 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 131 0 R + /XYZ + 62.69291 + 522.6236 + 0 ] + /Rect [ 82.69291 + 600.5936 + 144.3829 + 612.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER19': class LinkAnnotation +24 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 131 0 R + /XYZ + 62.69291 + 522.6236 + 0 ] + /Rect [ 521.4627 + 600.5936 + 532.5827 + 612.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER20': class LinkAnnotation +25 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 136 0 R + /XYZ + 62.69291 + 552.6236 + 0 ] + /Rect [ 82.69291 + 582.5936 + 166.6029 + 594.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER21': class LinkAnnotation +26 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 136 0 R + /XYZ + 62.69291 + 552.6236 + 0 ] + /Rect [ 521.4627 + 582.5936 + 532.5827 + 594.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER22': class LinkAnnotation +27 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 139 0 R + /XYZ + 62.69291 + 513.8236 + 0 ] + /Rect [ 82.69291 + 564.5936 + 171.6129 + 576.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER23': class LinkAnnotation +28 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 139 0 R + /XYZ + 62.69291 + 513.8236 + 0 ] + /Rect [ 521.4627 + 564.5936 + 532.5827 + 576.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER24': class LinkAnnotation +29 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 141 0 R + /XYZ + 62.69291 + 493.4236 + 0 ] + /Rect [ 82.69291 + 546.5936 + 228.8629 + 558.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER25': class LinkAnnotation +30 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 141 0 R + /XYZ + 62.69291 + 493.4236 + 0 ] + /Rect [ 521.4627 + 546.5936 + 532.5827 + 558.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER26': class LinkAnnotation +31 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 158 0 R + /XYZ + 62.69291 + 422.6236 + 0 ] + /Rect [ 82.69291 + 528.5936 + 156.0529 + 540.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER27': class LinkAnnotation +32 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 158 0 R + /XYZ + 62.69291 + 422.6236 + 0 ] + /Rect [ 521.4627 + 528.5936 + 532.5827 + 540.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER28': class LinkAnnotation +33 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 207 0 R + /XYZ + 62.69291 + 741.0236 + 0 ] + /Rect [ 82.69291 + 510.5936 + 204.4129 + 522.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER29': class LinkAnnotation +34 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 207 0 R + /XYZ + 62.69291 + 741.0236 + 0 ] + /Rect [ 521.4627 + 510.5936 + 532.5827 + 522.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER30': class LinkAnnotation +35 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 207 0 R + /XYZ + 62.69291 + 513.0236 + 0 ] + /Rect [ 82.69291 + 492.5936 + 128.2729 + 504.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER31': class LinkAnnotation +36 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 207 0 R + /XYZ + 62.69291 + 513.0236 + 0 ] + /Rect [ 521.4627 + 492.5936 + 532.5827 + 504.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER32': class LinkAnnotation +37 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 207 0 R + /XYZ + 62.69291 + 309.0236 + 0 ] + /Rect [ 82.69291 + 474.5936 + 228.3129 + 486.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER33': class LinkAnnotation +38 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 207 0 R + /XYZ + 62.69291 + 309.0236 + 0 ] + /Rect [ 521.4627 + 474.5936 + 532.5827 + 486.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER34': class LinkAnnotation +39 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 221 0 R + /XYZ + 62.69291 + 681.0236 + 0 ] + /Rect [ 62.69291 + 456.5936 + 182.7329 + 468.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER35': class LinkAnnotation +40 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 221 0 R + /XYZ + 62.69291 + 681.0236 + 0 ] + /Rect [ 521.4627 + 456.5936 + 532.5827 + 468.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER36': class LinkAnnotation +41 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 221 0 R + /XYZ + 62.69291 + 648.0236 + 0 ] + /Rect [ 82.69291 + 438.5936 + 134.9429 + 450.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER37': class LinkAnnotation +42 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 221 0 R + /XYZ + 62.69291 + 648.0236 + 0 ] + /Rect [ 521.4627 + 438.5936 + 532.5827 + 450.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER38': class LinkAnnotation +43 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 221 0 R + /XYZ + 62.69291 + 426.0236 + 0 ] + /Rect [ 82.69291 + 420.5936 + 252.7429 + 432.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER39': class LinkAnnotation +44 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 221 0 R + /XYZ + 62.69291 + 426.0236 + 0 ] + /Rect [ 521.4627 + 420.5936 + 532.5827 + 432.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER40': class LinkAnnotation +45 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 224 0 R + /XYZ + 62.69291 + 629.8236 + 0 ] + /Rect [ 82.69291 + 402.5936 + 195.5229 + 414.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER41': class LinkAnnotation +46 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 224 0 R + /XYZ + 62.69291 + 629.8236 + 0 ] + /Rect [ 521.4627 + 402.5936 + 532.5827 + 414.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER42': class LinkAnnotation +47 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 227 0 R + /XYZ + 62.69291 + 609.0236 + 0 ] + /Rect [ 82.69291 + 384.5936 + 149.9429 + 396.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER43': class LinkAnnotation +48 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 227 0 R + /XYZ + 62.69291 + 609.0236 + 0 ] + /Rect [ 521.4627 + 384.5936 + 532.5827 + 396.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER44': class LinkAnnotation +49 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 238 0 R + /XYZ + 62.69291 + 299.8485 + 0 ] + /Rect [ 82.69291 + 366.5936 + 161.0529 + 378.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER45': class LinkAnnotation +50 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 238 0 R + /XYZ + 62.69291 + 299.8485 + 0 ] + /Rect [ 521.4627 + 366.5936 + 532.5827 + 378.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER46': class LinkAnnotation +51 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 243 0 R + /XYZ + 62.69291 + 378.6236 + 0 ] + /Rect [ 82.69291 + 348.5936 + 210.5129 + 360.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER47': class LinkAnnotation +52 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 243 0 R + /XYZ + 62.69291 + 378.6236 + 0 ] + /Rect [ 521.4627 + 348.5936 + 532.5827 + 360.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER48': class LinkAnnotation +53 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 248 0 R + /XYZ + 62.69291 + 330.0679 + 0 ] + /Rect [ 82.69291 + 330.5936 + 167.7229 + 342.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER49': class LinkAnnotation +54 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 248 0 R + /XYZ + 62.69291 + 330.0679 + 0 ] + /Rect [ 521.4627 + 330.5936 + 532.5827 + 342.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER50': class LinkAnnotation +55 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 254 0 R + /XYZ + 62.69291 + 717.0236 + 0 ] + /Rect [ 82.69291 + 312.5936 + 158.2829 + 324.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER51': class LinkAnnotation +56 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 254 0 R + /XYZ + 62.69291 + 717.0236 + 0 ] + /Rect [ 521.4627 + 312.5936 + 532.5827 + 324.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER52': class LinkAnnotation +57 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 267 0 R + /XYZ + 62.69291 + 204.6236 + 0 ] + /Rect [ 82.69291 + 294.5936 + 152.7229 + 306.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER53': class LinkAnnotation +58 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 267 0 R + /XYZ + 62.69291 + 204.6236 + 0 ] + /Rect [ 521.4627 + 294.5936 + 532.5827 + 306.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER54': class LinkAnnotation +59 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 271 0 R + /XYZ + 62.69291 + 371.4236 + 0 ] + /Rect [ 82.69291 + 276.5936 + 205.5229 + 288.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER55': class LinkAnnotation +60 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 271 0 R + /XYZ + 62.69291 + 371.4236 + 0 ] + /Rect [ 521.4627 + 276.5936 + 532.5827 + 288.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER56': class LinkAnnotation +61 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 274 0 R + /XYZ + 62.69291 + 384.6236 + 0 ] + /Rect [ 82.69291 + 258.5936 + 209.9529 + 270.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER57': class LinkAnnotation +62 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 274 0 R + /XYZ + 62.69291 + 384.6236 + 0 ] + /Rect [ 521.4627 + 258.5936 + 532.5827 + 270.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER58': class LinkAnnotation +63 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 278 0 R + /XYZ + 62.69291 + 211.4236 + 0 ] + /Rect [ 82.69291 + 240.5936 + 192.7429 + 252.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER59': class LinkAnnotation +64 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 278 0 R + /XYZ + 62.69291 + 211.4236 + 0 ] + /Rect [ 521.4627 + 240.5936 + 532.5827 + 252.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER60': class LinkAnnotation +65 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 281 0 R + /XYZ + 62.69291 + 318.6236 + 0 ] + /Rect [ 82.69291 + 222.5936 + 177.1729 + 234.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER61': class LinkAnnotation +66 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 281 0 R + /XYZ + 62.69291 + 318.6236 + 0 ] + /Rect [ 521.4627 + 222.5936 + 532.5827 + 234.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER62': class LinkAnnotation +67 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 285 0 R + /XYZ + 62.69291 + 475.8236 + 0 ] + /Rect [ 82.69291 + 204.5936 + 271.6529 + 216.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER63': class LinkAnnotation +68 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 285 0 R + /XYZ + 62.69291 + 475.8236 + 0 ] + /Rect [ 521.4627 + 204.5936 + 532.5827 + 216.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER64': class LinkAnnotation +69 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 287 0 R + /XYZ + 62.69291 + 675.0236 + 0 ] + /Rect [ 82.69291 + 186.5936 + 286.6829 + 198.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER65': class LinkAnnotation +70 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 287 0 R + /XYZ + 62.69291 + 675.0236 + 0 ] + /Rect [ 521.4627 + 186.5936 + 532.5827 + 198.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER66': class LinkAnnotation +71 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 289 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Rect [ 82.69291 + 168.5936 + 206.6229 + 180.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER67': class LinkAnnotation +72 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 289 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Rect [ 521.4627 + 168.5936 + 532.5827 + 180.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER68': class LinkAnnotation +73 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 294 0 R + /XYZ + 62.69291 + 630.4159 + 0 ] + /Rect [ 82.69291 + 150.5936 + 151.6029 + 162.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER69': class LinkAnnotation +74 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 294 0 R + /XYZ + 62.69291 + 630.4159 + 0 ] + /Rect [ 521.4627 + 150.5936 + 532.5827 + 162.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER70': class LinkAnnotation +75 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 294 0 R + /XYZ + 62.69291 + 160.0159 + 0 ] + /Rect [ 82.69291 + 132.5936 + 125.4729 + 144.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER71': class LinkAnnotation +76 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 294 0 R + /XYZ + 62.69291 + 160.0159 + 0 ] + /Rect [ 521.4627 + 132.5936 + 532.5827 + 144.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER72': class LinkAnnotation +77 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 296 0 R + /XYZ + 62.69291 + 508.6772 + 0 ] + /Rect [ 82.69291 + 114.5936 + 246.1129 + 126.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER73': class LinkAnnotation +78 0 obj +<< /Border [ 0 + 0 + 0 ] + /Contents () + /Dest [ 296 0 R + /XYZ + 62.69291 + 508.6772 + 0 ] + /Rect [ 521.4627 + 114.5936 + 532.5827 + 126.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page2': class PDFPage +79 0 obj +% Page dictionary +<< /Annots [ 9 0 R + 10 0 R + 11 0 R + 12 0 R + 13 0 R + 14 0 R + 15 0 R + 16 0 R + 17 0 R + 18 0 R + 19 0 R + 20 0 R + 21 0 R + 22 0 R + 23 0 R + 24 0 R + 25 0 R + 26 0 R + 27 0 R + 28 0 R + 29 0 R + 30 0 R + 31 0 R + 32 0 R + 33 0 R + 34 0 R + 35 0 R + 36 0 R + 37 0 R + 38 0 R + 39 0 R + 40 0 R + 41 0 R + 42 0 R + 43 0 R + 44 0 R + 45 0 R + 46 0 R + 47 0 R + 48 0 R + 49 0 R + 50 0 R + 51 0 R + 52 0 R + 53 0 R + 54 0 R + 55 0 R + 56 0 R + 57 0 R + 58 0 R + 59 0 R + 60 0 R + 61 0 R + 62 0 R + 63 0 R + 64 0 R + 65 0 R + 66 0 R + 67 0 R + 68 0 R + 69 0 R + 70 0 R + 71 0 R + 72 0 R + 73 0 R + 74 0 R + 75 0 R + 76 0 R + 77 0 R + 78 0 R ] + /Contents 338 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER74': class PDFDictionary +80 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/getopt.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 214.8914 + 720.5936 + 246.5585 + 732.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER75': class PDFDictionary +81 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/optparse.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 346.507 + 720.5936 + 389.2842 + 732.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER76': class PDFDictionary +82 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 493.1227 + 720.5936 + 531.4956 + 732.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER77': class PDFDictionary +83 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 346.384 + 708.5936 + 388.8477 + 720.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER78': class PDFDictionary +84 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://www.welton.it/articles/scalable_systems) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 311.5097 + 666.5936 + 371.5115 + 678.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER79': class PDFDictionary +85 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 618.5936 + 84.90623 + 630.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER80': class PDFDictionary +86 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 453.4229 + 606.5936 + 492.8829 + 618.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER81': class PDFDictionary +87 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 116.9711 + 588.5936 + 139.5794 + 600.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER82': class PDFDictionary +88 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 277.9887 + 588.5936 + 321.7169 + 600.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER83': class PDFDictionary +89 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 504.0394 + 576.5936 + 525.3627 + 588.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER84': class PDFDictionary +90 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 351.0408 + 564.5936 + 390.5008 + 576.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER85': class PDFDictionary +91 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 247.8817 + 540.5936 + 266.2217 + 552.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER86': class PDFDictionary +92 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 510.5936 + 85.3538 + 522.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER87': class PDFDictionary +93 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 124.2211 + 438.5936 + 146.9252 + 450.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'F4': class PDFType1Font +94 0 obj +% Font Helvetica-Oblique +<< /BaseFont /Helvetica-Oblique + /Encoding /WinAnsiEncoding + /Name /F4 + /Subtype /Type1 + /Type /Font >> +endobj +% 'Page3': class PDFPage +95 0 obj +% Page dictionary +<< /Annots [ 80 0 R + 81 0 R + 82 0 R + 83 0 R + 84 0 R + 85 0 R + 86 0 R + 87 0 R + 88 0 R + 89 0 R + 90 0 R + 91 0 R + 92 0 R + 93 0 R ] + /Contents 339 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER88': class PDFDictionary +96 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/getopt.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 325.341 + 720.5936 + 356.6198 + 732.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER89': class PDFDictionary +97 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/optparse.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 376.7786 + 720.5936 + 419.1674 + 732.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER90': class PDFDictionary +98 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 365.694 + 708.5936 + 408.8281 + 720.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER91': class PDFDictionary +99 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 83.82606 + 457.3936 + 106.0692 + 469.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER92': class PDFDictionary +100 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 243.8829 + 445.3936 + 265.0029 + 457.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER93': class PDFDictionary +101 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 83.6329 + 308.1936 + 105.6829 + 320.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER94': class PDFDictionary +102 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 421.9727 + 308.1936 + 465.1427 + 320.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER95': class PDFDictionary +103 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 107.8707 + 113.7936 + 129.1584 + 125.7936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER96': class PDFDictionary +104 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 117.7229 + 101.7936 + 138.8429 + 113.7936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page4': class PDFPage +105 0 obj +% Page dictionary +<< /Annots [ 96 0 R + 97 0 R + 98 0 R + 99 0 R + 100 0 R + 101 0 R + 102 0 R + 103 0 R + 104 0 R ] + /Contents 340 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER97': class PDFDictionary +106 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 321.4303 + 463.3936 + 363.754 + 475.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER98': class PDFDictionary +107 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 126.0429 + 451.3936 + 147.1629 + 463.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER99': class PDFDictionary +108 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 212.9936 + 84.20915 + 224.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page5': class PDFPage +109 0 obj +% Page dictionary +<< /Annots [ 106 0 R + 107 0 R + 108 0 R ] + /Contents 341 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER100': class PDFDictionary +110 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 446.1627 + 514.1936 + 464.5027 + 526.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER101': class PDFDictionary +111 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 484.1936 + 86.84915 + 496.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER102': class PDFDictionary +112 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://code.activestate.com/recipes/278844-parsing-the-command-line/) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 315.119 + 484.1936 + 367.369 + 496.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page6': class PDFPage +113 0 obj +% Page dictionary +<< /Annots [ 110 0 R + 111 0 R + 112 0 R ] + /Contents 342 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER103': class PDFDictionary +114 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 756.5936 + 83.81291 + 768.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER104': class PDFDictionary +115 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://code.activestate.com/recipes/278844-parsing-the-command-line/) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 240.1228 + 660.5936 + 297.7099 + 672.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER105': class PDFDictionary +116 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://code.activestate.com/recipes/278844-parsing-the-command-line/) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 648.5936 + 114.9429 + 660.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER106': class PDFDictionary +117 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 496.4721 + 648.5936 + 518.6827 + 660.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER107': class PDFDictionary +118 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 494.1558 + 487.3936 + 515.9027 + 499.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page7': class PDFPage +119 0 obj +% Page dictionary +<< /Annots [ 114 0 R + 115 0 R + 116 0 R + 117 0 R + 118 0 R ] + /Contents 343 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page8': class PDFPage +120 0 obj +% Page dictionary +<< /Contents 344 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER108': class PDFDictionary +121 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 732.5936 + 84.62846 + 744.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER109': class PDFDictionary +122 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 110.2829 + 209.2999 + 132.8629 + 221.2999 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page9': class PDFPage +123 0 obj +% Page dictionary +<< /Annots [ 121 0 R + 122 0 R ] + /Contents 345 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER110': class PDFDictionary +124 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 392.5829 + 538.1936 + 413.7029 + 550.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER111': class PDFDictionary +125 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 157.3904 + 490.1936 + 179.9938 + 502.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER112': class PDFDictionary +126 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 184.0634 + 478.1936 + 223.5234 + 490.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER113': class PDFDictionary +127 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 466.1936 + 102.1529 + 478.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER114': class PDFDictionary +128 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 192.7997 + 466.1936 + 237.9391 + 478.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER115': class PDFDictionary +129 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 324.4372 + 466.1936 + 342.7772 + 478.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER116': class PDFDictionary +130 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 360.0429 + 238.1936 + 402.2829 + 250.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page10': class PDFPage +131 0 obj +% Page dictionary +<< /Annots [ 124 0 R + 125 0 R + 126 0 R + 127 0 R + 128 0 R + 129 0 R + 130 0 R ] + /Contents 346 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page11': class PDFPage +132 0 obj +% Page dictionary +<< /Contents 347 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER117': class PDFDictionary +133 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 338.1568 + 520.1936 + 360.5113 + 532.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER118': class PDFDictionary +134 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://www.sqlalchemy.org/) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 110.6843 + 508.1936 + 169.0343 + 520.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER119': class PDFDictionary +135 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://www.sqlalchemy.org/docs/reference/ext/sqlsoup.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 168.3029 + 496.1936 + 208.8829 + 508.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page12': class PDFPage +136 0 obj +% Page dictionary +<< /Annots [ 133 0 R + 134 0 R + 135 0 R ] + /Contents 348 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER120': class PDFDictionary +137 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 185.0709 + 481.3936 + 208.0228 + 493.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER121': class PDFDictionary +138 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 220.5998 + 469.3936 + 243.819 + 481.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page13': class PDFPage +139 0 obj +% Page dictionary +<< /Annots [ 137 0 R + 138 0 R ] + /Contents 349 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER122': class PDFDictionary +140 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 374.4929 + 436.9936 + 395.6129 + 448.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page14': class PDFPage +141 0 obj +% Page dictionary +<< /Annots [ 140 0 R ] + /Contents 350 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER123': class PDFDictionary +142 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 304.0655 + 378.3936 + 348.3808 + 390.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER124': class PDFDictionary +143 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 293.7749 + 204.3936 + 316.2402 + 216.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page15': class PDFPage +144 0 obj +% Page dictionary +<< /Annots [ 142 0 R + 143 0 R ] + /Contents 351 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER125': class PDFDictionary +145 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 390.1936 + 84.8789 + 402.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER126': class PDFDictionary +146 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 466.5307 + 390.1936 + 509.8367 + 402.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER127': class PDFDictionary +147 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 124.3929 + 366.1936 + 163.8529 + 378.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER128': class PDFDictionary +148 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 85.69291 + 285.1936 + 127.9329 + 297.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER129': class PDFDictionary +149 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 85.69291 + 267.1936 + 107.9337 + 279.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER130': class PDFDictionary +150 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 308.5389 + 267.1936 + 351.8997 + 279.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER131': class PDFDictionary +151 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 380.6856 + 243.1936 + 423.7999 + 255.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER132': class PDFDictionary +152 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 494.4684 + 243.1936 + 516.4627 + 255.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER133': class PDFDictionary +153 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 85.69291 + 213.1936 + 108.3529 + 225.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER134': class PDFDictionary +154 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 277.2428 + 213.1936 + 321.0228 + 225.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER135': class PDFDictionary +155 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 404.5839 + 201.1936 + 426.0657 + 213.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER136': class PDFDictionary +156 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 85.69291 + 147.1936 + 108.61 + 159.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER137': class PDFDictionary +157 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 459.2622 + 135.1936 + 481.289 + 147.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page16': class PDFPage +158 0 obj +% Page dictionary +<< /Annots [ 145 0 R + 146 0 R + 147 0 R + 148 0 R + 149 0 R + 150 0 R + 151 0 R + 152 0 R + 153 0 R + 154 0 R + 155 0 R + 156 0 R + 157 0 R ] + /Contents 352 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER138': class PDFDictionary +159 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 85.69291 + 753.5936 + 108.9242 + 765.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER139': class PDFDictionary +160 0 obj +<< /A << /S /URI + /Type /Action + /URI (file:///home/micheles/Dropbox/md/gcodedev/plac/doc/in-writing) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 340.9248 + 753.5936 + 470.1087 + 765.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER140': class PDFDictionary +161 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 85.69291 + 723.5936 + 107.9247 + 735.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER141': class PDFDictionary +162 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 85.69291 + 711.5936 + 104.0329 + 723.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER142': class PDFDictionary +163 0 obj +<< /A << /S /URI + /Type /Action + /URI (file:///home/micheles/Dropbox/md/gcodedev/plac/doc/in-writing) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 489.2227 + 711.5936 + 532.176 + 723.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER143': class PDFDictionary +164 0 obj +<< /A << /S /URI + /Type /Action + /URI (file:///home/micheles/Dropbox/md/gcodedev/plac/doc/in-writing) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 85.69291 + 699.5936 + 159.6229 + 711.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER144': class PDFDictionary +165 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 678.5936 + 83.81291 + 690.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER145': class PDFDictionary +166 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 219.4229 + 678.5936 + 261.6629 + 690.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER146': class PDFDictionary +167 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 + 648.5936 + 534.3667 + 660.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER147': class PDFDictionary +168 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 325.7268 + 481.3936 + 347.4138 + 493.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER148': class PDFDictionary +169 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 + 314.1936 + 410.5152 + 326.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER149': class PDFDictionary +170 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 275.5829 + 134.9936 + 317.8229 + 146.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER150': class PDFDictionary +171 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 307.9178 + 116.9936 + 351.5999 + 128.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER151': class PDFDictionary +172 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 329.8034 + 92.99362 + 352.1804 + 104.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page17': class PDFPage +173 0 obj +% Page dictionary +<< /Annots [ 159 0 R + 160 0 R + 161 0 R + 162 0 R + 163 0 R + 164 0 R + 165 0 R + 166 0 R + 167 0 R + 168 0 R + 169 0 R + 170 0 R + 171 0 R + 172 0 R ] + /Contents 353 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER152': class PDFDictionary +174 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 109.0098 + 708.5936 + 131.9967 + 720.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER153': class PDFDictionary +175 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 397.2929 + 684.5936 + 415.6329 + 696.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER154': class PDFDictionary +176 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/opterator) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 85.69291 + 663.5936 + 128.4929 + 675.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER155': class PDFDictionary +177 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/CLIArgs) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 85.69291 + 645.5936 + 124.5929 + 657.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER156': class PDFDictionary +178 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 464.3898 + 624.5936 + 503.8498 + 636.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER157': class PDFDictionary +179 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 305.0429 + 612.5936 + 323.3829 + 624.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER158': class PDFDictionary +180 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/Clap/0.7) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 455.0104 + 594.5936 + 479.9015 + 606.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER159': class PDFDictionary +181 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 303.707 + 582.5936 + 322.047 + 594.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER160': class PDFDictionary +182 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/Clap/0.7) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 328.8186 + 582.5936 + 353.3701 + 594.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER161': class PDFDictionary +183 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 570.5936 + 81.03291 + 582.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER162': class PDFDictionary +184 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 552.5936 + 84.4354 + 564.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER163': class PDFDictionary +185 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/cmd.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 275.6978 + 552.5936 + 297.9903 + 564.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER164': class PDFDictionary +186 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://packages.python.org/cmd2/) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 203.5285 + 540.5936 + 231.1357 + 552.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER165': class PDFDictionary +187 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://packages.python.org/cmd2/) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 164.4129 + 528.5936 + 191.6429 + 540.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER166': class PDFDictionary +188 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 302.7929 + 528.5936 + 321.1329 + 540.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER167': class PDFDictionary +189 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 156.6051 + 480.5936 + 177.8606 + 492.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER168': class PDFDictionary +190 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 186.6535 + 456.5936 + 226.1135 + 468.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER169': class PDFDictionary +191 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 493.1227 + 456.5936 + 532.4646 + 468.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER170': class PDFDictionary +192 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 72.7804 + 444.5936 + 96.20788 + 456.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER171': class PDFDictionary +193 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 149.2229 + 414.5936 + 171.2704 + 426.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER172': class PDFDictionary +194 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 128.0309 + 372.5936 + 149.4369 + 384.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER173': class PDFDictionary +195 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 502.8367 + 372.5936 + 524.2427 + 384.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER174': class PDFDictionary +196 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 187.4797 + 348.5936 + 209.0991 + 360.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER175': class PDFDictionary +197 0 obj +<< /A << /S /URI + /Type /Action + /URI (file:///home/micheles/Dropbox/md/gcodedev/plac/doc/in-writing) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 301.6965 + 348.5936 + 426.0446 + 360.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER176': class PDFDictionary +198 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 83.6829 + 276.5936 + 105.7829 + 288.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER177': class PDFDictionary +199 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://code.activestate.com/recipes/278844-parsing-the-command-line/) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 446.5627 + 276.5936 + 502.5727 + 288.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER178': class PDFDictionary +200 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 275.6828 + 264.5936 + 297.3688 + 276.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER179': class PDFDictionary +201 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/optparse.html?highlight=optionparser#optparse.OptionParser) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 77.19665 + 252.5936 + 139.4904 + 264.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER180': class PDFDictionary +202 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 96.54131 + 240.5936 + 139.0255 + 252.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER181': class PDFDictionary +203 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 203.5016 + 207.5936 + 245.8453 + 219.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER182': class PDFDictionary +204 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com/svn/tags/r11/doc/ArgumentParser.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 132.5936 + 138.7898 + 144.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER183': class PDFDictionary +205 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 114.6649 + 120.5936 + 154.1249 + 132.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER184': class PDFDictionary +206 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 191.6329 + 108.5936 + 233.8729 + 120.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page18': class PDFPage +207 0 obj +% Page dictionary +<< /Annots [ 174 0 R + 175 0 R + 176 0 R + 177 0 R + 178 0 R + 179 0 R + 180 0 R + 181 0 R + 182 0 R + 183 0 R + 184 0 R + 185 0 R + 186 0 R + 187 0 R + 188 0 R + 189 0 R + 190 0 R + 191 0 R + 192 0 R + 193 0 R + 194 0 R + 195 0 R + 196 0 R + 197 0 R + 198 0 R + 199 0 R + 200 0 R + 201 0 R + 202 0 R + 203 0 R + 204 0 R + 205 0 R + 206 0 R ] + /Contents 354 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER185': class PDFDictionary +208 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/Clap/0.7) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 263.3429 + 744.5936 + 286.6829 + 756.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER186': class PDFDictionary +209 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 258.5629 + 696.5936 + 276.9029 + 708.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER187': class PDFDictionary +210 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 185.4471 + 615.5936 + 207.1062 + 627.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER188': class PDFDictionary +211 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 177.6784 + 603.5936 + 199.6123 + 615.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER189': class PDFDictionary +212 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 549.5936 + 83.81291 + 561.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER190': class PDFDictionary +213 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 278.4678 + 531.5936 + 300.0328 + 543.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER191': class PDFDictionary +214 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 117.3573 + 519.5936 + 138.5845 + 531.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER192': class PDFDictionary +215 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://twill.idyll.org/) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 82.74466 + 477.5936 + 104.4564 + 489.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER193': class PDFDictionary +216 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 127.2882 + 477.5936 + 145.6282 + 489.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER194': class PDFDictionary +217 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 410.1674 + 477.5936 + 433.5592 + 489.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER195': class PDFDictionary +218 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 265.9578 + 453.5936 + 284.2978 + 465.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER196': class PDFDictionary +219 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 487.7492 + 453.5936 + 506.0892 + 465.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER197': class PDFDictionary +220 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://micheles.googlecode.com/hg/plac/doc/plac.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 297.5936 + 157.1829 + 309.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page19': class PDFPage +221 0 obj +% Page dictionary +<< /Annots [ 208 0 R + 209 0 R + 210 0 R + 211 0 R + 212 0 R + 213 0 R + 214 0 R + 215 0 R + 216 0 R + 217 0 R + 218 0 R + 219 0 R + 220 0 R ] + /Contents 355 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page20': class PDFPage +222 0 obj +% Page dictionary +<< /Contents 356 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER198': class PDFDictionary +223 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 383.9329 + 675.3936 + 405.0529 + 687.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page21': class PDFPage +224 0 obj +% Page dictionary +<< /Annots [ 223 0 R ] + /Contents 357 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER199': class PDFDictionary +225 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 370.6785 + 313.3936 + 392.4956 + 325.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER200': class PDFDictionary +226 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 455.8742 + 313.3936 + 477.6913 + 325.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page22': class PDFPage +227 0 obj +% Page dictionary +<< /Annots [ 225 0 R + 226 0 R ] + /Contents 358 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER201': class PDFDictionary +228 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 185.9351 + 756.5936 + 207.4695 + 768.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER202': class PDFDictionary +229 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/shlex.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 369.8905 + 756.5936 + 396.425 + 768.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER203': class PDFDictionary +230 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 408.8916 + 744.5936 + 427.2316 + 756.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER204': class PDFDictionary +231 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/shlex.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 732.5936 + 86.03291 + 744.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER205': class PDFDictionary +232 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 92.4689 + 732.5936 + 114.4649 + 744.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER206': class PDFDictionary +233 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/shlex.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 149.3204 + 720.5936 + 176.9472 + 732.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER207': class PDFDictionary +234 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 129.6923 + 624.5936 + 151.4655 + 636.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER208': class PDFDictionary +235 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 173.8529 + 612.5936 + 194.9729 + 624.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER209': class PDFDictionary +236 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 460.388 + 255.4185 + 482.0127 + 267.4185 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER210': class PDFDictionary +237 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 95.32996 + 189.4185 + 116.857 + 201.4185 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page23': class PDFPage +238 0 obj +% Page dictionary +<< /Annots [ 228 0 R + 229 0 R + 230 0 R + 231 0 R + 232 0 R + 233 0 R + 234 0 R + 235 0 R + 236 0 R + 237 0 R ] + /Contents 359 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER211': class PDFDictionary +239 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://micheles.googlecode.com/hg/plac/doc/plac.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 316.3528 + 346.1936 + 409.2453 + 358.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER212': class PDFDictionary +240 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 419.1694 + 334.1936 + 440.4061 + 346.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER213': class PDFDictionary +241 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 304.1936 + 84.70395 + 316.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER214': class PDFDictionary +242 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 293.2359 + 226.1936 + 316.4697 + 238.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page24': class PDFPage +243 0 obj +% Page dictionary +<< /Annots [ 239 0 R + 240 0 R + 241 0 R + 242 0 R ] + /Contents 360 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER215': class PDFDictionary +244 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 431.7904 + 231.3936 + 453.7245 + 243.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER216': class PDFDictionary +245 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 255.5885 + 201.3936 + 278.2757 + 213.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER217': class PDFDictionary +246 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/cmd.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 513.6927 + 177.3936 + 532.1846 + 189.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page25': class PDFPage +247 0 obj +% Page dictionary +<< /Annots [ 244 0 R + 245 0 R + 246 0 R ] + /Contents 361 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page26': class PDFPage +248 0 obj +% Page dictionary +<< /Contents 362 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page27': class PDFPage +249 0 obj +% Page dictionary +<< /Contents 363 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER218': class PDFDictionary +250 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 179.0529 + 684.5936 + 201.1953 + 696.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER219': class PDFDictionary +251 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://freshmeat.net/projects/rlwrap/) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 377.8504 + 104.6007 + 409.861 + 116.6007 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER220': class PDFDictionary +252 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 242.4466 + 92.60071 + 263.7922 + 104.6007 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER221': class PDFDictionary +253 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://ipython.scipy.org/moin/PyReadline/Intro) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 456.2271 + 92.60071 + 505.3627 + 104.6007 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page28': class PDFPage +254 0 obj +% Page dictionary +<< /Annots [ 250 0 R + 251 0 R + 252 0 R + 253 0 R ] + /Contents 364 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER222': class PDFDictionary +255 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 363.1739 + 744.5936 + 386.5004 + 756.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER223': class PDFDictionary +256 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 479.7508 + 732.5936 + 501.4627 + 744.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER224': class PDFDictionary +257 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/cmd.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 366.9454 + 702.5936 + 388.8033 + 714.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER225': class PDFDictionary +258 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/cmd.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 170.8855 + 690.5936 + 189.7755 + 702.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER226': class PDFDictionary +259 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 390.8027 + 349.3936 + 435.0027 + 361.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER227': class PDFDictionary +260 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 213.451 + 337.3936 + 237.5255 + 349.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER228': class PDFDictionary +261 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 114.9429 + 313.3936 + 157.1829 + 325.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER229': class PDFDictionary +262 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 385.0429 + 313.3936 + 403.3829 + 325.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER230': class PDFDictionary +263 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 350.6129 + 283.3936 + 371.7329 + 295.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER231': class PDFDictionary +264 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 256.6729 + 265.3936 + 277.7929 + 277.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER232': class PDFDictionary +265 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 149.5469 + 172.1936 + 172.1982 + 184.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER233': class PDFDictionary +266 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/distutils/) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 224.3178 + 160.1936 + 260.8853 + 172.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page29': class PDFPage +267 0 obj +% Page dictionary +<< /Annots [ 255 0 R + 256 0 R + 257 0 R + 258 0 R + 259 0 R + 260 0 R + 261 0 R + 262 0 R + 263 0 R + 264 0 R + 265 0 R + 266 0 R ] + /Contents 365 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page30': class PDFPage +268 0 obj +% Page dictionary +<< /Contents 366 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER234': class PDFDictionary +269 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://argparse.googlecode.com) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 381.1529 + 744.5936 + 423.3929 + 756.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER235': class PDFDictionary +270 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 338.9936 + 84.72012 + 350.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page31': class PDFPage +271 0 obj +% Page dictionary +<< /Annots [ 269 0 R + 270 0 R ] + /Contents 367 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page32': class PDFPage +272 0 obj +% Page dictionary +<< /Contents 368 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER236': class PDFDictionary +273 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 182.479 + 352.1936 + 203.7662 + 364.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page33': class PDFPage +274 0 obj +% Page dictionary +<< /Annots [ 273 0 R ] + /Contents 369 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER237': class PDFDictionary +275 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 255.1228 + 382.1936 + 276.5028 + 394.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER238': class PDFDictionary +276 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/multiprocessing.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 295.0229 + 226.9936 + 367.2629 + 238.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER239': class PDFDictionary +277 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 179.1295 + 178.9936 + 201.6839 + 190.9936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page34': class PDFPage +278 0 obj +% Page dictionary +<< /Annots [ 275 0 R + 276 0 R + 277 0 R ] + /Contents 370 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER240': class PDFDictionary +279 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 414.8874 + 346.1936 + 436.9487 + 358.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER241': class PDFDictionary +280 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 122.7054 + 202.1936 + 145.2085 + 214.1936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page35': class PDFPage +281 0 obj +% Page dictionary +<< /Annots [ 279 0 R + 280 0 R ] + /Contents 371 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page36': class PDFPage +282 0 obj +% Page dictionary +<< /Contents 372 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER242': class PDFDictionary +283 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 183.3657 + 431.3936 + 207.8364 + 443.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER243': class PDFDictionary +284 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://docs.python.org/library/multiprocessing.html) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 254.9929 + 407.3936 + 327.2329 + 419.3936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page37': class PDFPage +285 0 obj +% Page dictionary +<< /Annots [ 283 0 R + 284 0 R ] + /Contents 373 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER244': class PDFDictionary +286 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 642.5936 + 85.70846 + 654.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page38': class PDFPage +287 0 obj +% Page dictionary +<< /Annots [ 286 0 R ] + /Contents 374 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER245': class PDFDictionary +288 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 62.69291 + 732.5936 + 84.98766 + 744.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page39': class PDFPage +289 0 obj +% Page dictionary +<< /Annots [ 288 0 R ] + /Contents 375 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page40': class PDFPage +290 0 obj +% Page dictionary +<< /Contents 376 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER246': class PDFDictionary +291 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 473.5049 + 726.5936 + 494.7927 + 738.5936 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER247': class PDFDictionary +292 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 172.4311 + 513.9859 + 194.7087 + 525.9859 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Annot.NUMBER248': class PDFDictionary +293 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 91.57623 + 127.5859 + 114.8995 + 139.5859 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page41': class PDFPage +294 0 obj +% Page dictionary +<< /Annots [ 291 0 R + 292 0 R + 293 0 R ] + /Contents 377 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Annot.NUMBER249': class PDFDictionary +295 0 obj +<< /A << /S /URI + /Type /Action + /URI (http://pypi.python.org/pypi/plac) >> + /Border [ 0 + 0 + 0 ] + /Rect [ 106.6216 + 476.2472 + 128.3202 + 488.2472 ] + /Subtype /Link + /Type /Annot >> +endobj +% 'Page42': class PDFPage +296 0 obj +% Page dictionary +<< /Annots [ 295 0 R ] + /Contents 378 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'Page43': class PDFPage +297 0 obj +% Page dictionary +<< /Contents 379 0 R + /MediaBox [ 0 + 0 + 595.2756 + 841.8898 ] + /Parent 336 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'R298': class PDFCatalog +298 0 obj +% Document Root +<< /Outlines 300 0 R + /PageLabels 380 0 R + /PageMode /UseNone + /Pages 336 0 R + /Type /Catalog >> +endobj +% 'R299': class PDFInfo +299 0 obj +<< /Author () + /CreationDate (D:20100904090628-01'00') + /Keywords () + /Producer (ReportLab http://www.reportlab.com) + /Subject (\(unspecified\)) + /Title () >> +endobj +% 'R300': class PDFOutlines +300 0 obj +<< /Count 37 + /First 301 0 R + /Last 316 0 R + /Type /Outlines >> +endobj +% 'Outline.0': class OutlineEntryObject +301 0 obj +<< /Count 14 + /Dest [ 8 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /First 302 0 R + /Last 315 0 R + /Next 316 0 R + /Parent 300 0 R + /Title (Plac: Parsing the Command Line the Easy Way) >> +endobj +% 'Outline.36.0': class OutlineEntryObject +302 0 obj +<< /Dest [ 95 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Next 303 0 R + /Parent 301 0 R + /Title (The importance of scaling down) >> +endobj +% 'Outline.36.1': class OutlineEntryObject +303 0 obj +<< /Dest [ 95 0 R + /XYZ + 62.69291 + 411.0236 + 0 ] + /Next 304 0 R + /Parent 301 0 R + /Prev 302 0 R + /Title (Scripts with required arguments) >> +endobj +% 'Outline.36.2': class OutlineEntryObject +304 0 obj +<< /Dest [ 109 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Next 305 0 R + /Parent 301 0 R + /Prev 303 0 R + /Title (Scripts with default arguments) >> +endobj +% 'Outline.36.3': class OutlineEntryObject +305 0 obj +<< /Dest [ 119 0 R + /XYZ + 62.69291 + 741.0236 + 0 ] + /Next 306 0 R + /Parent 301 0 R + /Prev 304 0 R + /Title (Scripts with options \(and smart options\)) >> +endobj +% 'Outline.36.4': class OutlineEntryObject +306 0 obj +<< /Dest [ 123 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Next 307 0 R + /Parent 301 0 R + /Prev 305 0 R + /Title (Scripts with flags) >> +endobj +% 'Outline.36.5': class OutlineEntryObject +307 0 obj +<< /Dest [ 123 0 R + /XYZ + 62.69291 + 265.7299 + 0 ] + /Next 308 0 R + /Parent 301 0 R + /Prev 306 0 R + /Title (plac for Python 2.X users) >> +endobj +% 'Outline.36.6': class OutlineEntryObject +308 0 obj +<< /Dest [ 131 0 R + /XYZ + 62.69291 + 522.6236 + 0 ] + /Next 309 0 R + /Parent 301 0 R + /Prev 307 0 R + /Title (More features) >> +endobj +% 'Outline.36.7': class OutlineEntryObject +309 0 obj +<< /Dest [ 136 0 R + /XYZ + 62.69291 + 552.6236 + 0 ] + /Next 310 0 R + /Parent 301 0 R + /Prev 308 0 R + /Title (A realistic example) >> +endobj +% 'Outline.36.8': class OutlineEntryObject +310 0 obj +<< /Dest [ 139 0 R + /XYZ + 62.69291 + 513.8236 + 0 ] + /Next 311 0 R + /Parent 301 0 R + /Prev 309 0 R + /Title (Keyword arguments) >> +endobj +% 'Outline.36.9': class OutlineEntryObject +311 0 obj +<< /Dest [ 141 0 R + /XYZ + 62.69291 + 493.4236 + 0 ] + /Next 312 0 R + /Parent 301 0 R + /Prev 310 0 R + /Title (Final example: a shelve interface) >> +endobj +% 'Outline.36.10': class OutlineEntryObject +312 0 obj +<< /Dest [ 158 0 R + /XYZ + 62.69291 + 422.6236 + 0 ] + /Next 313 0 R + /Parent 301 0 R + /Prev 311 0 R + /Title (plac vs argparse) >> +endobj +% 'Outline.36.11': class OutlineEntryObject +313 0 obj +<< /Dest [ 207 0 R + /XYZ + 62.69291 + 741.0236 + 0 ] + /Next 314 0 R + /Parent 301 0 R + /Prev 312 0 R + /Title (plac vs the rest of the world) >> +endobj +% 'Outline.36.12': class OutlineEntryObject +314 0 obj +<< /Dest [ 207 0 R + /XYZ + 62.69291 + 513.0236 + 0 ] + /Next 315 0 R + /Parent 301 0 R + /Prev 313 0 R + /Title (The future) >> +endobj +% 'Outline.36.13': class OutlineEntryObject +315 0 obj +<< /Dest [ 207 0 R + /XYZ + 62.69291 + 309.0236 + 0 ] + /Parent 301 0 R + /Prev 314 0 R + /Title (Trivia: the story behind the name) >> +endobj +% 'Outline.1': class OutlineEntryObject +316 0 obj +<< /Count 19 + /Dest [ 221 0 R + /XYZ + 62.69291 + 681.0236 + 0 ] + /First 317 0 R + /Last 335 0 R + /Parent 300 0 R + /Prev 301 0 R + /Title (Advanced usages of plac) >> +endobj +% 'Outline.37.0': class OutlineEntryObject +317 0 obj +<< /Dest [ 221 0 R + /XYZ + 62.69291 + 648.0236 + 0 ] + /Next 318 0 R + /Parent 316 0 R + /Title (Introduction) >> +endobj +% 'Outline.37.1': class OutlineEntryObject +318 0 obj +<< /Dest [ 221 0 R + /XYZ + 62.69291 + 426.0236 + 0 ] + /Next 319 0 R + /Parent 316 0 R + /Prev 317 0 R + /Title (From scripts to interactive applications) >> +endobj +% 'Outline.37.2': class OutlineEntryObject +319 0 obj +<< /Dest [ 224 0 R + /XYZ + 62.69291 + 629.8236 + 0 ] + /Next 320 0 R + /Parent 316 0 R + /Prev 318 0 R + /Title (Testing a plac application) >> +endobj +% 'Outline.37.3': class OutlineEntryObject +320 0 obj +<< /Dest [ 227 0 R + /XYZ + 62.69291 + 609.0236 + 0 ] + /Next 321 0 R + /Parent 316 0 R + /Prev 319 0 R + /Title (Plac easy tests) >> +endobj +% 'Outline.37.4': class OutlineEntryObject +321 0 obj +<< /Dest [ 238 0 R + /XYZ + 62.69291 + 299.8485 + 0 ] + /Next 322 0 R + /Parent 316 0 R + /Prev 320 0 R + /Title (Plac batch scripts) >> +endobj +% 'Outline.37.5': class OutlineEntryObject +322 0 obj +<< /Dest [ 243 0 R + /XYZ + 62.69291 + 378.6236 + 0 ] + /Next 323 0 R + /Parent 316 0 R + /Prev 321 0 R + /Title (Implementing subcommands) >> +endobj +% 'Outline.37.6': class OutlineEntryObject +323 0 obj +<< /Dest [ 248 0 R + /XYZ + 62.69291 + 330.0679 + 0 ] + /Next 324 0 R + /Parent 316 0 R + /Prev 322 0 R + /Title (plac.Interpreter.call) >> +endobj +% 'Outline.37.7': class OutlineEntryObject +324 0 obj +<< /Dest [ 254 0 R + /XYZ + 62.69291 + 717.0236 + 0 ] + /Next 325 0 R + /Parent 316 0 R + /Prev 323 0 R + /Title (Readline support) >> +endobj +% 'Outline.37.8': class OutlineEntryObject +325 0 obj +<< /Dest [ 267 0 R + /XYZ + 62.69291 + 204.6236 + 0 ] + /Next 326 0 R + /Parent 316 0 R + /Prev 324 0 R + /Title (The plac runner) >> +endobj +% 'Outline.37.9': class OutlineEntryObject +326 0 obj +<< /Dest [ 271 0 R + /XYZ + 62.69291 + 371.4236 + 0 ] + /Next 327 0 R + /Parent 316 0 R + /Prev 325 0 R + /Title (A non class-based example) >> +endobj +% 'Outline.37.10': class OutlineEntryObject +327 0 obj +<< /Dest [ 274 0 R + /XYZ + 62.69291 + 384.6236 + 0 ] + /Next 328 0 R + /Parent 316 0 R + /Prev 326 0 R + /Title (Writing your own plac runner) >> +endobj +% 'Outline.37.11': class OutlineEntryObject +328 0 obj +<< /Dest [ 278 0 R + /XYZ + 62.69291 + 211.4236 + 0 ] + /Next 329 0 R + /Parent 316 0 R + /Prev 327 0 R + /Title (Long running commands) >> +endobj +% 'Outline.37.12': class OutlineEntryObject +329 0 obj +<< /Dest [ 281 0 R + /XYZ + 62.69291 + 318.6236 + 0 ] + /Next 330 0 R + /Parent 316 0 R + /Prev 328 0 R + /Title (Threaded commands) >> +endobj +% 'Outline.37.13': class OutlineEntryObject +330 0 obj +<< /Dest [ 285 0 R + /XYZ + 62.69291 + 475.8236 + 0 ] + /Next 331 0 R + /Parent 316 0 R + /Prev 329 0 R + /Title (Running commands as external processes) >> +endobj +% 'Outline.37.14': class OutlineEntryObject +331 0 obj +<< /Dest [ 287 0 R + /XYZ + 62.69291 + 675.0236 + 0 ] + /Next 332 0 R + /Parent 316 0 R + /Prev 330 0 R + /Title (Managing the output of concurrent commands) >> +endobj +% 'Outline.37.15': class OutlineEntryObject +332 0 obj +<< /Dest [ 289 0 R + /XYZ + 62.69291 + 765.0236 + 0 ] + /Next 333 0 R + /Parent 316 0 R + /Prev 331 0 R + /Title (Parallel computing with plac) >> +endobj +% 'Outline.37.16': class OutlineEntryObject +333 0 obj +<< /Dest [ 294 0 R + /XYZ + 62.69291 + 630.4159 + 0 ] + /Next 334 0 R + /Parent 316 0 R + /Prev 332 0 R + /Title (The plac server) >> +endobj +% 'Outline.37.17': class OutlineEntryObject +334 0 obj +<< /Dest [ 294 0 R + /XYZ + 62.69291 + 160.0159 + 0 ] + /Next 335 0 R + /Parent 316 0 R + /Prev 333 0 R + /Title (Summary) >> +endobj +% 'Outline.37.18': class OutlineEntryObject +335 0 obj +<< /Dest [ 296 0 R + /XYZ + 62.69291 + 508.6772 + 0 ] + /Parent 316 0 R + /Prev 334 0 R + /Title (Appendix: custom annotation objects) >> +endobj +% 'R336': class PDFPages +336 0 obj +% page tree +<< /Count 43 + /Kids [ 8 0 R + 79 0 R + 95 0 R + 105 0 R + 109 0 R + 113 0 R + 119 0 R + 120 0 R + 123 0 R + 131 0 R + 132 0 R + 136 0 R + 139 0 R + 141 0 R + 144 0 R + 158 0 R + 173 0 R + 207 0 R + 221 0 R + 222 0 R + 224 0 R + 227 0 R + 238 0 R + 243 0 R + 247 0 R + 248 0 R + 249 0 R + 254 0 R + 267 0 R + 268 0 R + 271 0 R + 272 0 R + 274 0 R + 278 0 R + 281 0 R + 282 0 R + 285 0 R + 287 0 R + 289 0 R + 290 0 R + 294 0 R + 296 0 R + 297 0 R ] + /Type /Pages >> +endobj +% 'R337': class PDFStream +337 0 obj +% page stream +<< /Length 2996 >> +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 (Plac: Parsing the Command Line the Easy Way) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 732.0236 cm +Q +q +1 0 0 1 62.69291 717.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F2 10 Tf 12 TL 36.93937 0 Td (Author:) Tj T* -36.93937 0 Td ET +Q +Q +q +1 0 0 1 91.03937 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Michele Simionato) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 702.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F2 10 Tf 12 TL 39.69937 0 Td (E-mail:) Tj T* -39.69937 0 Td ET +Q +Q +q +1 0 0 1 91.03937 3 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (michele.simionato@gmail.com) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 687.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F2 10 Tf 12 TL 48.03937 0 Td (Date:) Tj T* -48.03937 0 Td ET +Q +Q +q +1 0 0 1 91.03937 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (August 2010) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 660.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F2 10 Tf 12 TL 25.25937 0 Td (Download) Tj T* 21.11 0 Td (page:) Tj T* -46.36937 0 Td ET +Q +Q +q +1 0 0 1 91.03937 15 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (http://pypi.python.org/pypi/plac) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 645.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F2 10 Tf 12 TL 9.68937 0 Td (Project page:) Tj T* -9.68937 0 Td ET +Q +Q +q +1 0 0 1 91.03937 3 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (http://micheles.googlecode.com/hg/plac/doc/plac.html) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 630.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F2 10 Tf 12 TL 26.91937 0 Td (Requires:) Tj T* -26.91937 0 Td ET +Q +Q +q +1 0 0 1 91.03937 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Python 2.3+) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 615.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F2 10 Tf 12 TL 16.91937 0 Td (Installation:) Tj T* -16.91937 0 Td ET +Q +Q +q +1 0 0 1 91.03937 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL (easy_install -U plac) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 600.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F2 10 Tf 12 TL 32.46937 0 Td (License:) Tj T* -32.46937 0 Td ET +Q +Q +q +1 0 0 1 91.03937 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (BSD license) 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 +% 'R338': class PDFStream +338 0 obj +% page stream +<< /Length 9594 >> +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 (Contents) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 108.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 0 615 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F2 10 Tf 0 0 .501961 rg (Plac: Parsing the Command Line the Easy Way) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 615 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F2 10 Tf 12 TL 66.44 0 Td (1) Tj T* -66.44 0 Td ET +Q +Q +q +1 0 0 1 0 597 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (The importance of scaling down) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 597 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 66.44 0 Td (3) Tj T* -66.44 0 Td ET +Q +Q +q +1 0 0 1 0 579 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Scripts with required arguments) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 579 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 66.44 0 Td (3) Tj T* -66.44 0 Td ET +Q +Q +q +1 0 0 1 0 561 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Scripts with default arguments) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 561 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 66.44 0 Td (5) Tj T* -66.44 0 Td ET +Q +Q +q +1 0 0 1 0 543 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Scripts with options \(and smart options\)) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 543 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 66.44 0 Td (7) Tj T* -66.44 0 Td ET +Q +Q +q +1 0 0 1 0 525 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Scripts with flags) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 525 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 66.44 0 Td (9) Tj T* -66.44 0 Td ET +Q +Q +q +1 0 0 1 0 507 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (plac for Python 2.X users) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 507 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 66.44 0 Td (9) Tj T* -66.44 0 Td ET +Q +Q +q +1 0 0 1 0 489 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (More features) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 489 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (10) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 471 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (A realistic example) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 471 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (12) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 453 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Keyword arguments) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 453 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (13) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 435 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Final example: a shelve interface) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 435 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (14) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 417 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (plac vs argparse) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 417 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (16) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 399 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (plac vs the rest of the world) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 399 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (18) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 381 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (The future) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 381 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (18) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 363 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Trivia: the story behind the name) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 363 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (18) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 345 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F2 10 Tf 0 0 .501961 rg (Advanced usages of plac) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 345 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F2 10 Tf 12 TL 60.88 0 Td (19) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 327 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Introduction) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 327 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (19) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 309 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (From scripts to interactive applications) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 309 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (19) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 291 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Testing a plac application) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 291 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (21) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 273 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Plac easy tests) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 273 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (22) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 255 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Plac batch scripts) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 255 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (23) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 237 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Implementing subcommands) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 237 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (24) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 219 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (plac.Interpreter.call) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 219 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (26) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 201 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Readline support) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 201 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (28) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 183 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (The plac runner) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 183 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (29) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 165 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (A non class-based example) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 165 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (31) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 147 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Writing your own plac runner) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 147 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (33) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 129 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Long running commands) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 129 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (34) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 111 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Threaded commands) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 111 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (35) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 93 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Running commands as external processes) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 93 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (37) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 75 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Managing the output of concurrent commands) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 75 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (38) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 57 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Parallel computing with plac) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 57 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (39) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 39 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (The plac server) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 39 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (41) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 21 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Summary) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 21 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (41) Tj T* -60.88 0 Td ET +Q +Q +q +1 0 0 1 0 3 cm +q +BT 1 0 0 1 20 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (Appendix: custom annotation objects) Tj T* ET +Q +Q +q +1 0 0 1 397.8898 3 cm +q +0 0 .501961 rg +0 0 .501961 RG +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 60.88 0 Td (42) Tj T* -60.88 0 Td 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 +% 'R339': class PDFStream +339 0 obj +% page stream +<< /Length 5985 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 747.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (The importance of scaling down) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 681.0236 cm +q +BT 1 0 0 1 0 52.82 Tm 1.50936 Tw 12 TL /F1 10 Tf 0 0 0 rg (There is no want of command line arguments parsers in the Python world. The standard library alone) Tj T* 0 Tw 1.087126 Tw (contains three different modules: ) Tj 0 0 .501961 rg (getopt ) Tj 0 0 0 rg (\(from the stone age\), ) Tj 0 0 .501961 rg (optparse ) Tj 0 0 0 rg (\(from Python 2.3\) and ) Tj 0 0 .501961 rg (argparse) Tj T* 0 Tw .223735 Tw 0 0 0 rg (\(from Python 2.7\). All of them are quite powerful and especially ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (is an industrial strength solution;) Tj T* 0 Tw 1.40311 Tw (unfortunately, all of them feature a non-zero learning curve and a certain verbosity. They do not scale) Tj T* 0 Tw (down well, at least in my opinion.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 603.0236 cm +q +BT 1 0 0 1 0 64.82 Tm 2.20186 Tw 12 TL /F1 10 Tf 0 0 0 rg (It should not be necessary to stress the importance ) Tj 0 0 .501961 rg (scaling down) Tj 0 0 0 rg (; nevertheless, a lot of people are) Tj T* 0 Tw .968555 Tw (obsessed with features and concerned with the possibility of scaling up, forgetting the equally important) Tj T* 0 Tw .048221 Tw (issue of scaling down. This is an old meme in the computing world: programs should address the common) Tj T* 0 Tw .36311 Tw (cases simply and simple things should be kept simple, while at the same keeping difficult things possible.) Tj T* 0 Tw 1.09332 Tw 0 0 .501961 rg (plac ) Tj 0 0 0 rg (adhere as much as possible to this philosophy and it is designed to handle well the simple cases,) Tj T* 0 Tw (while retaining the ability to handle complex cases by relying on the underlying power of ) Tj 0 0 .501961 rg (argparse) Tj 0 0 0 rg (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 525.0236 cm +q +BT 1 0 0 1 0 64.82 Tm 1.488221 Tw 12 TL /F1 10 Tf 0 0 0 rg (Technically ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is just a simple wrapper over ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (which hides most of its complexity by using a) Tj T* 0 Tw .203318 Tw (declarative interface: the argument parser is inferred rather than written down by imperatively. Still, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is) Tj T* 0 Tw .125984 Tw (surprisingly scalable upwards, even without using the underlying ) Tj 0 0 .501961 rg (argparse) Tj 0 0 0 rg (. I have been using Python for 8) Tj T* 0 Tw 1.618876 Tw (years and in my experience it is extremely unlikely that you will ever need to go beyond the features) Tj T* 0 Tw 1.776457 Tw (provided by the declarative interface of ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (: they should be more than enough for 99.9% of the use) Tj T* 0 Tw (cases.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 423.0236 cm +q +BT 1 0 0 1 0 88.82 Tm 1.540888 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is targetting especially unsophisticated users, programmers, sys-admins, scientists and in general) Tj T* 0 Tw .81284 Tw (people writing throw-away scripts for themselves, choosing the command line interface because it is the) Tj T* 0 Tw .471751 Tw (quick and simple. Such users are not interested in features, they are interested in a small learning curve:) Tj T* 0 Tw .984988 Tw (they just want to be able to write a simple command line tool from a simple specification, not to build a) Tj T* 0 Tw 1.127318 Tw (command-line parser by hand. Unfortunately, the modules in the standard library forces them to go the) Tj T* 0 Tw .014104 Tw (hard way. They are designed to implement power user tools and they have a non-trivial learning curve. On) Tj T* 0 Tw 1.584104 Tw (the contrary, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is designed to be simple to use and extremely concise, as the examples below will) Tj T* 0 Tw (show.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 393.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Scripts with required arguments) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 327.0236 cm +q +BT 1 0 0 1 0 52.82 Tm .352209 Tw 12 TL /F1 10 Tf 0 0 0 rg (Let me start with the simplest possible thing: a script that takes a single argument and does something to) Tj T* 0 Tw 1.053984 Tw (it. It cannot get simpler than that, unless you consider a script without command-line arguments, where) Tj T* 0 Tw .735488 Tw (there is nothing to parse. Still, it is a use case ) Tj /F4 10 Tf (extremely common) Tj /F1 10 Tf (: I need to write scripts like that nearly) Tj T* 0 Tw .486655 Tw (every day, I wrote hundreds of them in the last few years and I have never been happy. Here is a typical) Tj T* 0 Tw (example of code I have been writing by hand for years:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 125.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 192 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 173.71 Tm /F3 10 Tf 12 TL (# example1.py) Tj T* (def main\(dsn\):) Tj T* ( "Do something with the database") Tj T* ( print\(dsn\)) Tj T* ( # ...) Tj T* T* (if __name__ == '__main__':) Tj T* ( import sys) Tj T* ( n = len\(sys.argv[1:]\)) Tj T* ( if n == 0:) Tj T* ( sys.exit\('usage: python %s dsn' % sys.argv[0]\)) Tj T* ( elif n == 1:) Tj T* ( main\(sys.argv[1]\)) Tj T* ( else:) Tj T* ( sys.exit\('Unrecognized arguments: %s' % ' '.join\(sys.argv[2:]\)\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 93.82362 cm +q +BT 1 0 0 1 0 16.82 Tm .880651 Tw 12 TL /F1 10 Tf 0 0 0 rg (As you see the whole ) Tj /F3 10 Tf (if __name__ == '__main__' ) Tj /F1 10 Tf (block \(nine lines\) is essentially boilerplate that ) Tj T* 0 Tw 1.972927 Tw (should not exist. Actually I think the language should recognize the main function and pass to it the) Tj T* 0 Tw 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 (3) Tj T* -238.1649 0 Td ET +Q +Q + +endstream + +endobj +% 'R340': class PDFStream +340 0 obj +% page stream +<< /Length 4448 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 693.0236 cm +q +BT 1 0 0 1 0 64.82 Tm 3.309147 Tw 12 TL /F1 10 Tf 0 0 0 rg (command-line arguments automatically; unfortunaly this is unlikely to happen. I have been writing) Tj T* 0 Tw 1.767356 Tw (boilerplate like this in hundreds of scripts for years, and every time I ) Tj /F4 10 Tf (hate ) Tj /F1 10 Tf (it. The purpose of using a) Tj T* 0 Tw 1.47229 Tw (scripting language is convenience and trivial things should be trivial. Unfortunately the standard library) Tj T* 0 Tw .69881 Tw (does not help for this incredibly common use case. Using ) Tj 0 0 .501961 rg (getopt ) Tj 0 0 0 rg (and ) Tj 0 0 .501961 rg (optparse ) Tj 0 0 0 rg (does not help, since they) Tj T* 0 Tw .894104 Tw (are intended to manage options and not positional arguments; the ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (module helps a bit and it is) Tj T* 0 Tw (able to reduce the boilerplate from nine lines to six lines:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 527.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 (# example2.py) Tj T* (def main\(dsn\):) Tj T* ( "Do something on the database") Tj T* ( print\(dsn\)) Tj T* ( # ...) Tj T* T* (if __name__ == '__main__':) Tj T* ( import argparse) Tj T* ( p = argparse.ArgumentParser\(\)) Tj T* ( p.add_argument\('dsn'\)) Tj T* ( arg = p.parse_args\(\)) Tj T* ( main\(arg.dsn\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 471.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 40.82 Tm /F1 10 Tf 12 TL 1.644269 Tw (However saving three lines does not justify introducing the external dependency: most people will not) Tj T* 0 Tw 2.206303 Tw (switch to Python 2.7, which at the time of this writing is just about to be released, for many years.) Tj T* 0 Tw .678488 Tw (Moreover, it just feels too complex to instantiate a class and to define a parser by hand for such a trivial) Tj T* 0 Tw (task.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 441.8236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.123145 Tw 12 TL /F1 10 Tf 0 0 0 rg (The ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (module is designed to manage well such use cases, and it is able to reduce the original nine) Tj T* 0 Tw (lines of boiler plate to two lines. With the ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (module all you need to write is) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 324.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 108 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 89.71 Tm /F3 10 Tf 12 TL (# example3.py) Tj T* (def main\(dsn\):) Tj T* ( "Do something with the database") Tj T* ( print\(dsn\)) Tj T* ( # ...) Tj T* ( ) Tj 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 292.6236 cm +q +BT 1 0 0 1 0 16.82 Tm .929986 Tw 12 TL /F1 10 Tf 0 0 0 rg (The ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (module provides for free \(actually the work is done by the underlying ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (module\) a nice) Tj T* 0 Tw (usage message:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 259.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 24 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL ($ python example3.py -h) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 130.2236 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 (usage: example3.py [-h] dsn) Tj T* T* (Do something with the database) Tj T* T* (positional arguments:) Tj T* ( dsn) Tj T* 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 98.22362 cm +q +BT 1 0 0 1 0 16.82 Tm .167765 Tw 12 TL /F1 10 Tf 0 0 0 rg (Moreover ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (manages the case of missing arguments and of too many arguments. This is only the tip of) Tj T* 0 Tw (the iceberg: ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is able to do much more than that.) 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 +% 'R341': class PDFStream +341 0 obj +% page stream +<< /Length 4050 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 747.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Scripts with default arguments) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 717.0236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL 2.609984 Tw (The need to have suitable defaults for command-line scripts is quite common. For instance I have) Tj T* 0 Tw (encountered this use case at work hundreds of times:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 515.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 192 re B* +Q +q +BT 1 0 0 1 0 173.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (# example4.py) Tj T* (from datetime import datetime) Tj T* T* (def main\(dsn, table='product', today=datetime.today\(\)\):) Tj T* ( "Do something on the database") Tj T* ( print\(dsn, table, today\)) Tj T* T* (if __name__ == '__main__':) Tj T* ( import sys) Tj T* ( args = sys.argv[1:]) Tj T* ( if not args:) Tj T* ( sys.exit\('usage: python %s dsn' % sys.argv[0]\)) Tj T* ( elif len\(args\) ) Tj (>) Tj ( 2:) Tj T* ( sys.exit\('Unrecognized arguments: %s' % ' '.join\(argv[2:]\)\)) Tj T* ( main\(*args\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 447.8236 cm +q +BT 1 0 0 1 0 52.82 Tm .038488 Tw 12 TL /F1 10 Tf 0 0 0 rg (Here I want to perform a query on a database table, by extracting the most recent data: it makes sense for) Tj T* 0 Tw .299988 Tw /F3 10 Tf (today ) Tj /F1 10 Tf (to be a default argument. If there is a most used table \(in this example a table called ) Tj /F3 10 Tf ('product') Tj /F1 10 Tf (\)) Tj T* 0 Tw 3.313984 Tw (it also makes sense to make it a default argument. Performing the parsing of the command-line) Tj T* 0 Tw .083735 Tw (arguments by hand takes 8 ugly lines of boilerplate \(using ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (would require about the same number) Tj T* 0 Tw (of lines\). With ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (the entire ) Tj /F3 10 Tf (__main__ ) Tj /F1 10 Tf (block reduces to the usual two lines:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 402.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 36 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 17.71 Tm /F3 10 Tf 12 TL (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 382.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (In other words, six lines of boilerplate have been removed, and we get the usage message for free:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 229.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 144 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 125.71 Tm /F3 10 Tf 12 TL (usage: example5.py [-h] dsn [table] [today]) Tj T* T* (Do something on the database) Tj T* T* (positional arguments:) Tj T* ( dsn) Tj T* ( table) Tj T* ( today) Tj T* 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 197.4236 cm +q +BT 1 0 0 1 0 16.82 Tm .396235 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (manages transparently even the case when you want to pass a variable number of arguments. Here) Tj T* 0 Tw (is an example, a script running on a database a series of SQL scripts:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 92.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 96 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 77.71 Tm /F3 10 Tf 12 TL (# example7.py) Tj T* (from datetime import datetime) Tj T* T* (def main\(dsn, *scripts\):) Tj T* ( "Run the given scripts on the database") Tj T* ( for script in scripts:) Tj T* ( print\('executing %s' % script\)) 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 +% 'R342': class PDFStream +342 0 obj +% page stream +<< /Length 3901 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 703.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 ( # ...) 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 683.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 542.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: example7.py [-h] dsn [scripts [scripts ...]]) Tj T* T* (Run the given scripts on the database) Tj T* T* (positional arguments:) Tj T* ( dsn) Tj T* ( scripts) Tj T* 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 498.6236 cm +q +BT 1 0 0 1 0 28.82 Tm .952485 Tw 12 TL /F1 10 Tf 0 0 0 rg (The examples here should have made clear that ) Tj /F4 10 Tf (plac is able to figure out the command-line arguments) Tj T* 0 Tw .899988 Tw (parser to use from the signature of the main function) Tj /F1 10 Tf (. This is the whole idea behind ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (: if the intent is) Tj T* 0 Tw (clear, let's the machine take care of the details.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 456.6236 cm +q +BT 1 0 0 1 0 28.82 Tm 3.036235 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is inspired to an old Python Cookbook recipe \() Tj 0 0 .501961 rg (optionparse) Tj 0 0 0 rg (\), in the sense that it delivers the) Tj T* 0 Tw .847209 Tw (programmer from the burden of writing the parser, but is less of a hack: instead of extracting the parser) Tj T* 0 Tw (from the docstring of the module, it extracts it from the signature of the ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (function.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 426.6236 cm +q +BT 1 0 0 1 0 16.82 Tm .319987 Tw 12 TL /F1 10 Tf 0 0 0 rg (The idea comes from the ) Tj /F4 10 Tf (function annotations ) Tj /F1 10 Tf (concept, a new feature of Python 3. An example is worth a) Tj T* 0 Tw (thousand words, so here it is:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 273.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 144 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 125.71 Tm /F3 10 Tf 12 TL (# example7_.py) Tj T* (from datetime import datetime) Tj T* T* (def main\(dsn: "Database dsn", *scripts: "SQL scripts"\):) Tj T* ( "Run the given scripts on the database") Tj T* ( for script in scripts:) Tj T* ( print\('executing %s' % script\)) Tj T* ( # ...) 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 241.4236 cm +q +BT 1 0 0 1 0 16.82 Tm .17528 Tw 12 TL /F1 10 Tf 0 0 0 rg (Here the arguments of the ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (function have been annotated with strings which are intented to be used) Tj T* 0 Tw (in the help message:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 100.2236 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: example7_.py [-h] dsn [scripts [scripts ...]]) Tj T* T* (Run the given scripts on the database) Tj T* T* (positional arguments:) Tj T* ( dsn Database dsn) Tj T* ( scripts SQL scripts) Tj T* 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 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 +% 'R343': class PDFStream +343 0 obj +% page stream +<< /Length 5019 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 753.0236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is able to recognize much more complex annotations, as I will show in the next paragraphs.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 723.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Scripts with options \(and smart options\)) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 633.0236 cm +q +BT 1 0 0 1 0 76.82 Tm .016457 Tw 12 TL /F1 10 Tf 0 0 0 rg (It is surprising how few command-line scripts with options I have written over the years \(probably less than) Tj T* 0 Tw 1.02311 Tw (a hundred\), compared to the number of scripts with positional arguments I wrote \(certainly more than a) Tj T* 0 Tw .177045 Tw (thousand of them\). Still, this use case cannot be neglected. The standard library modules \(all of them\) are) Tj T* 0 Tw 2.30686 Tw (quite verbose when it comes to specifying the options and frankly I have never used them directly.) Tj T* 0 Tw 2.557126 Tw (Instead, I have always relied on the ) Tj 0 0 .501961 rg (optionparse ) Tj 0 0 0 rg (recipe, which provides a convenient wrapper over) Tj T* 0 Tw 1.09061 Tw 0 0 .501961 rg (optionparse) Tj 0 0 0 rg (. Alternatively, in the simplest cases, I have just performed the parsing by hand. In ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (the) Tj T* 0 Tw (parser is inferred by the function annotations. Here is an example:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 515.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 +0 0 0 rg +BT 1 0 0 1 0 89.71 Tm /F3 10 Tf 12 TL (# example8.py) Tj T* (def main\(command: \("SQL query", 'option', 'c'\), dsn\):) Tj T* ( if command:) Tj T* ( print\('executing %s on %s' % \(command, dsn\)\)) Tj T* ( # ...) 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 459.8236 cm +q +BT 1 0 0 1 0 40.82 Tm .929213 Tw 12 TL /F1 10 Tf 0 0 0 rg (Here the argument ) Tj /F3 10 Tf (command ) Tj /F1 10 Tf (has been annotated with the tuple ) Tj /F3 10 Tf (\("SQL query", 'option', 'c'\)) Tj /F1 10 Tf (:) Tj T* 0 Tw .62683 Tw (the first string is the help string which will appear in the usage message, the second string tells ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (that) Tj T* 0 Tw .931894 Tw /F3 10 Tf (command ) Tj /F1 10 Tf (is an option and the third string that there is also a short form of the option ) Tj /F3 10 Tf (-c) Tj /F1 10 Tf (, the long form) Tj T* 0 Tw (being ) Tj /F3 10 Tf (--command) Tj /F1 10 Tf (. The usage message is the following:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 330.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 (usage: example8.py [-h] [-c COMMAND] dsn) Tj T* T* (positional arguments:) Tj T* ( dsn) Tj T* T* (optional arguments:) Tj T* ( -h, --help show this help message and exit) Tj T* ( -c COMMAND, --command COMMAND) Tj T* ( SQL query) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 310.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here are two examples of usage:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 229.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 72 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 53.71 Tm /F3 10 Tf 12 TL ($ python3 example8.py -c"select * from table" dsn) Tj T* (executing select * from table on dsn) Tj T* T* ($ python3 example8.py --command="select * from table" dsn) Tj T* (executing select * from table on dsn) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 173.4236 cm +q +BT 1 0 0 1 0 40.82 Tm .268935 Tw 12 TL /F1 10 Tf 0 0 0 rg (The third argument in the function annotation can be omitted: in such case it will be assumed to be ) Tj /F3 10 Tf (None) Tj /F1 10 Tf (.) Tj T* 0 Tw 2.839213 Tw (The consequence is that the usual dichotomy between long and short options \(GNU-style options\)) Tj T* 0 Tw .396235 Tw (disappears: we get ) Tj /F4 10 Tf (smart options) Tj /F1 10 Tf (, which have the single character prefix of short options and behave like) Tj T* 0 Tw (both long and short options, since they can be abbreviated. Here is an example featuring smart options:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 92.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 72 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 53.71 Tm /F3 10 Tf 12 TL (# example6.py) Tj T* (def main\(dsn, command: \("SQL query", 'option'\)\):) Tj T* ( print\('executing %r on %s' % \(command, dsn\)\)) Tj T* T* (if __name__ == '__main__':) 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 (7) Tj T* -238.1649 0 Td ET +Q +Q + +endstream + +endobj +% 'R344': class PDFStream +344 0 obj +% page stream +<< /Length 4318 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 739.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 24 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL ( import plac; plac.call\(main\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 622.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 108 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 89.71 Tm /F3 10 Tf 12 TL (usage: example6.py [-h] [-command COMMAND] dsn) Tj T* T* (positional arguments:) Tj T* ( dsn) Tj T* T* (optional arguments:) Tj T* ( -h, --help show this help message and exit) Tj T* ( -command COMMAND SQL query) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 602.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (The following are all valid invocations ot the script:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 509.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 84 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 65.71 Tm /F3 10 Tf 12 TL ($ python3 example6.py -c "select" dsn) Tj T* (executing 'select' on dsn) Tj T* ($ python3 example6.py -com "select" dsn) Tj T* (executing 'select' on dsn) Tj T* ($ python3 example6.py -command="select" dsn) Tj T* (executing 'select' on dsn) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 489.4236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (Notice that the form ) Tj /F3 10 Tf (-command=SQL ) Tj /F1 10 Tf (is recognized only for the full option, not for its abbreviations:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 432.2236 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 ($ python3 example6.py -com="select" dsn) Tj T* (usage: example6.py [-h] [-command COMMAND] dsn) Tj T* (example6.py: error: unrecognized arguments: -com=select) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 400.2236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.724987 Tw 12 TL /F1 10 Tf 0 0 0 rg (If the option is not passed, the variable ) Tj /F3 10 Tf (command ) Tj /F1 10 Tf (will get the value ) Tj /F3 10 Tf (None) Tj /F1 10 Tf (. However, it is possible to) Tj T* 0 Tw (specify a non-trivial default. Here is an example:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 307.0236 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 (# example8_.py) Tj T* (def main\(dsn, command: \("SQL query", 'option'\)='select * from table'\):) Tj T* ( print\('executing %r on %s' % \(command, dsn\)\)) 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 287.0236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Notice that the default value appears in the help message:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 157.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 120 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 101.71 Tm /F3 10 Tf 12 TL (usage: example8_.py [-h] [-command select * from table] dsn) Tj T* T* (positional arguments:) Tj T* ( dsn) Tj T* T* (optional arguments:) Tj T* ( -h, --help show this help message and exit) Tj T* ( -command select * from table) Tj T* ( SQL query) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 137.8236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (When you run the script and you do not pass the ) Tj /F3 10 Tf (-command ) Tj /F1 10 Tf (option, the default query will be executed:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 92.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 36 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 17.71 Tm /F3 10 Tf 12 TL ($ python3 example8_.py dsn) Tj T* (executing 'select * from table' on dsn) 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 +% 'R345': class PDFStream +345 0 obj +% page stream +<< /Length 4916 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 747.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Scripts with flags) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 717.0236 cm +q +BT 1 0 0 1 0 16.82 Tm .815542 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is able to recognize flags, i.e. boolean options which are ) Tj /F3 10 Tf (True ) Tj /F1 10 Tf (if they are passed to the command) Tj T* 0 Tw (line and ) Tj /F3 10 Tf (False ) Tj /F1 10 Tf (if they are absent. Here is an example:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 592.1299 cm +q +q +.96447 0 0 .96447 0 0 cm +q +1 0 0 1 6.6 6.843137 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 486 120 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 101.71 Tm /F3 10 Tf 12 TL (# example9.py) Tj T* T* (def main\(verbose: \('prints more info', 'flag', 'v'\), dsn: 'connection string'\):) Tj T* ( if verbose:) Tj T* ( print\('connecting to %s' % dsn\)) Tj T* ( # ...) 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 474.9299 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 +0 0 0 rg +BT 1 0 0 1 0 89.71 Tm /F3 10 Tf 12 TL (usage: example9.py [-h] [-v] dsn) Tj T* T* (positional arguments:) Tj T* ( dsn connection string) Tj T* T* (optional arguments:) Tj T* ( -h, --help show this help message and exit) Tj T* ( -v, --verbose prints more info) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 429.7299 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 ($ python3 example9.py -v dsn) Tj T* (connecting to dsn) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 385.7299 cm +q +BT 1 0 0 1 0 28.82 Tm .31408 Tw 12 TL /F1 10 Tf 0 0 0 rg (Notice that it is an error trying to specify a default for flags: the default value for a flag is always ) Tj /F3 10 Tf (False) Tj /F1 10 Tf (. If) Tj T* 0 Tw 2.652485 Tw (you feel the need to implement non-boolean flags, you should use an option with two choices, as) Tj T* 0 Tw (explained in the "more features" section.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 319.7299 cm +q +BT 1 0 0 1 0 52.82 Tm 5.832651 Tw 12 TL /F1 10 Tf 0 0 0 rg (For consistency with the way the usage message is printed, I suggest you to follow the) Tj T* 0 Tw 1.895433 Tw (Flag-Option-Required-Default \(FORD\) convention: in the ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (function write first the flag arguments,) Tj T* 0 Tw .881235 Tw (then the option arguments, then the required arguments and finally the default arguments. This is just a) Tj T* 0 Tw .110574 Tw (convention and you are not forced to use it, except for the default arguments \(including the varargs\) which) Tj T* 0 Tw (must stay at the end as it is required by the Python syntax.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 277.7299 cm +q +BT 1 0 0 1 0 28.82 Tm .897045 Tw 12 TL /F1 10 Tf 0 0 0 rg (I also suggests to specify a one-character abbreviation for flags: in this way you can use the GNU-style) Tj T* 0 Tw 2.034431 Tw (composition of flags \(i.e. ) Tj /F3 10 Tf (-zxvf ) Tj /F1 10 Tf (is an abbreviation of ) Tj /F3 10 Tf (-z -x -v -f) Tj /F1 10 Tf (\). I usually do not provide the) Tj T* 0 Tw (one-character abbreviation for options, since it does not make sense to compose them.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 247.7299 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (plac for Python 2.X users) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 181.7299 cm +q +BT 1 0 0 1 0 52.82 Tm .211807 Tw 12 TL /F1 10 Tf 0 0 0 rg (I do not use Python 3. At work we are just starting to think about migrating to Python 2.6. It will take years) Tj T* 0 Tw .304724 Tw (before we think to migrate to Python 3. I am pretty much sure most Pythonistas are in the same situation.) Tj T* 0 Tw 1.459984 Tw (Therefore ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (provides a way to work with function annotations even in Python 2.X \(including Python) Tj T* 0 Tw 2.692339 Tw (2.3\). There is no magic involved; you just need to add the annotations by hand. For instance the) Tj T* 0 Tw (annotated function declaration) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 136.5299 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 (def main\(dsn: "Database dsn", *scripts: "SQL scripts"\):) Tj T* ( ...) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 116.5299 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (is equivalent to the following code:) 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 +% 'R346': class PDFStream +346 0 obj +% page stream +<< /Length 6172 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 691.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 72 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 53.71 Tm /F3 10 Tf 12 TL (def main\(dsn, *scripts\):) Tj T* ( ...) Tj T* (main.__annotations__ = dict\() Tj T* ( dsn="Database dsn",) Tj T* ( scripts="SQL scripts"\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 647.8236 cm +q +BT 1 0 0 1 0 28.82 Tm .536098 Tw 12 TL /F1 10 Tf 0 0 0 rg (One should be careful to match the keys of the annotation dictionary with the names of the arguments in) Tj T* 0 Tw 3.347485 Tw (the annotated function; for lazy people with Python 2.4 available the simplest way is to use the) Tj T* 0 Tw /F3 10 Tf (plac.annotations ) Tj /F1 10 Tf (decorator that performs the check for you:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 566.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 72 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 53.71 Tm /F3 10 Tf 12 TL (@plac.annotations\() Tj T* ( dsn="Database dsn",) Tj T* ( scripts="SQL scripts"\)) Tj T* (def main\(dsn, *scripts\):) Tj T* ( ...) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 534.6236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.846077 Tw 12 TL /F1 10 Tf 0 0 0 rg (In the rest of this article I will assume that you are using Python 2.X with X >) Tj (= 4 and I will use the) Tj T* 0 Tw /F3 10 Tf (plac.annotations ) Tj /F1 10 Tf (decorator. Notice however that the core features of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (run even on Python 2.3.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 504.6236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (More features) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 450.6236 cm +q +BT 1 0 0 1 0 40.82 Tm 1.483488 Tw 12 TL /F1 10 Tf 0 0 0 rg (One of the goals of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is to have a learning curve of ) Tj /F4 10 Tf (minutes ) Tj /F1 10 Tf (for its core features, compared to the) Tj T* 0 Tw 1.152093 Tw (learning curve of ) Tj /F4 10 Tf (hours ) Tj /F1 10 Tf (of ) Tj 0 0 .501961 rg (argparse) Tj 0 0 0 rg (. In order to reach this goal, I have ) Tj /F4 10 Tf (not ) Tj /F1 10 Tf (sacrificed all the features of) Tj T* 0 Tw 2.89936 Tw 0 0 .501961 rg (argparse) Tj 0 0 0 rg (. Actually a lot of ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (power persists in ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (. Until now, I have only showed simple) Tj T* 0 Tw (annotations, but in general an annotation is a 6-tuple of the form) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 444.6236 cm +Q +q +1 0 0 1 62.69291 432.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +BT 1 0 0 1 0 2 Tm T* ET +q +1 0 0 1 20 0 cm +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL (\(help, kind, abbrev, type, choices, metavar\)) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 432.6236 cm +Q +q +1 0 0 1 62.69291 390.6236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.068735 Tw 12 TL /F1 10 Tf 0 0 0 rg (where ) Tj /F3 10 Tf (help ) Tj /F1 10 Tf (is the help message, ) Tj /F3 10 Tf (kind ) Tj /F1 10 Tf (is a string in the set { ) Tj /F3 10 Tf ("flag") Tj /F1 10 Tf (, ) Tj /F3 10 Tf ("option") Tj /F1 10 Tf (, ) Tj /F3 10 Tf ("positional") Tj /F1 10 Tf (},) Tj T* 0 Tw 1.579431 Tw /F3 10 Tf (abbrev ) Tj /F1 10 Tf (is a one-character string or ) Tj /F3 10 Tf (None) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (type ) Tj /F1 10 Tf (is a callable taking a string in input, ) Tj /F3 10 Tf (choices ) Tj /F1 10 Tf (is a) Tj T* 0 Tw (discrete sequence of values and ) Tj /F3 10 Tf (metavar ) Tj /F1 10 Tf (is a string.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 360.6236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.05061 Tw 12 TL /F3 10 Tf 0 0 0 rg (type ) Tj /F1 10 Tf (is used to automagically convert the command line arguments from the string type to any Python) Tj T* 0 Tw (type; by default there is no conversion and ) Tj /F3 10 Tf (type=None) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 330.6236 cm +q +BT 1 0 0 1 0 16.82 Tm 2.904692 Tw 12 TL /F3 10 Tf 0 0 0 rg (choices ) Tj /F1 10 Tf (is used to restrict the number of the valid options; by default there is no restriction i.e.) Tj T* 0 Tw /F3 10 Tf (choices=None) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 252.6236 cm +q +BT 1 0 0 1 0 64.82 Tm 1.171163 Tw 12 TL /F3 10 Tf 0 0 0 rg (metavar ) Tj /F1 10 Tf (has two meanings. For a positional argument it is used to change the argument name in the) Tj T* 0 Tw .352209 Tw (usage message \(and only there\). By default the metavar is ) Tj /F3 10 Tf (None ) Tj /F1 10 Tf (and the name in the usage message is) Tj T* 0 Tw .752339 Tw (the same as the argument name. For an option the ) Tj /F3 10 Tf (metavar ) Tj /F1 10 Tf (is used differently in the usage message,) Tj T* 0 Tw .802927 Tw (which has now the form ) Tj /F3 10 Tf ([--option-name METAVAR]) Tj /F1 10 Tf (. If the ) Tj /F3 10 Tf (metavar ) Tj /F1 10 Tf (is ) Tj /F3 10 Tf (None) Tj /F1 10 Tf (, then it is equal to the) Tj T* 0 Tw .50683 Tw (uppercased name of the argument, unless the argument has a default and in such a case is equal to the) Tj T* 0 Tw (stringified form of the default.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 234.6236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (Here is an example showing many of the features \(copied from the ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (documentation\):) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 96.55439 cm +q +q +.976496 0 0 .976496 0 0 cm +q +1 0 0 1 6.6 6.758862 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 480 132 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 113.71 Tm /F3 10 Tf 12 TL (# example10.py) Tj T* (import plac) Tj T* T* (@plac.annotations\() Tj T* (operator=\("The name of an operator", 'positional', None, str, ['add', 'mul']\),) Tj T* (numbers=\("A number", 'positional', None, float, None, "n"\)\)) Tj T* (def main\(operator, *numbers\):) Tj T* ( "A script to add and multiply numbers") Tj T* ( if operator == 'mul':) Tj T* ( op = float.__mul__) 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 235.3849 0 Td (10) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R347': class PDFStream +347 0 obj +% page stream +<< /Length 4255 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 631.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 132 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 113.71 Tm /F3 10 Tf 12 TL ( result = 1.0) Tj T* ( else: # operator == 'add') Tj T* ( op = float.__add__) Tj T* ( result = 0.0) Tj T* ( for n in numbers:) Tj T* ( result = op\(result, n\)) Tj T* ( return result) Tj T* T* (if __name__ == '__main__':) Tj T* ( print\(plac.call\(main\)\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 611.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:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 470.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: example10.py [-h] {add,mul} [n [n ...]]) Tj T* T* (A script to add and multiply numbers) Tj T* T* (positional arguments:) Tj T* ( {add,mul} The name of an operator) Tj T* ( n A number) Tj T* 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 438.6236 cm +q +BT 1 0 0 1 0 16.82 Tm .15186 Tw 12 TL /F1 10 Tf 0 0 0 rg (Notice that the docstring of the ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (function has been automatically added to the usage message. Here) Tj T* 0 Tw (are a couple of examples of use:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 345.2849 cm +q +q +.87797 0 0 .87797 0 0 cm +q +1 0 0 1 6.6 7.517338 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 534 96 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 77.71 Tm /F3 10 Tf 12 TL ($ python example10.py add 1 2 3 4) Tj T* (10.0) Tj T* ($ python example10.py mul 1 2 3 4) Tj T* (24.0) Tj T* ($ python example10.py ad 1 2 3 4 # a mispelling error) Tj T* (usage: example10.py [-h] {add,mul} [n [n ...]]) Tj T* (example10.py: error: argument operator: invalid choice: 'ad' \(choose from 'add', 'mul'\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 325.2849 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F3 10 Tf 0 0 0 rg (plac.call ) Tj /F1 10 Tf (can also be used in doctests like this:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 268.0849 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 +BT 1 0 0 1 0 29.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (>) Tj (>) Tj (>) Tj ( import plac, example10) Tj T* (>) Tj (>) Tj (>) Tj ( plac.call\(example10.main, ['add', '1', '2']\)) Tj T* (3.0) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 248.0849 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F3 10 Tf 0 0 0 rg (plac.call ) Tj /F1 10 Tf (works for generators too:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 166.8849 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 72 re B* +Q +q +BT 1 0 0 1 0 53.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (>) Tj (>) Tj (>) Tj ( def main\(n\):) Tj T* (... for i in range\(int\(n\)\):) Tj T* (... yield i) Tj T* (>) Tj (>) Tj (>) Tj ( plac.call\(main, ['3']\)) Tj T* ([0, 1, 2]) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 122.8849 cm +q +BT 1 0 0 1 0 28.82 Tm .158409 Tw 12 TL /F1 10 Tf 0 0 0 rg (Internally ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (tries to convert the output of the main function into a list, if possible. If the output is) Tj T* 0 Tw .725703 Tw (not iterable or it is a string, it is left unchanged, but if it is iterable it is converted. In particular, generator) Tj T* 0 Tw (objects are exhausted by ) Tj /F3 10 Tf (plac.call) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 92.8849 cm +q +BT 1 0 0 1 0 16.82 Tm 1.450751 Tw 12 TL /F1 10 Tf 0 0 0 rg (This behavior avoids mistakes like forgetting of applying ) Tj /F3 10 Tf (list\(result\) ) Tj /F1 10 Tf (to the result of ) Tj /F3 10 Tf (plac.call) Tj /F1 10 Tf (;) Tj T* 0 Tw (moreover it makes errors visible early, and avoids mistakes in code like the following:) 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 235.3849 0 Td (11) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R348': class PDFStream +348 0 obj +% page stream +<< /Length 4298 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 703.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 (try:) Tj T* ( result = plac.call\(main, args\)) Tj T* (except:) Tj T* ( # do something) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 671.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL 3.122126 Tw (Without the "listify" functionality, a main function returning a generator object would not raise any) Tj T* 0 Tw (exception until the generator is iterated over.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 641.8236 cm +q +BT 1 0 0 1 0 16.82 Tm .647262 Tw 12 TL /F1 10 Tf 0 0 0 rg (If you are a fan of lazyness, you can still have it by setting the ) Tj /F3 10 Tf (eager ) Tj /F1 10 Tf (flag to ) Tj /F3 10 Tf (False) Tj /F1 10 Tf (, as in the following) Tj T* 0 Tw (example:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 596.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 36 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 17.71 Tm /F3 10 Tf 12 TL (for line in plac.call\(main, args, eager=False\):) Tj T* ( print\(line\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 564.6236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.35528 Tw 12 TL /F1 10 Tf 0 0 0 rg (If ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (returns a generator object this example will print each line as soon as available, whereas the) Tj T* 0 Tw (default behaviour is to print all the lines together and the end of the computation.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 534.6236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (A realistic example) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 492.6236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.234488 Tw 12 TL /F1 10 Tf 0 0 0 rg (Here is a more realistic script using most of the features of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (to run SQL queries on a database by) Tj T* 0 Tw .930697 Tw (relying on ) Tj 0 0 .501961 rg (SQLAlchemy) Tj 0 0 0 rg (. Notice the usage of the ) Tj /F3 10 Tf (type ) Tj /F1 10 Tf (feature to automagically convert a SQLAlchemy) Tj T* 0 Tw (connection string into a ) Tj 0 0 .501961 rg (SqlSoup ) Tj 0 0 0 rg (object:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 123.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 360 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 341.71 Tm /F3 10 Tf 12 TL (# dbcli.py) Tj T* (import plac) Tj T* (from sqlalchemy.ext.sqlsoup import SqlSoup) Tj T* T* (@plac.annotations\() Tj T* ( db=\("Connection string", 'positional', None, SqlSoup\),) Tj T* ( header=\("Header", 'flag', 'H'\),) Tj T* ( sqlcmd=\("SQL command", 'option', 'c', str, None, "SQL"\),) Tj T* ( delimiter=\("Column separator", 'option', 'd'\),) Tj T* ( scripts="SQL scripts",) Tj T* ( \)) Tj T* (def main\(db, header, sqlcmd, delimiter="|", *scripts\):) Tj T* ( "A script to run queries and SQL scripts on a database") Tj T* ( yield 'Working on %s' % db.bind.url) Tj T* T* ( if sqlcmd:) Tj T* ( result = db.bind.execute\(sqlcmd\)) Tj T* ( if header: # print the header) Tj T* ( yield delimiter.join\(result.keys\(\)\)) Tj T* ( for row in result: # print the rows) Tj T* ( yield delimiter.join\(map\(str, row\)\)) Tj T* T* ( for script in scripts:) Tj T* ( db.bind.execute\(file\(script\).read\(\)\)) Tj T* ( yield 'executed %s' % script) Tj T* T* (if __name__ == '__main__':) Tj T* ( for output in plac.call\(main\):) Tj T* ( print\(output\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 91.42362 cm +q +BT 1 0 0 1 0 16.82 Tm .049987 Tw 12 TL /F1 10 Tf 0 0 0 rg (You can see the ) Tj /F4 10 Tf (yield-is-print ) Tj /F1 10 Tf (pattern here: instead of using ) Tj /F3 10 Tf (print ) Tj /F1 10 Tf (in the main function, I use ) Tj /F3 10 Tf (yield) Tj /F1 10 Tf (, and ) Tj T* 0 Tw 3.55061 Tw (I perform the print in the ) Tj /F3 10 Tf (__main__ ) Tj /F1 10 Tf (block. The advantage of the pattern is that tests invoking) Tj T* 0 Tw 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 235.3849 0 Td (12) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R349': class PDFStream +349 0 obj +% page stream +<< /Length 3562 >> +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 16.82 Tm .52936 Tw 12 TL /F3 10 Tf 0 0 0 rg (plac.call ) Tj /F1 10 Tf (and checking the result become trivial: had I performed the printing in the main function, the) Tj T* 0 Tw (test would have involved an ugly hack like redirecting ) Tj /F3 10 Tf (sys.stdout ) Tj /F1 10 Tf (to a ) Tj /F3 10 Tf (StringIO ) Tj /F1 10 Tf (object.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 723.0236 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 545.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 168 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 149.71 Tm /F3 10 Tf 12 TL (usage: dbcli.py [-h] [-H] [-c SQL] [-d |] db [scripts [scripts ...]]) Tj T* T* (A script to run queries and SQL scripts on a database) Tj T* T* (positional arguments:) Tj T* ( db Connection string) Tj T* ( scripts SQL scripts) Tj T* T* (optional arguments:) Tj T* ( -h, --help show this help message and exit) Tj T* ( -H, --header Header) Tj T* ( -c SQL, --sqlcmd SQL SQL command) Tj T* ( -d |, --delimiter | Column separator) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 525.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (You can check for yourself that the script works.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 495.8236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Keyword arguments) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 453.8236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.831984 Tw 12 TL /F1 10 Tf 0 0 0 rg (Starting from release 0.4, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (supports keyword arguments. In practice that means that if your main) Tj T* 0 Tw 2.099213 Tw (function has keyword arguments, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (treats specially arguments of the form ) Tj /F3 10 Tf ("name=value" ) Tj /F1 10 Tf (in the) Tj T* 0 Tw (command line. Here is an example:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 216.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 228 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 209.71 Tm /F3 10 Tf 12 TL (# example12.py) Tj T* (import plac) Tj T* T* (@plac.annotations\() Tj T* ( opt=\('some option', 'option'\),) Tj T* ( args='default arguments',) Tj T* ( kw='keyword arguments'\)) Tj T* (def main\(opt, *args, **kw\):) Tj T* ( if opt:) Tj T* ( yield 'opt=%s' % opt) Tj T* ( if args:) Tj T* ( yield 'args=%s' % str\(args\)) Tj T* ( if kw:) Tj T* ( yield 'kw=%s' % kw) Tj T* T* (if __name__ == '__main__':) Tj T* ( for output in plac.call\(main\):) Tj T* ( print\(output\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 196.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here is the generated usage message:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 91.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 96 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 77.71 Tm /F3 10 Tf 12 TL (usage: example12.py [-h] [-opt OPT] [args [args ...]] [kw [kw ...]]) Tj T* T* (positional arguments:) Tj T* ( args default arguments) Tj T* ( kw keyword arguments) Tj T* T* (optional 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 235.3849 0 Td (13) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R350': class PDFStream +350 0 obj +% page stream +<< /Length 4297 >> +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 ( -h, --help show this help message and exit) Tj T* ( -opt OPT some option) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 707.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here is how you call the script:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 638.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 60 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 41.71 Tm /F3 10 Tf 12 TL ($ python example12.py -o X a1 a2 name=value) Tj T* (opt=X) Tj T* (args=\('a1', 'a2'\)) Tj T* (kw={'name': 'value'}) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 606.6236 cm +q +BT 1 0 0 1 0 16.82 Tm 2.133735 Tw 12 TL /F1 10 Tf 0 0 0 rg (When using keyword arguments, one must be careful to use names which are not alreay taken; for) Tj T* 0 Tw (instance in this examples the name ) Tj /F3 10 Tf (opt ) Tj /F1 10 Tf (is taken:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 549.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 48 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 29.71 Tm /F3 10 Tf 12 TL ($ python example12.py 1 2 kw1=1 kw2=2 opt=0) Tj T* (usage: example12.py [-h] [-o OPT] [args [args ...]] [kw [kw ...]]) Tj T* (example12.py: error: colliding keyword arguments: opt) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 505.4236 cm +q +0 0 0 rg +BT 1 0 0 1 0 28.82 Tm /F1 10 Tf 12 TL 1.024104 Tw (The names taken are the names of the flags, of the options, and of the positional arguments, excepted) Tj T* 0 Tw .60561 Tw (varargs and keywords. This limitation is a consequence of the way the argument names are managed in) Tj T* 0 Tw (function calls by the Python language.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 475.4236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Final example: a shelve interface) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 433.4236 cm +q +BT 1 0 0 1 0 28.82 Tm .603516 Tw 12 TL /F1 10 Tf 0 0 0 rg (Here is a less trivial example for the keyword arguments feature. The use case is the following: suppose) Tj T* 0 Tw .82881 Tw (we have stored the configuration parameters of a given application into a Python shelve and we need a) Tj T* 0 Tw (command-line tool to edit the shelve. A possible implementation using ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (could be the following:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 115.5936 cm +q +q +.952737 0 0 .952737 0 0 cm +q +1 0 0 1 6.6 6.927412 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 492 324 re B* +Q +q +BT 1 0 0 1 0 305.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (# ishelve.py) Tj T* (import os, shelve, plac) Tj T* T* (DEFAULT_SHELVE = os.path.expanduser\('~/conf.shelve'\)) Tj T* T* (@plac.annotations\() Tj T* ( help=\('show help', 'flag'\),) Tj T* ( showall=\('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 main\(help, showall, clear, delete, filename=DEFAULT_SHELVE,) Tj T* ( *params, **setters\):) Tj T* ( "A simple interface to a shelve. Use .help to see the available commands.") Tj T* ( sh = shelve.open\(filename\)) Tj T* ( try:) Tj T* ( if not any\([help, showall, clear, delete, params, setters]\):) Tj T* ( yield 'no arguments passed, use .help to see the available commands') Tj T* ( elif help: # custom help) Tj T* ( yield 'Commands: .help, .showall, .clear, .delete') Tj T* ( yield ') Tj (<) Tj (param) Tj (>) Tj ( ...') Tj T* ( yield ') Tj (<) Tj (param=value) Tj (>) Tj ( ...') Tj T* ( elif showall:) Tj T* ( for param, name in sh.items\(\):) 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 235.3849 0 Td (14) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R351': class PDFStream +351 0 obj +% page stream +<< /Length 6595 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 415.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 348 re B* +Q +q +BT 1 0 0 1 0 329.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ( 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* ( 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* (main.add_help = False # there is a custom help, remove the default one) Tj T* (main.prefix_chars = '.' # use dot-prefixed commands) Tj T* T* (if __name__ == '__main__':) Tj T* ( for output in plac.call\(main\):) Tj T* ( print\(output\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 395.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (A few notes are in order:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 389.8236 cm +Q +q +1 0 0 1 62.69291 389.8236 cm +Q +q +1 0 0 1 62.69291 359.8236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 15 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (1.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 16.82 Tm 2.075318 Tw 12 TL /F1 10 Tf 0 0 0 rg (I have disabled the ordinary help provided by ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (and I have implemented a custom help) Tj T* 0 Tw (command.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 359.8236 cm +Q +q +1 0 0 1 62.69291 359.8236 cm +Q +q +1 0 0 1 62.69291 341.8236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (2.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (I have changed the prefix character used to recognize the options to a dot.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 341.8236 cm +Q +q +1 0 0 1 62.69291 341.8236 cm +Q +q +1 0 0 1 62.69291 311.8236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 15 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (3.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 16.82 Tm .864985 Tw 12 TL /F1 10 Tf 0 0 0 rg (Keyword arguments recognition \(in the ) Tj /F3 10 Tf (**setters) Tj /F1 10 Tf (\) is used to make it possible to store a value in) Tj T* 0 Tw (the shelve with the syntax ) Tj /F3 10 Tf (param_name=param_value) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 311.8236 cm +Q +q +1 0 0 1 62.69291 311.8236 cm +Q +q +1 0 0 1 62.69291 281.8236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 15 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (4.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 16.82 Tm .649318 Tw 12 TL /F3 10 Tf 0 0 0 rg (*params ) Tj /F1 10 Tf (are used to retrieve parameters from the shelve and some error checking is performed in) Tj T* 0 Tw (the case of missing parameters) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 281.8236 cm +Q +q +1 0 0 1 62.69291 281.8236 cm +Q +q +1 0 0 1 62.69291 263.8236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (5.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (A command to clear the shelve is implemented as a flag \() Tj /F3 10 Tf (.clear) Tj /F1 10 Tf (\).) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 263.8236 cm +Q +q +1 0 0 1 62.69291 263.8236 cm +Q +q +1 0 0 1 62.69291 245.8236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (6.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (A command to delete a given parameter is implemented as an option \() Tj /F3 10 Tf (.delete) Tj /F1 10 Tf (\).) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 245.8236 cm +Q +q +1 0 0 1 62.69291 245.8236 cm +Q +q +1 0 0 1 62.69291 227.8236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (7.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (There is an option with default \() Tj /F3 10 Tf (.filename=conf.shelve) Tj /F1 10 Tf (\) to store the filename of the shelve.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 227.8236 cm +Q +q +1 0 0 1 62.69291 227.8236 cm +Q +q +1 0 0 1 62.69291 185.8236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 27 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (8.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 28.82 Tm 1.001984 Tw 12 TL /F1 10 Tf 0 0 0 rg (All things considered, the code looks like a poor man object oriented interface implemented with a) Tj T* 0 Tw 1.345251 Tw (chain of elifs instead of methods. Of course, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (can do better than that, but let me start from a) Tj T* 0 Tw (low-level approach first.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 185.8236 cm +Q +q +1 0 0 1 62.69291 185.8236 cm +Q +q +1 0 0 1 62.69291 167.8236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (If you run ) Tj /F3 10 Tf (ishelve.py ) Tj /F1 10 Tf (without arguments you get the following message:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 122.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 36 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 17.71 Tm /F3 10 Tf 12 TL ($ python ishelve.py) Tj T* (no arguments passed, use .help to see the available commands) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 102.6236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (If you run ) Tj /F3 10 Tf (ishelve.py ) Tj /F1 10 Tf (with the option ) Tj /F3 10 Tf (.h ) Tj /F1 10 Tf (\(or any abbreviation of ) Tj /F3 10 Tf (.help) Tj /F1 10 Tf (\) you get:) 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 235.3849 0 Td (15) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R352': class PDFStream +352 0 obj +% page stream +<< /Length 6264 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 703.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 +BT 1 0 0 1 0 41.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ($ python ishelve.py .h) Tj T* (Commands: .help, .showall, .clear, .delete) Tj T* (<) Tj (param) Tj (>) Tj ( ...) Tj T* (<) Tj (param=value) Tj (>) Tj ( ...) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 683.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (You can check by hand that the tool work:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 434.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 240 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 221.71 Tm /F3 10 Tf 12 TL ($ python ishelve.py .clear # start from an empty shelve) Tj T* (cleared the shelve) Tj T* ($ python ishelve.py a=1 b=2) Tj T* (setting a=1) Tj T* (setting b=2) Tj T* ($ python ishelve.py .showall) Tj T* (b=2) Tj T* (a=1) Tj T* ($ python ishelve.py .del b # abbreviation for .delete) Tj T* (deleted b) Tj T* ($ python ishelve.py a) Tj T* (1) Tj T* ($ python ishelve.py b) Tj T* (b: not found) Tj T* ($ python ishelve.py .cler # mispelled command) Tj T* (usage: ishelve.py [.help] [.showall] [.clear] [.delete DELETE]) Tj T* ( [.filename /home/micheles/conf.shelve]) Tj T* ( [params [params ...]] [setters [setters ...]]) Tj T* (ishelve.py: error: unrecognized arguments: .cler) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 404.6236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (plac vs argparse) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 362.6236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.065988 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is opinionated and by design it does not try to make available all of the features of ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (in an) Tj T* 0 Tw .177126 Tw (easy way. In particular you should be aware of the following limitations/differences \(the following assumes) Tj T* 0 Tw (knowledge of ) Tj 0 0 .501961 rg (argparse) Tj 0 0 0 rg (\):) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 356.6236 cm +Q +q +1 0 0 1 62.69291 356.6236 cm +Q +q +1 0 0 1 62.69291 278.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 63 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 64.82 Tm 2.69784 Tw 12 TL /F1 10 Tf 0 0 0 rg (plac does not support the destination concept: the destination coincides with the name of the) Tj T* 0 Tw .359983 Tw (argument, always. This restriction has some drawbacks. For instance, suppose you want to define a) Tj T* 0 Tw 2.758651 Tw (long option called ) Tj /F3 10 Tf (--yield) Tj /F1 10 Tf (. In this case the destination would be ) Tj /F3 10 Tf (yield) Tj /F1 10 Tf (, which is a Python) Tj T* 0 Tw 1.181235 Tw (keyword, and since you cannot introduce an argument with that name in a function definition, it is) Tj T* 0 Tw 2.12528 Tw (impossible to implement it. Your choices are to change the name of the long option, or to use) Tj T* 0 Tw 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (with a suitable destination.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 278.6236 cm +Q +q +1 0 0 1 62.69291 278.6236 cm +Q +q +1 0 0 1 62.69291 224.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 39 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 40.82 Tm 1.120751 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (does not support "required options". As the ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (documentation puts it: ) Tj /F4 10 Tf (Required options) Tj T* 0 Tw 1.075318 Tw (are generally considered bad form - normal users expect options to be optional. You should avoid) Tj T* 0 Tw .874269 Tw (the use of required options whenever possible. ) Tj /F1 10 Tf (Notice that since ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (supports them, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (can) Tj T* 0 Tw (manage them too, but not directly.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 224.6236 cm +Q +q +1 0 0 1 62.69291 224.6236 cm +Q +q +1 0 0 1 62.69291 158.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 51 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 52.82 Tm 1.539982 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (supports only regular boolean flags. ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (has the ability to define generalized two-value) Tj T* 0 Tw .361751 Tw (flags with values different from ) Tj /F3 10 Tf (True ) Tj /F1 10 Tf (and ) Tj /F3 10 Tf (False) Tj /F1 10 Tf (. An earlier version of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (had this feature too, but) Tj T* 0 Tw .814985 Tw (since you can use options with two choices instead, and in any case the conversion from ) Tj /F3 10 Tf ({True,) Tj T* 0 Tw .901984 Tw (False} ) Tj /F1 10 Tf (to any couple of values can be trivially implemented with a ternary operator \() Tj /F3 10 Tf (value1 if) Tj T* 0 Tw (flag else value2) Tj /F1 10 Tf (\), I have removed it \(KISS rules!\).) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 158.6236 cm +Q +q +1 0 0 1 62.69291 158.6236 cm +Q +q +1 0 0 1 62.69291 116.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 27 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 28.82 Tm 1.797126 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (does not support ) Tj /F3 10 Tf (nargs ) Tj /F1 10 Tf (options directly \(it uses them internally, though, to implement flag) Tj T* 0 Tw .90683 Tw (recognition\). The reason it that all the use cases of interest to me are covered by ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (and did not) Tj T* 0 Tw (feel the need to increase the learning curve by adding direct support for ) Tj /F3 10 Tf (nargs) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 116.6236 cm +Q +q +1 0 0 1 62.69291 116.6236 cm +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 235.3849 0 Td (16) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R353': class PDFStream +353 0 obj +% page stream +<< /Length 7562 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 735.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 15 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 16.82 Tm 2.111318 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (does support subparsers, but you must read the ) Tj 0 0 .501961 rg (advanced usage document ) Tj 0 0 0 rg (to see how it) Tj T* 0 Tw (works.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 735.0236 cm +Q +q +1 0 0 1 62.69291 735.0236 cm +Q +q +1 0 0 1 62.69291 693.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 27 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 28.82 Tm 1.111751 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (does not support actions directly. This also looks like a feature too advanced for the goals of) Tj T* 0 Tw .406651 Tw 0 0 .501961 rg (plac) Tj 0 0 0 rg (. Notice however that the ability to define your own annotation objects \(again, see the ) Tj 0 0 .501961 rg (advanced) Tj T* 0 Tw (usage document) Tj 0 0 0 rg (\) may mitigate the need for custom actions.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 693.0236 cm +Q +q +1 0 0 1 62.69291 693.0236 cm +Q +q +1 0 0 1 62.69291 675.0236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (can leverage directly on many ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (features.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 633.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 579.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 509.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 465.8236 cm +q +BT 1 0 0 1 0 28.82 Tm .239318 Tw 12 TL /F1 10 Tf 0 0 0 rg (disables the recognition of the help flag ) Tj /F3 10 Tf (-h, --help) Tj /F1 10 Tf (. This mechanism does not look particularly elegant,) Tj T* 0 Tw .566988 Tw (but it works well enough. I assume that the typical user of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (will be happy with the defaults and would) Tj T* 0 Tw (not want to change them; still it is possible if she wants to.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 435.8236 cm +q +BT 1 0 0 1 0 16.82 Tm 2.391235 Tw 12 TL /F1 10 Tf 0 0 0 rg (For instance, by setting the ) Tj /F3 10 Tf (description ) Tj /F1 10 Tf (attribute, it is possible to add a comment to the usage) Tj T* 0 Tw (message \(by default the docstring of the ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (function is used as description\).) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 405.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL .392619 Tw (It is also possible to change the option prefix; for instance if your script must run under Windows and you) Tj T* 0 Tw (want to use "/" as option prefix you can add the line:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 372.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 328.6236 cm +q +BT 1 0 0 1 0 28.82 Tm .924198 Tw 12 TL /F1 10 Tf 0 0 0 rg (The first prefix char \() Tj /F3 10 Tf (/) Tj /F1 10 Tf (\) is used as the default for the recognition of options and flags; the second prefix) Tj T* 0 Tw .26832 Tw (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: however you can disable it and reimplement it, if) Tj T* 0 Tw (you like, as seen in the ) Tj /F3 10 Tf (ishelve ) Tj /F1 10 Tf (example.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 298.6236 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 62.69291 205.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 84 re B* +Q +q +BT 1 0 0 1 0 65.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\)\) #doctest: +ELLIPSIS) Tj T* (ArgumentParser\(prog=...\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 161.4236 cm +q +BT 1 0 0 1 0 28.82 Tm 2.646905 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 to the main function as an) Tj T* 0 Tw .982126 Tw (attribute. When ) Tj /F3 10 Tf (plac.call\(func\) ) Tj /F1 10 Tf (is invoked multiple time, the parser is re-used and not rebuilt from) Tj T* 0 Tw (scratch again.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 131.4236 cm +q +BT 1 0 0 1 0 16.82 Tm .982765 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 not need to use it,) Tj T* 0 Tw (unless they want to access ) Tj /F4 10 Tf (all ) Tj /F1 10 Tf (of the features of ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (directly without calling the main function.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 89.42362 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 1.257045 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 except the) Tj T* 0 Tw 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 235.3849 0 Td (17) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R354': class PDFStream +354 0 obj +% page stream +<< /Length 8202 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 753.0236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (addition of the ) Tj /F3 10 Tf (.p ) Tj /F1 10 Tf (attribute.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 723.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (plac vs the rest of the world) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 681.0236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.866905 Tw 12 TL /F1 10 Tf 0 0 0 rg (Originally ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (boasted about being "the easiest command-line arguments parser in the world". Since) Tj T* 0 Tw .306457 Tw (then, people started pointing out to me various projects which are based on the same idea \(extracting the) Tj T* 0 Tw (parser from the main function signature\) and are arguably even easier than ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 675.0236 cm +Q +q +1 0 0 1 62.69291 675.0236 cm +Q +q +1 0 0 1 62.69291 657.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (opterator ) Tj 0 0 0 rg (by Dusty Phillips) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 657.0236 cm +Q +q +1 0 0 1 62.69291 657.0236 cm +Q +q +1 0 0 1 62.69291 639.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 .501961 rg (CLIArgs ) Tj 0 0 0 rg (by Pavel Panchekha) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 639.0236 cm +Q +q +1 0 0 1 62.69291 639.0236 cm +Q +q +1 0 0 1 62.69291 609.0236 cm +q +BT 1 0 0 1 0 16.82 Tm 2.136457 Tw 12 TL /F1 10 Tf 0 0 0 rg (Luckily for me none of such projects had the idea of using function annotations and ) Tj 0 0 .501961 rg (argparse) Tj 0 0 0 rg (; as a) Tj T* 0 Tw (consequence, they are no match for the capabilities of ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 567.0236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.551163 Tw 12 TL /F1 10 Tf 0 0 0 rg (Of course, there are tons of other libraries to parse the command line. For instance ) Tj 0 0 .501961 rg (Clap ) Tj 0 0 0 rg (by Matthew) Tj T* 0 Tw 1.211567 Tw (Frazier which appeared on PyPI just the day before ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (; ) Tj 0 0 .501961 rg (Clap ) Tj 0 0 0 rg (is fine but it is certainly not easier than) Tj T* 0 Tw 0 0 .501961 rg (plac) Tj 0 0 0 rg (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 525.0236 cm +q +BT 1 0 0 1 0 28.82 Tm .622488 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (can also be used as a replacement of the ) Tj 0 0 .501961 rg (cmd ) Tj 0 0 0 rg (module in the standard library and as such it shares) Tj T* 0 Tw .377126 Tw (many features with the module ) Tj 0 0 .501961 rg (cmd2 ) Tj 0 0 0 rg (by Catherine Devlin. However, this is completely coincidental, since) Tj T* 0 Tw (I became aware of the ) Tj 0 0 .501961 rg (cmd2 ) Tj 0 0 0 rg (module only after writing ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 495.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (The future) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 429.0236 cm +q +BT 1 0 0 1 0 52.82 Tm .135542 Tw 12 TL /F1 10 Tf 0 0 0 rg (Currently the core of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is around 200 lines of code, not counting blanks, comments and docstrings. I do) Tj T* 0 Tw .968626 Tw (not plan to extend the core much in the future. The idea is to keep the module short: it is and it should) Tj T* 0 Tw .11811 Tw (remain a little wrapper over ) Tj 0 0 .501961 rg (argparse) Tj 0 0 0 rg (. Actually I have thought about contributing the core back to ) Tj 0 0 .501961 rg (argparse) Tj T* 0 Tw 2.307485 Tw 0 0 0 rg (if ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (becomes successfull and gains a reasonable number of users. For the moment it should be) Tj T* 0 Tw (considered in alpha status.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 387.0236 cm +q +BT 1 0 0 1 0 28.82 Tm .927488 Tw 12 TL /F1 10 Tf 0 0 0 rg (Notice that even if ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (has been designed to be simple to use for simple stuff, its power should not be) Tj T* 0 Tw 1.02186 Tw (underestimated; it is actually a quite advanced tool with a domain of applicability which far exceeds the) Tj T* 0 Tw (realm of command-line arguments parsers.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 321.0236 cm +q +BT 1 0 0 1 0 52.82 Tm .285988 Tw 12 TL /F1 10 Tf 0 0 0 rg (Version 0.5 of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (doubled the code base and the documentation: it is based on the idea of using ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (to) Tj T* 0 Tw .408555 Tw (implement command-line interpreters, i.e. something akin to the ) Tj /F3 10 Tf (cmd ) Tj /F1 10 Tf (module in the standard library, only) Tj T* 0 Tw .49936 Tw (better. The new features of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (are described in the ) Tj 0 0 .501961 rg (advanced usage document ) Tj 0 0 0 rg (. They are implemented) Tj T* 0 Tw .313828 Tw (in a separated module \() Tj /F3 10 Tf (plac_ext.py) Tj /F1 10 Tf (\), since they require Python 2.5 to work, whereas ) Tj /F3 10 Tf (plac_core.py) Tj T* 0 Tw /F1 10 Tf (only requires Python 2.3.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 291.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Trivia: the story behind the name) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 225.0236 cm +q +BT 1 0 0 1 0 52.82 Tm .979984 Tw 12 TL /F1 10 Tf 0 0 0 rg (The ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (project started very humbly: I just wanted to make easy_installable my old ) Tj 0 0 .501961 rg (optionparse ) Tj 0 0 0 rg (recipe,) Tj T* 0 Tw .565988 Tw (and to publish it on PyPI. The original name of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (was optionparser and the idea behind it was to build) Tj T* 0 Tw .603735 Tw (an ) Tj 0 0 .501961 rg (OptionParser ) Tj 0 0 0 rg (object from the docstring of the module. However, before doing that, I decided to check) Tj T* 0 Tw .244198 Tw (out the ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (module, since I knew it was going into Python 2.7 and Python 2.7 was coming out. Soon) Tj T* 0 Tw (enough I realized two things:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 219.0236 cm +Q +q +1 0 0 1 62.69291 219.0236 cm +Q +q +1 0 0 1 62.69291 189.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 15 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (1.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 16.82 Tm .103735 Tw 12 TL /F1 10 Tf 0 0 0 rg (the single greatest idea of ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (was unifying the positional arguments and the options in a single) Tj T* 0 Tw (namespace object;) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 189.0236 cm +Q +q +1 0 0 1 62.69291 189.0236 cm +Q +q +1 0 0 1 62.69291 159.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 15 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (2.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL 1.66748 Tw (parsing the docstring was so old-fashioned, considering the existence of functions annotations in) Tj T* 0 Tw (Python 3.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 159.0236 cm +Q +q +1 0 0 1 62.69291 159.0236 cm +Q +q +1 0 0 1 62.69291 105.0236 cm +q +BT 1 0 0 1 0 40.82 Tm .600574 Tw 12 TL /F1 10 Tf 0 0 0 rg (Putting together these two observations with the original idea of inferring the parser I decided to build an) Tj T* 0 Tw .516905 Tw 0 0 .501961 rg (ArgumentParser ) Tj 0 0 0 rg (object from function annotations. The ) Tj /F3 10 Tf (optionparser ) Tj /F1 10 Tf (name was ruled out, since I was) Tj T* 0 Tw 2.085984 Tw (now using ) Tj 0 0 .501961 rg (argparse) Tj 0 0 0 rg (; a name like ) Tj /F3 10 Tf (argparse_plus ) Tj /F1 10 Tf (was also ruled out, since the typical usage was) Tj T* 0 Tw (completely different from the ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (usage.) 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 235.3849 0 Td (18) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R355': class PDFStream +355 0 obj +% page stream +<< /Length 5857 >> +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 16.82 Tm 1.093876 Tw 12 TL /F1 10 Tf 0 0 0 rg (I made a research on PyPI and the name ) Tj /F4 10 Tf (clap ) Tj /F1 10 Tf (\(Command Line Arguments Parser\) was not taken, so I) Tj T* 0 Tw (renamed everything to clap. After two days a ) Tj 0 0 .501961 rg (Clap ) Tj 0 0 0 rg (module appeared on PyPI <) Tj (expletives deleted) Tj (>) Tj (!) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 711.0236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL .877209 Tw (Having little imagination, I decided to rename everything again to plac, an anagram of clap: since it is a) Tj T* 0 Tw (non-existing English name, I hope nobody will steal it from me!) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 693.0236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (That's all, I hope you will enjoy working with ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (!) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 660.0236 cm +q +BT 1 0 0 1 0 8.435 Tm 21 TL /F2 17.5 Tf 0 0 0 rg (Advanced usages of plac) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 630.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Introduction) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 588.0236 cm +q +BT 1 0 0 1 0 28.82 Tm .539036 Tw 12 TL /F1 10 Tf 0 0 0 rg (One of the design goals of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is to make it dead easy to write a scriptable and testable interface for an) Tj T* 0 Tw .813876 Tw (application. You can use ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (whenever you have an API with strings in input and strings in output, and) Tj T* 0 Tw (that includes a ) Tj /F4 10 Tf (huge ) Tj /F1 10 Tf (domain of applications.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 546.0236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.756651 Tw 12 TL /F1 10 Tf 0 0 0 rg (A string-oriented interface is a scriptable interface by construction. That means that you can define a) Tj T* 0 Tw .918735 Tw (command language for your application and that it is possible to write scripts which are interpretable by) Tj T* 0 Tw 0 0 .501961 rg (plac ) Tj 0 0 0 rg (and can be run as batch scripts.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 504.0236 cm +q +BT 1 0 0 1 0 28.82 Tm .444987 Tw 12 TL /F1 10 Tf 0 0 0 rg (Actually, at the most general level, you can see ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (as a generic tool to write domain specific languages) Tj T* 0 Tw .107209 Tw (\(DSL\). With ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (you can test your application interactively as well as with batch scripts, and even with the) Tj T* 0 Tw (analogous of Python doctests for your defined language.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 438.0236 cm +q +BT 1 0 0 1 0 52.82 Tm .694104 Tw 12 TL /F1 10 Tf 0 0 0 rg (You can easily replace the ) Tj /F3 10 Tf (cmd ) Tj /F1 10 Tf (module of the standard library and you could easily write an application) Tj T* 0 Tw 2.271751 Tw (like ) Tj 0 0 .501961 rg (twill ) Tj 0 0 0 rg (with ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (. Or you could use it to script your building procedure. ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (also supports parallel) Tj T* 0 Tw .907765 Tw (execution of multiple commands and can be used as task manager and monitor. It is also quite easy to) Tj T* 0 Tw 1.483488 Tw (build a GUI or a Web application on top of ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (. When speaking of things you can do with ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (, your) Tj T* 0 Tw (imagination is the only limit!) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 408.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (From scripts to interactive applications) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 354.0236 cm +q +BT 1 0 0 1 0 40.82 Tm 1.300751 Tw 12 TL /F1 10 Tf 0 0 0 rg (Command-line scripts have many advantages, but they are no substitute for interactive applications. In) Tj T* 0 Tw .088171 Tw (particular, if you have a script with a large startup time which must be run multiple times, it is best to turn it) Tj T* 0 Tw 4.582126 Tw (into an interactive application, so that the startup is performed only once. ) Tj /F3 10 Tf (plac ) Tj /F1 10 Tf (provides an) Tj T* 0 Tw /F3 10 Tf (Interpreter ) Tj /F1 10 Tf (class just for this purpose.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 324.0236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.293984 Tw 12 TL /F1 10 Tf 0 0 0 rg (The ) Tj /F3 10 Tf (Interpreter ) Tj /F1 10 Tf (class wraps the main function of a script and provides an ) Tj /F3 10 Tf (.interact ) Tj /F1 10 Tf (method to) Tj T* 0 Tw (start an interactive interpreter reading commands from the console.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 294.0236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.49436 Tw 12 TL /F1 10 Tf 0 0 0 rg (For instance, you can define an interactive interpreter on top of the ) Tj /F3 10 Tf (ishelve ) Tj /F1 10 Tf (script introduced in the) Tj T* 0 Tw 0 0 .501961 rg (basic documentation ) Tj 0 0 0 rg (as follows:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 92.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 192 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 173.71 Tm /F3 10 Tf 12 TL (# shelve_interpreter.py) Tj T* (import plac, ishelve) Tj T* T* (@plac.annotations\() Tj T* ( interactive=\('start interactive interface', 'flag'\),) Tj T* ( subcommands='the commands of the underlying ishelve interpreter'\)) Tj T* (def main\(interactive, *subcommands\):) Tj T* ( """) Tj T* ( This script works both interactively and non-interactively.) Tj T* ( Use .help to see the internal commands.) Tj T* ( """) Tj T* ( if interactive:) Tj T* ( plac.Interpreter\(ishelve.main\).interact\(\)) Tj T* ( else:) Tj T* ( for out in plac.call\(ishelve.main, subcommands\):) 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 235.3849 0 Td (19) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R356': class PDFStream +356 0 obj +% page stream +<< /Length 3560 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 703.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 ( print\(out\)) 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 28.82 Tm 2.200651 Tw 12 TL /F1 10 Tf 0 0 0 rg (A trick has been used here: the ishelve command-line interface has been hidden inside an external) Tj T* 0 Tw .917674 Tw (interface. They are distinct: for instance the external interface recognizes the ) Tj /F3 10 Tf (-h/--help ) Tj /F1 10 Tf (flag whereas) Tj T* 0 Tw (the internal interface only recognizes the ) Tj /F3 10 Tf (.help ) Tj /F1 10 Tf (command:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 626.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 ($ python shelve_interpreter.py -h) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 449.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 (usage: shelve_interpreter.py [-h] [-interactive]) Tj T* ( [subcommands [subcommands ...]]) Tj T* T* ( This script works both interactively and non-interactively.) Tj T* ( Use .help to see the internal commands.) Tj T* ( ) Tj T* T* (positional arguments:) Tj T* ( subcommands the commands of the underlying ishelve interpreter) Tj T* T* (optional arguments:) Tj T* ( -h, --help show this help message and exit) Tj T* ( -interactive start interactive interface) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 429.4236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Thanks to this ingenuous trick, the script can be run both interactively and non-interactively:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 384.2236 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 ($ python shelve_interpreter.py .clear # non-interactive use) Tj T* (cleared the shelve) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 364.2236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here is an usage session:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 91.02362 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 264 re B* +Q +q +BT 1 0 0 1 0 245.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ($ python shelve_interpreter.py -i # interactive use) Tj T* (A simple interface to a shelve. Use .help to see the available commands.) Tj T* (i) Tj (>) Tj ( .help) Tj T* (Commands: .help, .showall, .clear, .delete) Tj T* (<) Tj (param) Tj (>) Tj ( ...) Tj T* (<) Tj (param=value) Tj (>) Tj ( ...) 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 ( .del a) Tj T* (deleted a) Tj T* (i) Tj (>) Tj ( a) Tj T* (a: not found) Tj T* (i) Tj (>) Tj ( .show) Tj T* (b=2) 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 235.3849 0 Td (20) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R357': class PDFStream +357 0 obj +% page stream +<< /Length 5568 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 739.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 24 re B* +Q +q +BT 1 0 0 1 0 5.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (i) Tj (>) Tj ( [CTRL-D]) 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 52.82 Tm .256412 Tw 12 TL /F1 10 Tf 0 0 0 rg (The ) Tj /F3 10 Tf (.interact ) Tj /F1 10 Tf (method reads commands from the console and send them to the underlying interpreter,) Tj T* 0 Tw .065984 Tw (until the user send a CTRL-D command \(CTRL-Z in Windows\). There is a default argument ) Tj /F3 10 Tf (prompt='i) Tj (>) Tj T* 0 Tw .41832 Tw (' ) Tj /F1 10 Tf (which can be used to change the prompt. The text displayed at the beginning of the interactive session) Tj T* 0 Tw 1.407126 Tw (is the docstring of the main function. ) Tj /F3 10 Tf (plac ) Tj /F1 10 Tf (also understands command abbreviations: in this example) Tj T* 0 Tw /F3 10 Tf (del ) Tj /F1 10 Tf (is an abbreviation for ) Tj /F3 10 Tf (delete) Tj /F1 10 Tf (. In case of ambiguous abbreviations ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (raises a ) Tj /F3 10 Tf (NameError) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 641.8236 cm +q +BT 1 0 0 1 0 16.82 Tm .847045 Tw 12 TL /F1 10 Tf 0 0 0 rg (Finally I must notice that the ) Tj /F3 10 Tf (plac.Interpreter ) Tj /F1 10 Tf (is available only if you are using a recent version of) Tj T* 0 Tw (Python \() Tj (>) Tj (= 2.5\), because it is a context manager object which uses extended generators internally.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 611.8236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Testing a plac application) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 581.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL 3.034269 Tw (You can conveniently test your application in interactive mode. However manual testing is a poor) Tj T* 0 Tw (substitute for automatic testing.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 563.8236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (In principle, one could write automatic tests for the ) Tj /F3 10 Tf (ishelve ) Tj /F1 10 Tf (application by using ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (directly:) 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 156 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 137.71 Tm /F3 10 Tf 12 TL (# test_ishelve.py) Tj T* (import plac, ishelve) Tj T* T* (def test\(\):) Tj T* ( assert plac.call\(ishelve.main, ['.clear']\) == ['cleared the shelve']) Tj T* ( assert plac.call\(ishelve.main, ['a=1']\) == ['setting a=1']) Tj T* ( assert plac.call\(ishelve.main, ['a']\) == ['1']) Tj T* ( assert plac.call\(ishelve.main, ['.delete=a']\) == ['deleted a']) Tj T* ( assert plac.call\(ishelve.main, ['a']\) == ['a: not found']) Tj T* T* (if __name__ == '__main__':) Tj T* ( test\(\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 354.6236 cm +q +BT 1 0 0 1 0 28.82 Tm .390651 Tw 12 TL /F1 10 Tf 0 0 0 rg (However, using ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (is not especially nice. The big issue is that ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (responds to invalid) Tj T* 0 Tw 1.249987 Tw (input by printing an error message on stderr and by raising a ) Tj /F3 10 Tf (SystemExit) Tj /F1 10 Tf (: this is certainly not a nice) Tj T* 0 Tw (thing to do in a test.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 312.6236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.616457 Tw 12 TL /F1 10 Tf 0 0 0 rg (As a consequence of this behavior it is impossible to test for invalid commands, unless you wrap the) Tj T* 0 Tw .259985 Tw /F3 10 Tf (SystemExit ) Tj /F1 10 Tf (exception by hand each time \(a possibly you do something with the error message in stderr) Tj T* 0 Tw (too\). Luckily, ) Tj /F3 10 Tf (plac ) Tj /F1 10 Tf (offers a better testing support through the ) Tj /F3 10 Tf (check ) Tj /F1 10 Tf (method of ) Tj /F3 10 Tf (Interpreter ) Tj /F1 10 Tf (objects:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 159.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 144 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 125.71 Tm /F3 10 Tf 12 TL (# test_ishelve_more.py) Tj T* (from __future__ import with_statement) Tj T* (import plac, ishelve) Tj T* T* (def test\(\):) Tj T* ( with plac.Interpreter\(ishelve.main\) as i:) Tj T* ( i.check\('.clear', 'cleared the shelve'\)) Tj T* ( i.check\('a=1', 'setting a=1'\)) Tj T* ( i.check\('a', '1'\)) Tj T* ( i.check\('.delete=a', 'deleted a'\)) Tj T* ( i.check\('a', 'a: not found'\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 103.4236 cm +q +BT 1 0 0 1 0 40.82 Tm 6.299974 Tw 12 TL /F1 10 Tf 0 0 0 rg (The method ) Tj /F3 10 Tf (.check\(given_input, expected_output\) ) Tj /F1 10 Tf (works on strings and raises an) Tj T* 0 Tw .971318 Tw /F3 10 Tf (AssertionError ) Tj /F1 10 Tf (if the output produced by the interpreter is different from the expected output for the) Tj T* 0 Tw 2.186905 Tw (given input. Notice that ) Tj /F3 10 Tf (AssertionError ) Tj /F1 10 Tf (is catched by tools like ) Tj /F3 10 Tf (py.test ) Tj /F1 10 Tf (and ) Tj /F3 10 Tf (nosetests ) Tj /F1 10 Tf (and) Tj T* 0 Tw (actually ) Tj /F3 10 Tf (plac ) Tj /F1 10 Tf (tests are intended to be run with such tools.) 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 235.3849 0 Td (21) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R358': class PDFStream +358 0 obj +% page stream +<< /Length 6085 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 717.0236 cm +q +BT 1 0 0 1 0 40.82 Tm .239984 Tw 12 TL /F1 10 Tf 0 0 0 rg (Interpreters offer a minor syntactic advantage with respect to calling ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (directly, but they offer a) Tj T* 0 Tw .96748 Tw /F4 10 Tf (major ) Tj /F1 10 Tf (semantic advantage when things go wrong \(read exceptions\): an ) Tj /F3 10 Tf (Interpreter ) Tj /F1 10 Tf (object internally) Tj T* 0 Tw 1.181318 Tw (invokes something like ) Tj /F3 10 Tf (plac.call) Tj /F1 10 Tf (, but it wraps all exceptions, so that ) Tj /F3 10 Tf (i.check ) Tj /F1 10 Tf (is guaranteed not to) Tj T* 0 Tw (raise any exception except ) Tj /F3 10 Tf (AssertionError) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 699.0236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (Even the ) Tj /F3 10 Tf (SystemExit ) Tj /F1 10 Tf (exception is captured and you can write your test as) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 693.0236 cm +Q +q +1 0 0 1 62.69291 681.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +BT 1 0 0 1 0 2 Tm T* ET +q +1 0 0 1 20 0 cm +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL (i.check\('-cler', 'SystemExit: unrecognized arguments: -cler'\)) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 681.0236 cm +Q +q +1 0 0 1 62.69291 663.0236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (without risk of exiting from the Python interpreter.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 621.0236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.422651 Tw 12 TL /F1 10 Tf 0 0 0 rg (There is a second advantage of interpreters: if the main function contains some initialization code and) Tj T* 0 Tw .454651 Tw (finalization code \() Tj /F3 10 Tf (__enter__ ) Tj /F1 10 Tf (and ) Tj /F3 10 Tf (__exit__ ) Tj /F1 10 Tf (functions\) they will be run only once at the beginning and) Tj T* 0 Tw (at the end of the interpreter loop. ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (instead ignores the initialization/finalization code.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 591.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Plac easy tests) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 549.0236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.517126 Tw 12 TL /F1 10 Tf 0 0 0 rg (Writing your tests in terms of ) Tj /F3 10 Tf (Interpreter.check ) Tj /F1 10 Tf (is certainly an improvement over writing them in) Tj T* 0 Tw 1.807318 Tw (terms of ) Tj /F3 10 Tf (plac.call) Tj /F1 10 Tf (, but they are still too low-level for my taste. The ) Tj /F3 10 Tf (Interpreter ) Tj /F1 10 Tf (class provides) Tj T* 0 Tw (support for doctest-style tests, a.k.a. ) Tj /F4 10 Tf (plac easy tests) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 507.0236 cm +q +BT 1 0 0 1 0 28.82 Tm 2.142209 Tw 12 TL /F1 10 Tf 0 0 0 rg (By using plac easy tests you can cut and paste your interactive session and turn it into a runnable) Tj T* 0 Tw .519213 Tw (automatics test. Consider for instance the following file ) Tj /F3 10 Tf (ishelve.placet ) Tj /F1 10 Tf (\(the ) Tj /F3 10 Tf (.placet ) Tj /F1 10 Tf (extension is a) Tj T* 0 Tw (mnemonic for plac easy tests\):) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 329.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 168 re B* +Q +q +BT 1 0 0 1 0 149.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (#!ishelve.py) Tj T* (i) Tj (>) Tj ( .clear # start from a clean state) 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 ( .del a) Tj T* (deleted a) Tj T* (i) Tj (>) Tj ( a) Tj T* (a: not found) Tj T* (i) Tj (>) Tj ( .cler # spelling error) Tj T* (.cler: not found) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 273.8236 cm +q +BT 1 0 0 1 0 40.82 Tm .697132 Tw 12 TL /F1 10 Tf 0 0 0 rg (Notice the precence of the shebang line containing the name of the ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (tool to test \(a ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (tool is just a) Tj T* 0 Tw 1.511751 Tw (Python module with a function called ) Tj /F3 10 Tf (main) Tj /F1 10 Tf (\). The shebang is ignored by the interpreter \(it looks like a) Tj T* 0 Tw .487608 Tw (comment to it\) but it is there so that external tools \(say a test runner\) can infer the plac interpreter to use) Tj T* 0 Tw (to test the file.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 243.8236 cm +q +BT 1 0 0 1 0 16.82 Tm 2.419984 Tw 12 TL /F1 10 Tf 0 0 0 rg (You can test ) Tj /F3 10 Tf (ishelve.placet ) Tj /F1 10 Tf (file by calling the ) Tj /F3 10 Tf (.doctest ) Tj /F1 10 Tf (method of the interpreter, as in this) Tj T* 0 Tw (example:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 199.0393 cm +q +q +.988825 0 0 .988825 0 0 cm +q +1 0 0 1 6.6 6.674587 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 474 36 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 17.71 Tm /F3 10 Tf 12 TL ($ python -c"import plac, ishelve) Tj T* (plac.Interpreter\(ishelve.main\).doctest\(open\('ishelve.placet'\), verbose=True\)") Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 155.0393 cm +q +BT 1 0 0 1 0 28.82 Tm 4.007109 Tw 12 TL /F1 10 Tf 0 0 0 rg (Internally ) Tj /F3 10 Tf (Interpreter.doctests ) Tj /F1 10 Tf (invokes something like ) Tj /F3 10 Tf (Interpreter.check ) Tj /F1 10 Tf (multiple times) Tj T* 0 Tw .226654 Tw (inside the same context and compare the output with the expected output: if even a check fails, the whole) Tj T* 0 Tw (test fail.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 113.0393 cm +q +BT 1 0 0 1 0 28.82 Tm .175868 Tw 12 TL /F1 10 Tf 0 0 0 rg (You should realize tha the easy tests supported by ) Tj /F3 10 Tf (plac ) Tj /F1 10 Tf (are ) Tj /F4 10 Tf (not ) Tj /F1 10 Tf (unittests: they are functional tests. They) Tj T* 0 Tw 1.22936 Tw (model then user interaction and the order of the operations generally matters. The single subtests in a) Tj T* 0 Tw /F3 10 Tf (.placet ) Tj /F1 10 Tf (file are not independent and it makes sense to exit immediately at the first failure.) 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 235.3849 0 Td (22) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R359': class PDFStream +359 0 obj +% page stream +<< /Length 6456 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 693.0236 cm +q +BT 1 0 0 1 0 64.82 Tm .414431 Tw 12 TL /F1 10 Tf 0 0 0 rg (The support for doctests in ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (comes nearly for free, thanks to the ) Tj 0 0 .501961 rg (shlex ) Tj 0 0 0 rg (module in the standard library,) Tj T* 0 Tw .352765 Tw (which is able to parse simple languages as the ones you can implement with ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (. In particular, thanks to) Tj T* 0 Tw .875984 Tw 0 0 .501961 rg (shlex) Tj 0 0 0 rg (, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is able to recognize comments \(the default comment character is ) Tj /F3 10 Tf (#) Tj /F1 10 Tf (\), escape sequences and) Tj T* 0 Tw 1.50686 Tw (more. Look at the ) Tj 0 0 .501961 rg (shlex ) Tj 0 0 0 rg (documentation if you need to customize how the language is interpreted. For) Tj T* 0 Tw 2.794985 Tw (more flexibility, it is even possible to pass to the interpreter a custom split function with signature) Tj T* 0 Tw /F3 10 Tf (split\(line, commentchar\)) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 639.0236 cm +q +BT 1 0 0 1 0 40.82 Tm .136654 Tw 12 TL /F1 10 Tf 0 0 0 rg (In addition, I have implemented from scratch some support for line number recognition, so that if a test fail) Tj T* 0 Tw .042093 Tw (you get the line number of the failing command. This is especially useful if your tests are stored in external) Tj T* 0 Tw .610898 Tw (files, even if plac easy tests does not need to be in a file: you can just pass to the ) Tj /F3 10 Tf (.doctest ) Tj /F1 10 Tf (method a) Tj T* 0 Tw (list of strings corresponding to the lines of the file.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 609.0236 cm +q +BT 1 0 0 1 0 16.82 Tm .653145 Tw 12 TL /F1 10 Tf 0 0 0 rg (At the present ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (does not use any code from the doctest module, but the situation may change in the) Tj T* 0 Tw (future \(it would be nice if ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (could reuse doctests directives like ELLIPSIS\).) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 579.0236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.447318 Tw 12 TL /F1 10 Tf 0 0 0 rg (It is straighforward to integrate your ) Tj /F3 10 Tf (.placet ) Tj /F1 10 Tf (tests with standard testing tools. For instance, you can) Tj T* 0 Tw (integrate your doctests with ) Tj /F3 10 Tf (nose ) Tj /F1 10 Tf (or ) Tj /F3 10 Tf (py.test ) Tj /F1 10 Tf (as follow:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 391.8485 cm +q +q +.988825 0 0 .988825 0 0 cm +q +1 0 0 1 6.6 6.674587 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 474 180 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 161.71 Tm /F3 10 Tf 12 TL (import os, shlex, plac) Tj T* T* (def test_doct\(\):) Tj T* ( """) Tj T* ( Find all the doctests in the current directory and run them with the) Tj T* ( corresponding plac interpreter \(the shebang rules!\)) Tj T* ( """) Tj T* ( placets = [f for f in os.listdir\('.'\) if f.endswith\('.placet'\)]) Tj T* ( for placet in placets:) Tj T* ( lines = list\(open\(placet\)\)) Tj T* ( assert lines[0].startswith\('#!'\), 'Missing or incorrect shebang line!') Tj T* ( firstline = lines[0][2:] # strip the shebang) Tj T* ( main = plac.import_main\(*shlex.split\(firstline\)\)) Tj T* ( yield plac.Interpreter\(main\).doctest, lines[1:]) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 311.8485 cm +q +BT 1 0 0 1 0 64.82 Tm 1.44811 Tw 12 TL /F1 10 Tf 0 0 0 rg (Here you should notice that usage of ) Tj /F3 10 Tf (plac.import_main) Tj /F1 10 Tf (, an utility which is able to import the main) Tj T* 0 Tw .775703 Tw (function of the script specified in the shebang line. You can use both the full path name of the tool, or a) Tj T* 0 Tw .87686 Tw (relative path name. In this case the runner look at the environment variable ) Tj /F3 10 Tf (PLACPATH ) Tj /F1 10 Tf (and it searches) Tj T* 0 Tw 1.900651 Tw (the plac tool in the directories specified there \() Tj /F3 10 Tf (PLACPATH ) Tj /F1 10 Tf (is just a string containing directory names) Tj T* 0 Tw .56332 Tw (separated by colons\). If the variable ) Tj /F3 10 Tf (PLACPATH ) Tj /F1 10 Tf (is not defined, it just looks in the current directory. If the) Tj T* 0 Tw (plac tool is not found, an ) Tj /F3 10 Tf (ImportError ) Tj /F1 10 Tf (is raised.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 281.8485 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Plac batch scripts) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 239.8485 cm +q +BT 1 0 0 1 0 28.82 Tm .772093 Tw 12 TL /F1 10 Tf 0 0 0 rg (It is pretty easy to realize that an interactive interpreter can also be used to run batch scripts: instead of) Tj T* 0 Tw .504692 Tw (reading the commands from the console, it is enough to read the commands from a file. ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (interpreters) Tj T* 0 Tw (provide an ) Tj /F3 10 Tf (.execute ) Tj /F1 10 Tf (method to perform just that.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 173.8485 cm +q +BT 1 0 0 1 0 52.82 Tm .098935 Tw 12 TL /F1 10 Tf 0 0 0 rg (There is just a subtle point to notice: whereas in an interactive loop one wants to manage all exceptions, a) Tj T* 0 Tw 3.866412 Tw (batch script should not in the background in case of unexpected errors. The implementation of) Tj T* 0 Tw .103059 Tw /F3 10 Tf (Interpreter.execute ) Tj /F1 10 Tf (makes sure that any error raised by ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (internally is re-raised. In other) Tj T* 0 Tw .407045 Tw (words, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (interpreters ) Tj /F4 10 Tf (wrap the errors, but does not eat them) Tj /F1 10 Tf (: the errors are always accessible and can) Tj T* 0 Tw (be re-raised on demand.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 143.8485 cm +q +BT 1 0 0 1 0 16.82 Tm 1.239318 Tw 12 TL /F1 10 Tf 0 0 0 rg (The exception is the case of invalid commands, which are skipped. Consider for instance the following) Tj T* 0 Tw (batch file, which contains a mispelled command \() Tj /F3 10 Tf (.dl ) Tj /F1 10 Tf (instead of ) Tj /F3 10 Tf (.del) Tj /F1 10 Tf (\):) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 98.64848 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 (#!ishelve.py) Tj T* (.clear ) 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 235.3849 0 Td (23) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R360': class PDFStream +360 0 obj +% page stream +<< /Length 4430 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 691.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 72 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 53.71 Tm /F3 10 Tf 12 TL (a=1 b=2) Tj T* (.show) Tj T* (.del a) Tj T* (.dl b) Tj T* (.show) 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 16.82 Tm 1.939461 Tw 12 TL /F1 10 Tf 0 0 0 rg (If you execute the batch file, the interpreter will print a ) Tj /F3 10 Tf (.dl: not found ) Tj /F1 10 Tf (at the ) Tj /F3 10 Tf (.dl ) Tj /F1 10 Tf (line and will) Tj T* 0 Tw (continue:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 434.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 216 re B* +Q +q +BT 1 0 0 1 0 197.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ($ python -c "import plac, ishelve) Tj T* (plac.Interpreter\(ishelve.main\).execute\(open\('ishelve.plac'\), verbose=True\)") Tj T* (i) Tj (>) Tj ( .clear) Tj T* (cleared the shelve) Tj T* (i) Tj (>) Tj ( a=1 b=2) Tj T* (setting a=1) Tj T* (setting b=2) Tj T* (i) Tj (>) Tj ( .show) Tj T* (b=2) Tj T* (a=1) Tj T* (i) Tj (>) Tj ( .del a) Tj T* (deleted a) Tj T* (i) Tj (>) Tj ( .dl b) Tj T* (2) Tj T* (.dl: not found) Tj T* (i) Tj (>) Tj ( .show) Tj T* (b=2) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 390.6236 cm +q +BT 1 0 0 1 0 28.82 Tm .159988 Tw 12 TL /F1 10 Tf 0 0 0 rg (The ) Tj /F3 10 Tf (verbose ) Tj /F1 10 Tf (flag is there to show the lines which are being interpreted \(prefixed by ) Tj /F3 10 Tf (i) Tj (>) Tj /F1 10 Tf (\). This is done on) Tj T* 0 Tw 1.359988 Tw (purpose, so that you can cut and paste the output of the batch script and turn it into a ) Tj /F3 10 Tf (.placet ) Tj /F1 10 Tf (test) Tj T* 0 Tw (\(cool, isn't it?\).) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 360.6236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Implementing subcommands) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 318.6236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.182485 Tw 12 TL /F1 10 Tf 0 0 0 rg (When I discussed the ) Tj /F3 10 Tf (ishelve ) Tj /F1 10 Tf (implementation in the ) Tj 0 0 .501961 rg (basic documentation) Tj 0 0 0 rg (, I said that it looked like a) Tj T* 0 Tw .116655 Tw (poor man implementation of an object system as a chain of elifs; I also said that ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (was able to do much) Tj T* 0 Tw (better than that. Here I will substantiate my claim.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 276.6236 cm +q +BT 1 0 0 1 0 28.82 Tm .89104 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is actually able to infer a set of subparsers from a generic container of commands. This is useful if) Tj T* 0 Tw 3.125814 Tw (you want to implement ) Tj /F4 10 Tf (subcommands ) Tj /F1 10 Tf (\(a familiar example of a command-line application featuring) Tj T* 0 Tw (subcommands is subversion\).) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 210.6236 cm +q +BT 1 0 0 1 0 52.82 Tm .015868 Tw 12 TL /F1 10 Tf 0 0 0 rg (Technically 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) Tj T* 0 Tw 2.550888 Tw (methods which are valid commands. A command container may have initialization/finalization hooks) Tj T* 0 Tw 2.55664 Tw (\() Tj /F3 10 Tf (__enter__/__exit__) Tj /F1 10 Tf (\) and dispatch hooks \() Tj /F3 10 Tf (__missing__) Tj /F1 10 Tf (, invoked for invalid command names\).) Tj T* 0 Tw 2.113828 Tw (Moreover, only when using command containers ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is able to provide automatic autocompletion of) Tj T* 0 Tw (commands.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 192.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (The shelve interface can be rewritten in an object-oriented way as follows:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 99.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 84 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 65.71 Tm /F3 10 Tf 12 TL (# ishelve2.py) Tj T* (import shelve, os, sys, plac) Tj T* T* (class ShelveInterface\(object\):) Tj T* ( "A minimal interface over a shelve object.") Tj T* ( commands = 'set', 'show', 'showall', 'delete') 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 235.3849 0 Td (24) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R361': class PDFStream +361 0 obj +% page stream +<< /Length 4341 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 307.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 456 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 437.71 Tm /F3 10 Tf 12 TL ( @plac.annotations\() Tj T* ( configfile=\('path name of the shelve', 'option'\)\)) Tj T* ( def __init__\(self, configfile\):) Tj T* ( self.configfile = configfile or '~/conf.shelve') Tj T* ( self.fname = os.path.expanduser\(self.configfile\)) Tj T* ( self.__doc__ += '\\nOperating on %s.\\n.help to see '\\) Tj T* ( 'the available commands.\\n' % self.fname) Tj T* ( def __enter__\(self\):) Tj T* ( self.sh = shelve.open\(self.fname\)) Tj T* ( return self) Tj T* ( def __exit__\(self, etype, exc, tb\):) Tj T* ( self.sh.close\(\)) 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' % \(name, self.sh[name]\) # no error checking) Tj T* ( def showall\(self\):) Tj T* ( "show all parameters") Tj T* ( for name in self.sh:) Tj T* ( yield '%s = %s' % \(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] # no error checking) Tj T* T* (main = ShelveInterface # useful for the tests) Tj T* T* (if __name__ == '__main__':) Tj T* ( plac.Interpreter.call\(ShelveInterface\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 215.8236 cm +q +BT 1 0 0 1 0 76.82 Tm .885366 Tw 12 TL /F3 10 Tf 0 0 0 rg (plac.Interpreter ) Tj /F1 10 Tf (objects wrap context manager objects consistently. In other words, if you wrap an) Tj T* 0 Tw 2.323828 Tw (object with ) Tj /F3 10 Tf (__enter__ ) Tj /F1 10 Tf (and ) Tj /F3 10 Tf (__exit__ ) Tj /F1 10 Tf (methods, they are invoked in the right order \() Tj /F3 10 Tf (__enter__) Tj T* 0 Tw .23528 Tw /F1 10 Tf (before the interpreter loop starts and ) Tj /F3 10 Tf (__exit__ ) Tj /F1 10 Tf (after the interpreter loop ends, both in the regular and in) Tj T* 0 Tw 1.916412 Tw (the exceptional case\). In our example, the methods ) Tj /F3 10 Tf (__enter__ ) Tj /F1 10 Tf (and ) Tj /F3 10 Tf (__exit__ ) Tj /F1 10 Tf (make sure the the) Tj T* 0 Tw .339398 Tw (shelve is opened and closed correctly even in the case of exceptions. Notice that I have not implemented) Tj T* 0 Tw .814104 Tw (any error checking in the ) Tj /F3 10 Tf (show ) Tj /F1 10 Tf (and ) Tj /F3 10 Tf (delete ) Tj /F1 10 Tf (methods on purpose, to verify that ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (works correctly in) Tj T* 0 Tw (the presence of exceptions.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 137.8236 cm +q +BT 1 0 0 1 0 64.82 Tm 1.567126 Tw 12 TL /F1 10 Tf 0 0 0 rg (When working with command containers, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (automatically adds two special commands to the set of) Tj T* 0 Tw 1.176136 Tw (provided commands: ) Tj /F3 10 Tf (.help ) Tj /F1 10 Tf (and ) Tj /F3 10 Tf (.last_tb) Tj /F1 10 Tf (. The ) Tj /F3 10 Tf (.help ) Tj /F1 10 Tf (command is the easier to understand: when) Tj T* 0 Tw .39811 Tw (invoked without arguments it displays the list of available commands with the same formatting of the ) Tj 0 0 .501961 rg (cmd) Tj T* 0 Tw 1.19561 Tw 0 0 0 rg (module; when invoked with the name of a command it displays the usage message for that command.) Tj T* 0 Tw 2.33686 Tw (The ) Tj /F3 10 Tf (.last_tb ) Tj /F1 10 Tf (command is useful when debugging: in case of errors, it allows you to display the) Tj T* 0 Tw (traceback of the last executed command.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 119.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 56.69291 56.69291 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 235.3849 0 Td (25) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R362': class PDFStream +362 0 obj +% page stream +<< /Length 4233 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 374.0679 cm +q +q +.773863 0 0 .773863 0 0 cm +q +1 0 0 1 6.6 8.528639 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 606 504 re B* +Q +q +BT 1 0 0 1 0 485.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ($ python ishelve2.py) Tj T* (A minimal interface over a shelve object.) Tj T* (Operating on /home/micheles/conf.shelve.) Tj T* (.help to see the available commands.) Tj T* (i) Tj (>) Tj ( .help) Tj T* T* (special commands) Tj T* (================) Tj T* (.help .last_tb) Tj T* T* (custom commands) Tj T* (===============) Tj T* (delete set show showall) Tj T* T* (i) Tj (>) Tj ( delete) Tj T* (deleting everything) Tj T* (i) Tj (>) Tj ( set a pippo) Tj T* (setting a=pippo) Tj T* (i) Tj (>) Tj ( set b lippo) Tj T* (setting b=lippo) Tj T* (i) Tj (>) Tj ( showall) Tj T* (b = lippo) Tj T* (a = pippo) Tj T* (i) Tj (>) Tj ( show a b) Tj T* (a = pippo) Tj T* (b = lippo) Tj T* (i) Tj (>) Tj ( del a) Tj T* (deleting a) Tj T* (i) Tj (>) Tj ( showall) Tj T* (b = lippo) Tj T* (i) Tj (>) Tj ( delete a) Tj T* (deleting a) Tj T* (KeyError: 'a') Tj T* (i) Tj (>) Tj ( .last_tb) Tj T* ( File "/usr/local/lib/python2.6/dist-packages/plac-0.6.0-py2.6.egg/plac_ext.py", line 190, in _wrap) Tj T* ( for value in genobj:) Tj T* ( File "./ishelve2.py", line 37, in delete) Tj T* ( del self.sh[name] # no error checking) Tj T* ( File "/usr/lib/python2.6/shelve.py", line 136, in __delitem__) Tj T* ( del self.dict[key]) Tj T* (i) Tj (>) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 342.0679 cm +q +BT 1 0 0 1 0 16.82 Tm 2.571235 Tw 12 TL /F1 10 Tf 0 0 0 rg (Notice that in interactive mode the traceback is hidden, unless you pass the ) Tj /F3 10 Tf (verbose ) Tj /F1 10 Tf (flag to the) Tj T* 0 Tw /F3 10 Tf (Interpreter.interact ) Tj /F1 10 Tf (method.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 312.0679 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (plac.Interpreter.call) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 270.0679 cm +q +BT 1 0 0 1 0 28.82 Tm 1.066303 Tw 12 TL /F1 10 Tf 0 0 0 rg (At the core of ) Tj /F3 10 Tf (plac ) Tj /F1 10 Tf (there is the ) Tj /F3 10 Tf (call ) Tj /F1 10 Tf (function which invokes a callable with the list of the arguments) Tj T* 0 Tw .937674 Tw (passed at the command-line \() Tj /F3 10 Tf (sys.argv[1:]) Tj /F1 10 Tf (\). Thanks to ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (you can launch your module by) Tj T* 0 Tw (simply adding the lines:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 224.8679 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 (if __name__ == '__main__':) Tj T* ( plac.call\(main\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 168.8679 cm +q +BT 1 0 0 1 0 40.82 Tm .50436 Tw 12 TL /F1 10 Tf 0 0 0 rg (Everything works fine if ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (is a simple callable performing some action; however, in many cases, one) Tj T* 0 Tw .087633 Tw (has a ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf ("function" which is a actually a factory returning a command container object. For instance, in) Tj T* 0 Tw .573318 Tw (my second shelve example the main function is the class ) Tj /F3 10 Tf (ShelveInterface) Tj /F1 10 Tf (, and the two lines needed) Tj T* 0 Tw (to run the module are a bit ugly:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 123.6679 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 (if __name__ == '__main__':) Tj T* ( plac.Interpreter\(plac.call\(ShelveInterface\)\).interact\(\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 91.66791 cm +q +BT 1 0 0 1 0 16.82 Tm .435227 Tw 12 TL /F1 10 Tf 0 0 0 rg (Moreover, now the program runs, but only in interactive mode, i.e. it is not possible to run it as a script. It ) Tj T* 0 Tw .721098 Tw (would be nice instead to be able to specify the command to execute on the command-line and have the) Tj T* 0 Tw 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 235.3849 0 Td (26) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R363': class PDFStream +363 0 obj +% page stream +<< /Length 4718 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 693.0236 cm +q +BT 1 0 0 1 0 64.82 Tm 4.08229 Tw 12 TL /F1 10 Tf 0 0 0 rg (interpreter start, execute the command and finish properly \(I mean by calling ) Tj /F3 10 Tf (__enter__ ) Tj /F1 10 Tf (and) Tj T* 0 Tw .427633 Tw /F3 10 Tf (__exit__) Tj /F1 10 Tf (\) without needing user input. The the script could be called from a batch shell script working in) Tj T* 0 Tw 2.26816 Tw (the background. In order to provide such functionality ) Tj /F3 10 Tf (plac.Interpreter ) Tj /F1 10 Tf (provides a classmethod) Tj T* 0 Tw 1.173318 Tw (named ) Tj /F3 10 Tf (.call ) Tj /F1 10 Tf (which takes the factory, instantiates it with the arguments read from the command line,) Tj T* 0 Tw 1.517045 Tw (wraps the resulting container object as an interpreter and runs it with the rest arguments found in the) Tj T* 0 Tw (command line. Here is the code to turn the ) Tj /F3 10 Tf (ShelveInterface ) Tj /F1 10 Tf (into a script) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 611.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 72 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 53.71 Tm /F3 10 Tf 12 TL (# ishelve3.py) Tj T* (from ishelve2 import ShelveInterface as main) Tj T* T* (if __name__ == '__main__':) Tj T* ( import plac; plac.Interpreter.call\(main\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 591.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (and here are a few examples of usage:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 378.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 204 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 185.71 Tm /F3 10 Tf 12 TL ($ python ishelve3.py -h) Tj T* (usage: ishelve3.py [-h] [-i] [-configfile CONFIGFILE] [args [args ...]]) Tj T* T* (positional arguments:) Tj T* ( args) Tj T* T* (optional arguments:) Tj T* ( -h, --help show this help message and exit) Tj T* ( -i, --interact start interactive interpreter) Tj T* ( -configfile CONFIGFILE) Tj T* ( path name of the shelve) Tj T* T* ($ python ishelve3.py set a 1) Tj T* (setting a=1) Tj T* ($ python ishelve3.py show a) Tj T* (a = 1) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 346.6236 cm +q +BT 1 0 0 1 0 16.82 Tm .079989 Tw 12 TL /F1 10 Tf 0 0 0 rg (If you pass the ) Tj /F3 10 Tf (-i ) Tj /F1 10 Tf (flag in the command line, then the script will enter in interactive mode and ask the user) Tj T* 0 Tw (for the commands to execute:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 253.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 84 re B* +Q +q +BT 1 0 0 1 0 65.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ($ python ishelve3.py -i) Tj T* (A minimal interface over a shelve object.) Tj T* (Operating on /home/micheles/conf.shelve.) Tj T* (.help to see the available commands.) Tj T* T* (i) Tj (>) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 209.4236 cm +q +BT 1 0 0 1 0 28.82 Tm .221417 Tw 12 TL /F1 10 Tf 0 0 0 rg (In a sense, I have closed the circle: at the beginning of this document I discussed how to turn a script into) Tj T* 0 Tw .784147 Tw (an interactive application \(the ) Tj /F3 10 Tf (shelve_interpreter.py ) Tj /F1 10 Tf (example\), whereas here I have show how to) Tj T* 0 Tw (turn an interactive application into a script.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 191.4236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (The complete signature of ) Tj /F3 10 Tf (plac.Interpreter.call ) Tj /F1 10 Tf (is the following:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 134.2236 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 +BT 1 0 0 1 0 29.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (call\(factory, arglist=sys.argv[1:],) Tj T* ( commentchar='#', split=shlex.split,) Tj T* ( stdin=sys.stdin, prompt='i) Tj (>) Tj ( ', verbose=False\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 90.22362 cm +q +BT 1 0 0 1 0 28.82 Tm 1.756651 Tw 12 TL /F1 10 Tf 0 0 0 rg (The factory must have a fixed number of positional arguments \(no default arguments, no varargs, no ) Tj T* 0 Tw 1.87881 Tw (kwargs\), otherwise a ) Tj /F3 10 Tf (TypeError ) Tj /F1 10 Tf (is raised: the reason is that we want to be able to distinguish the ) Tj T* 0 Tw .829984 Tw (command-line arguments needed to instantiate the factory from the rest arguments that must be sent to) Tj T* 0 Tw 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 235.3849 0 Td (27) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R364': class PDFStream +364 0 obj +% page stream +<< /Length 5072 >> +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 2.609984 Tw 12 TL /F1 10 Tf 0 0 0 rg (the corresponding interpreter object. It is also possible to specify a list of arguments different from) Tj T* 0 Tw .513318 Tw /F3 10 Tf (sys.argv[1:] ) Tj /F1 10 Tf (\(useful in tests\), the character to be recognized as a comment, the splitting function, the) Tj T* 0 Tw (input source and the prompt to use while in interactive mode, and a verbose flag.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 699.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Readline support) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 621.0236 cm +q +BT 1 0 0 1 0 64.82 Tm 1.022485 Tw 12 TL /F1 10 Tf 0 0 0 rg (Starting from release 0.6 ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (offers full readline support. That means that if your Python was compiled) Tj T* 0 Tw 2.120697 Tw (with readline support you get autocompletion and persistent command history for free. By default all) Tj T* 0 Tw .144104 Tw (commands are autocomplete in a case sensitive way. If you want to add new words to the autocompletion) Tj T* 0 Tw .116488 Tw (set, or you want to change the location of the ) Tj /F3 10 Tf (.history ) Tj /F1 10 Tf (file, or to change the case sensitivity, the way to) Tj T* 0 Tw .18436 Tw (go is to pass a ) Tj /F3 10 Tf (plac.ReadlineInput ) Tj /F1 10 Tf (object to the interpreter. Here is an example, assuming you want) Tj T* 0 Tw (to build a database interface understanding SQL commands:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 276.2307 cm +q +q +.96447 0 0 .96447 0 0 cm +q +1 0 0 1 6.6 6.843137 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 486 348 re B* +Q +q +BT 1 0 0 1 0 329.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (import os, plac) Tj T* (from sqlalchemy.ext.sqlsoup import SqlSoup) Tj T* T* (SQLKEYWORDS = set\(['select', 'from', 'inner', 'join', 'outer', 'left', 'right']) Tj T* ( \) # and many others) Tj T* (DBTABLES = set\(['table1', 'table2']\) # you can read them from the db schema) Tj T* T* (COMPLETIONS = SQLKEYWORDS | DBTABLES) Tj T* T* (class SqlInterface\(object\):) Tj T* ( commands = ['SELECT']) Tj T* ( def __init__\(self, dsn\):) Tj T* ( self.soup = SqlSoup\(dsn\)) Tj T* ( def SELECT\(self, argstring\):) Tj T* ( sql = 'SELECT ' + argstring) Tj T* ( for row in self.soup.bind.execute\(sql\):) Tj T* ( yield str\(row\) # the formatting can be much improved) Tj T* T* (rl_input = plac.ReadlineInput\() Tj T* ( COMPLETIONS, histfile=os.path.expanduser\('~/.sql_interface.history'\), ) Tj T* ( case_sensitive=False\)) Tj T* T* (def split_on_first_space\(line, commentchar\):) Tj T* ( return line.strip\(\).split\(' ', 1\) # ignoring comments) Tj T* ( ) Tj T* (if __name__ == '__main__':) Tj T* ( plac.Interpreter.call\(SqlInterface, split=split_on_first_space,) Tj T* ( stdin=rl_input, prompt='sql) Tj (>) Tj ( '\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 256.2307 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here is an example of usage:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 199.0307 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 +BT 1 0 0 1 0 29.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ($ python sql_interface.py ) Tj (<) Tj (some dsn) Tj (>) Tj T* (sql) Tj (>) Tj ( SELECT a.* FROM TABLE1 AS a INNER JOIN TABLE2 AS b ON a.id = b.id) Tj T* (...) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 119.0307 cm +q +BT 1 0 0 1 0 64.82 Tm 1.951318 Tw 12 TL /F1 10 Tf 0 0 0 rg (You can check that entering just ) Tj /F3 10 Tf (sel ) Tj /F1 10 Tf (and pressing TAB the readline library completes the ) Tj /F3 10 Tf (SELECT) Tj T* 0 Tw .797356 Tw /F1 10 Tf (keyword for you and makes it upper case; idem for ) Tj /F3 10 Tf (FROM) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (INNER) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (JOIN ) Tj /F1 10 Tf (and even for the names of the) Tj T* 0 Tw .256235 Tw (tables. An obvious improvement is to read the names of the tables by introspecting the database: actually) Tj T* 0 Tw 1.616654 Tw (you can even read the names of the views and of the columns, and have full autocompletion. All the) Tj T* 0 Tw 2.047251 Tw (entered commands and recorded and saved in the file ) Tj /F3 10 Tf (~/.sql_interface.history ) Tj /F1 10 Tf (when exiting) Tj T* 0 Tw (from the command-line interface.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 89.03071 cm +q +BT 1 0 0 1 0 16.82 Tm 2.010574 Tw 12 TL /F1 10 Tf 0 0 0 rg (If the readline library is not available, my suggestion is to use the ) Tj 0 0 .501961 rg (rlwrap ) Tj 0 0 0 rg (tool which provides similar ) Tj T* 0 Tw .22561 Tw (features, at least on Unix-like platforms. ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (should also work fine on Windows with the ) Tj 0 0 .501961 rg (pyreadline ) Tj 0 0 0 rg (library) Tj T* 0 Tw 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 235.3849 0 Td (28) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R365': class PDFStream +365 0 obj +% page stream +<< /Length 5002 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 717.0236 cm +q +BT 1 0 0 1 0 40.82 Tm .389989 Tw 12 TL /F1 10 Tf 0 0 0 rg (\(I do not use Windows, so this part is very little tested: I tried it only once and it worked, but your mileage) Tj T* 0 Tw 2.206457 Tw (may vary\). For people worried about licenses, I will notice that ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (uses the readline library only if) Tj T* 0 Tw .591894 Tw (available, it does not include it and it does not rely on it in any fundamental way, so that the ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (licence) Tj T* 0 Tw (does not need to be the GPL \(actually it is a BSD do-whatever-you-want-with-it licence\).) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 675.0236 cm +q +BT 1 0 0 1 0 28.82 Tm .187882 Tw 12 TL /F1 10 Tf 0 0 0 rg (The interactive mode of ) Tj /F3 10 Tf (plac ) Tj /F1 10 Tf (can be used as a replacement of the ) Tj 0 0 .501961 rg (cmd ) Tj 0 0 0 rg (module in the standard library. It) Tj T* 0 Tw 2.730651 Tw (is actually better than ) Tj 0 0 .501961 rg (cmd) Tj 0 0 0 rg (: for instance, the ) Tj /F3 10 Tf (.help ) Tj /F1 10 Tf (command is more powerful, since it provides) Tj T* 0 Tw (information about the arguments accepted by the given command:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 365.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 300 re B* +Q +q +BT 1 0 0 1 0 281.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (i) Tj (>) Tj ( .help set) Tj T* (usage: set name value) Tj T* T* (set name value) Tj T* T* (positional arguments:) Tj T* ( name) Tj T* ( value) Tj T* T* (i) Tj (>) Tj ( .help delete) Tj T* (usage: delete [name]) Tj T* T* (delete given parameter \(or everything\)) Tj T* T* (positional arguments:) Tj T* ( name) Tj T* T* (i) Tj (>) Tj ( .help show) Tj T* (usage: show [names [names ...]]) Tj T* T* (show given parameters) Tj T* T* (positional arguments:) Tj T* ( names) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 309.8236 cm +q +BT 1 0 0 1 0 40.82 Tm 1.959985 Tw 12 TL /F1 10 Tf 0 0 0 rg (As you can imagine, the help message is provided by the underlying ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (subparser \(there is a) Tj T* 0 Tw 2.954524 Tw (subparser for each command\). ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (commands accept options, flags, varargs, keyword arguments,) Tj T* 0 Tw .719318 Tw (arguments with defaults, arguments with a fixed number of choices, type conversion and all the features) Tj T* 0 Tw (provided of ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (which should be reimplemented from scratch using ) Tj 0 0 .501961 rg (plac) Tj 0 0 0 rg (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 279.8236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.78248 Tw 12 TL /F1 10 Tf 0 0 0 rg (Moreover at the moment ) Tj /F3 10 Tf (plac ) Tj /F1 10 Tf (also understands command abbreviations. However, this feature may) Tj T* 0 Tw (disappear in future releases. It was meaningful in the past, when ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (did not support readline.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 261.8236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (Notice that if an abbreviation is ambiguous, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (warns you:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 216.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 36 re B* +Q +q +BT 1 0 0 1 0 17.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (i) Tj (>) Tj ( sh) Tj T* (NameError: Ambiguous command 'sh': matching ['showall', 'show']) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 186.6236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (The plac runner) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 120.6236 cm +q +BT 1 0 0 1 0 52.82 Tm 1.531318 Tw 12 TL /F1 10 Tf 0 0 0 rg (The distribution of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (includes a runner script named ) Tj /F3 10 Tf (plac_runner.py) Tj /F1 10 Tf (, which will be installed in a) Tj T* 0 Tw .44748 Tw (suitable directory in your system by ) Tj 0 0 .501961 rg (distutils ) Tj 0 0 0 rg (\(say in ) Tj /F3 10 Tf (\\usr\\local\\bin\\plac_runner.py ) Tj /F1 10 Tf (in a Unix-like) Tj T* 0 Tw .680651 Tw (operative system\). The runner provides many facilities to run ) Tj /F3 10 Tf (.plac ) Tj /F1 10 Tf (scripts and ) Tj /F3 10 Tf (.placet ) Tj /F1 10 Tf (files, as well) Tj T* 0 Tw 1.47311 Tw (as Python modules containg a ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (object, which can be a function, a command container object or) Tj T* 0 Tw (even a command container class.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 90.62362 cm +q +BT 1 0 0 1 0 16.82 Tm 1.994269 Tw 12 TL /F1 10 Tf 0 0 0 rg (For instance, suppose you want to execute a script containing commands defined in the ) Tj /F3 10 Tf (ishelve2) Tj T* 0 Tw /F1 10 Tf (module like the following one:) 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 235.3849 0 Td (29) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R366': class PDFStream +366 0 obj +% page stream +<< /Length 4243 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 703.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 (#!ishelve2.py:ShelveInterface -c ~/conf.shelve) Tj T* (set a 1) Tj T* (del a) Tj T* (del a # intentional error) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 635.8236 cm +q +BT 1 0 0 1 0 52.82 Tm .575868 Tw 12 TL /F1 10 Tf 0 0 0 rg (The first line of the ) Tj /F3 10 Tf (.plac ) Tj /F1 10 Tf (script contains the name of the python module containing the plac interpreter) Tj T* 0 Tw 2.327209 Tw (and the arguments which must be passed to its main function in order to be able to instantiate an) Tj T* 0 Tw .202485 Tw (interpreter object. In this case I appended ) Tj /F3 10 Tf (:ShelveInterface ) Tj /F1 10 Tf (to the name of the module to specify the) Tj T* 0 Tw 1.030574 Tw (object that must be imported: if not specified, by default the object named 'main' is imported. The other) Tj T* 0 Tw (lines contains commands. You can run the script as follows:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 546.6505 cm +q +q +.952737 0 0 .952737 0 0 cm +q +1 0 0 1 6.6 6.927412 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 492 84 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 65.71 Tm /F3 10 Tf 12 TL ($ plac_runner.py --batch ishelve2.plac) Tj T* (setting a=1) Tj T* (deleting a) Tj T* (Traceback \(most recent call last\):) Tj T* ( ...) Tj T* (_bsddb.DBNotFoundError: \(-30988, 'DB_NOTFOUND: No matching key/data pair found'\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 514.6505 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL 2.79186 Tw (The last command intentionally contained an error, to show that the plac runner does not eat the) Tj T* 0 Tw (traceback.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 484.6505 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL .437633 Tw (The runner can also be used to run Python modules in interactive mode and non-interactive mode. If you) Tj T* 0 Tw (put this alias in your bashrc) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 478.6505 cm +Q +q +1 0 0 1 62.69291 466.6505 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +BT 1 0 0 1 0 2 Tm T* ET +q +1 0 0 1 20 0 cm +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL (alias plac="plac_runner.py") Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 466.6505 cm +Q +q +1 0 0 1 62.69291 436.6505 cm +q +BT 1 0 0 1 0 16.82 Tm 2.955318 Tw 12 TL /F1 10 Tf 0 0 0 rg (\(or you define a suitable ) Tj /F3 10 Tf (plac.bat ) Tj /F1 10 Tf (script in Windows\) you can run the ) Tj /F3 10 Tf (ishelve2.py ) Tj /F1 10 Tf (script in) Tj T* 0 Tw (interactive mode as follows:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 259.4505 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 ($ plac -i ishelve2.py:ShelveInterface) Tj T* (A minimal interface over a shelve object.) Tj T* (Operating on /home/micheles/conf.shelve.) Tj T* (.help to see the available commands.) Tj T* T* (i) Tj (>) Tj ( del) Tj T* (deleting everything) Tj T* (i) Tj (>) Tj ( set a 1) Tj T* (setting a=1) Tj T* (i) Tj (>) Tj ( set b 2) Tj T* (setting b=2) Tj T* (i) Tj (>) Tj ( show b) Tj T* (b = 2) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 239.4505 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (Now you can cut and paste the interactive session an turns into into a ) Tj /F3 10 Tf (.placet ) Tj /F1 10 Tf (file like the following:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 110.2505 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 +BT 1 0 0 1 0 101.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (#!ishelve2.py:ShelveInterface -configfile=~/test.shelve) Tj T* (i) Tj (>) Tj ( del) Tj T* (deleting everything) Tj T* (i) Tj (>) Tj ( set a 1) Tj T* (setting a=1) Tj T* (i) Tj (>) Tj ( set b 2) Tj T* (setting b=2) Tj T* (i) Tj (>) Tj ( show a) Tj T* (a = 1) 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 235.3849 0 Td (30) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R367': class PDFStream +367 0 obj +% page stream +<< /Length 5344 >> +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 16.82 Tm 2.145697 Tw 12 TL /F1 10 Tf 0 0 0 rg (Notice that the first line specifies a test database ) Tj /F3 10 Tf (~/test.shelve) Tj /F1 10 Tf (, to avoid clobbering your default) Tj T* 0 Tw (shelve. If you mispell the arguments in the first line plac will give you an ) Tj 0 0 .501961 rg (argparse ) Tj 0 0 0 rg (error message \(just try\).) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 723.0236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (You can run placets following the shebang convention directly with the plac runner:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 677.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 ($ plac --test ishelve2.placet) Tj T* (run 1 plac test\(s\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 633.8236 cm +q +BT 1 0 0 1 0 28.82 Tm .32104 Tw 12 TL /F1 10 Tf 0 0 0 rg (If you want to see the output of the tests, pass the ) Tj /F3 10 Tf (-v/--verbose ) Tj /F1 10 Tf (flag. Notice that he runner ignore the) Tj T* 0 Tw .24856 Tw (extension, so you can actually use any extension your like, but ) Tj /F4 10 Tf (it relies on the first line of the file to invoke) Tj T* 0 Tw (the corresponding plac tool with the given arguments) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 603.8236 cm +q +BT 1 0 0 1 0 16.82 Tm .537209 Tw 12 TL /F1 10 Tf 0 0 0 rg (The plac runner does not provide any test discovery facility, but you can use standard Unix tools to help.) Tj T* 0 Tw (For instance, you can run all the ) Tj /F3 10 Tf (.placet ) Tj /F1 10 Tf (files into a directory and its subdirectories as follows:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 570.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 ($ find . -name \\*.placet | xargs plac_runner.py -t) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 538.6236 cm +q +BT 1 0 0 1 0 16.82 Tm .760988 Tw 12 TL /F1 10 Tf 0 0 0 rg (The plac runner expects the main function of your script to return a plac tool, i.e. a function or an object) Tj T* 0 Tw (with a ) Tj /F3 10 Tf (.commands ) Tj /F1 10 Tf (attribute. It this is not the case the runner gracefully exits.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 520.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (It also works in non-interactive mode, if you call it as) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 514.6236 cm +Q +q +1 0 0 1 62.69291 502.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +BT 1 0 0 1 0 2 Tm T* ET +q +1 0 0 1 20 0 cm +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL ($ plac module.py args ...) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 502.6236 cm +Q +q +1 0 0 1 62.69291 484.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here is an example:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 415.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 60 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 41.71 Tm /F3 10 Tf 12 TL ($ plac ishelve.py a=1) Tj T* (setting a=1) Tj T* ($ plac ishelve.py .show) Tj T* (a=1) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 383.4236 cm +q +BT 1 0 0 1 0 16.82 Tm .01561 Tw 12 TL /F1 10 Tf 0 0 0 rg (Notice that in non-interactive mode the runner just invokes ) Tj /F3 10 Tf (plac.call ) Tj /F1 10 Tf (on the ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (object of the Python) Tj T* 0 Tw (module.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 353.4236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (A non class-based example) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 311.4236 cm +q +BT 1 0 0 1 0 28.82 Tm .907209 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (does not force you to use classes to define command containers. Even a simple function can be a) Tj T* 0 Tw 1.796651 Tw (valid command container, it is enough to add to it a ) Tj /F3 10 Tf (.commands ) Tj /F1 10 Tf (attribute and possibly ) Tj /F3 10 Tf (__enter__) Tj T* 0 Tw /F1 10 Tf (and/or ) Tj /F3 10 Tf (__exit__ ) Tj /F1 10 Tf (attributes.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 281.4236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL .327485 Tw (In particular, a Python module is a perfect container of commands. As an example, consider the following) Tj T* 0 Tw (module implementing a fake Version Control System:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 92.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 180 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 161.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\(url='url of the source code'\)) Tj T* (def checkout\(url\):) Tj T* ( "A fake checkout command") Tj T* ( return \('checkout ', url\)) Tj T* T* (@plac.annotations\(message=\('commit message', 'option'\)\)) Tj T* (def commit\(message\):) Tj T* ( "A fake commit command") 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 235.3849 0 Td (31) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R368': class PDFStream +368 0 obj +% page stream +<< /Length 3894 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 559.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 204 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 185.71 Tm /F3 10 Tf 12 TL ( return \('commit ', message\)) Tj T* T* (@plac.annotations\(quiet=\('summary information', 'flag', 'q'\)\)) Tj T* (def status\(quiet\):) Tj T* ( "A fake status command") Tj T* ( return \('status ', quiet\)) Tj T* T* (def __missing__\(name\):) Tj T* ( return 'Command %r does not exist' % name) Tj T* T* (def __exit__\(etype, exc, tb\):) Tj T* ( "Will be called automatically at the end of the call/cmdloop") Tj T* ( if etype in \(None, GeneratorExit\): # success) Tj T* ( print\('ok'\)) Tj T* T* (main = __import__\(__name__\) # the module imports itself!) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 515.8236 cm +q +BT 1 0 0 1 0 28.82 Tm .431318 Tw 12 TL /F1 10 Tf 0 0 0 rg (Notice that I have defined both an ) Tj /F3 10 Tf (__exit__ ) Tj /F1 10 Tf (hook and a ) Tj /F3 10 Tf (__missing__ ) Tj /F1 10 Tf (hook, invoked for non-existing) Tj T* 0 Tw .592651 Tw (commands. The real trick here is the line ) Tj /F3 10 Tf (main = __import__\(__name__\)) Tj /F1 10 Tf (, which define ) Tj /F3 10 Tf (main ) Tj /F1 10 Tf (to be) Tj T* 0 Tw (an alias for the current module.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 485.8236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.259986 Tw 12 TL /F1 10 Tf 0 0 0 rg (The ) Tj /F3 10 Tf (vcs ) Tj /F1 10 Tf (module does not contain an ) Tj /F3 10 Tf (if __name__ == '__main__' ) Tj /F1 10 Tf (block, but you can still run it) Tj T* 0 Tw (through the plac runner \(try ) Tj /F3 10 Tf (plac vcs.py -h) Tj /F1 10 Tf (\):) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 344.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: plac_runner.py 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 324.6236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (You can get help for the subcommands by postponing ) Tj /F3 10 Tf (-h ) Tj /F1 10 Tf (after the name of the command:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 207.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 108 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 89.71 Tm /F3 10 Tf 12 TL ($ plac vcs.py status -h) Tj T* (usage: vcs.py status [-h] [-q]) Tj T* T* (A fake status command) Tj T* T* (optional arguments:) Tj T* ( -h, --help show this help message and exit) Tj T* ( -q, --quiet summary information) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 175.4236 cm +q +BT 1 0 0 1 0 16.82 Tm 2.064985 Tw 12 TL /F1 10 Tf 0 0 0 rg (Notice how the docstring of the command is automatically shown in usage message, as well as the) Tj T* 0 Tw (documentation for the sub flag ) Tj /F3 10 Tf (-q) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 157.4236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here is an example of a non-interactive session:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 100.2236 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 ($ plac vcs.py check url) Tj T* (checkout) Tj T* (url) 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 235.3849 0 Td (32) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R369': class PDFStream +369 0 obj +% page stream +<< /Length 5647 >> +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 +0 0 0 rg +BT 1 0 0 1 0 65.71 Tm /F3 10 Tf 12 TL ($ plac vcs.py st -q) Tj T* (status) Tj T* (True) Tj T* ($ plac vcs.py co) Tj T* (commit) Tj T* (None) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 659.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (and here is an interactive session:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 458.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 192 re B* +Q +q +BT 1 0 0 1 0 173.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ($ plac -i vcs.py) Tj T* (usage: plac_runner.py vcs.py [-h] {status,commit,checkout} ...) Tj T* (i) Tj (>) Tj ( check url) Tj T* (checkout) Tj T* (url) Tj T* (i) Tj (>) Tj ( st -q) Tj T* (status) Tj T* (True) Tj T* (i) Tj (>) Tj ( co) Tj T* (commit) Tj T* (None) Tj T* (i) Tj (>) Tj ( sto) Tj T* (Command 'sto' does not exist) Tj T* (i) Tj (>) Tj ( [CTRL-D]) Tj T* (ok) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 426.6236 cm +q +BT 1 0 0 1 0 16.82 Tm 2.986905 Tw 12 TL /F1 10 Tf 0 0 0 rg (Notice the invocation of the ) Tj /F3 10 Tf (__missing__ ) Tj /F1 10 Tf (hook for non-existing commands. Notice also that the) Tj T* 0 Tw /F3 10 Tf (__exit__ ) Tj /F1 10 Tf (hook gets called only in interactive mode.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 396.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.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 (situations, it is best to use a custom class.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 366.6236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Writing your own plac runner) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 312.6236 cm +q +BT 1 0 0 1 0 40.82 Tm .167209 Tw 12 TL /F1 10 Tf 0 0 0 rg (The runner included in the ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (distribution is intentionally kept small \(around 50 lines of code\) so that you) Tj T* 0 Tw .081294 Tw (can study it and write your own runner if want to. If you need to go to such level of detail, you should know) Tj T* 0 Tw .42061 Tw (that the most important method of the ) Tj /F3 10 Tf (Interpreter ) Tj /F1 10 Tf (class is the ) Tj /F3 10 Tf (.send ) Tj /F1 10 Tf (method, which takes strings in) Tj T* 0 Tw (input and returns a four-tuple with attributes ) Tj /F3 10 Tf (.str) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (.etype) Tj /F1 10 Tf (, ) Tj /F3 10 Tf (.exc ) Tj /F1 10 Tf (and ) Tj /F3 10 Tf (.tb) Tj /F1 10 Tf (:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 306.6236 cm +Q +q +1 0 0 1 62.69291 306.6236 cm +Q +q +1 0 0 1 62.69291 288.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F3 10 Tf 0 0 0 rg (.str ) Tj /F1 10 Tf (is the output of the command, if successful \(a string\);) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 288.6236 cm +Q +q +1 0 0 1 62.69291 288.6236 cm +Q +q +1 0 0 1 62.69291 270.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F3 10 Tf 0 0 0 rg (.etype ) Tj /F1 10 Tf (is the class of the exception, if the command fail;) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 270.6236 cm +Q +q +1 0 0 1 62.69291 270.6236 cm +Q +q +1 0 0 1 62.69291 252.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F3 10 Tf 0 0 0 rg (.exc ) Tj /F1 10 Tf (is the exception instance;) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 252.6236 cm +Q +q +1 0 0 1 62.69291 252.6236 cm +Q +q +1 0 0 1 62.69291 234.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F3 10 Tf 0 0 0 rg (.tb ) Tj /F1 10 Tf (is the traceback.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 234.6236 cm +Q +q +1 0 0 1 62.69291 234.6236 cm +Q +q +1 0 0 1 62.69291 192.6236 cm +q +BT 1 0 0 1 0 28.82 Tm .937485 Tw 12 TL /F1 10 Tf 0 0 0 rg (Moreover the ) Tj /F3 10 Tf (__str__ ) Tj /F1 10 Tf (representation of the output object is redefined to return the output string if the) Tj T* 0 Tw 2.686651 Tw (command was successful or the error message if the command failed \(actually it returns the error) Tj T* 0 Tw (message preceded by the name of the exception class\).) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 174.6236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (For instance, if you send a mispelled option to the interpreter a ) Tj /F3 10 Tf (SystemExit ) Tj /F1 10 Tf (will be trapped:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 93.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 72 re B* +Q +q +BT 1 0 0 1 0 53.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (>) Tj (>) Tj (>) Tj ( import plac) Tj T* (>) Tj (>) Tj (>) Tj ( from ishelve import ishelve) Tj T* (>) Tj (>) Tj (>) Tj ( with plac.Interpreter\(ishelve\) as i:) Tj T* (... print\(i.send\('.cler'\)\)) Tj T* (...) 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 235.3849 0 Td (33) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R370': class PDFStream +370 0 obj +% page stream +<< /Length 5039 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 739.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 24 re B* +Q +q +BT 1 0 0 1 0 5.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (SystemExit: unrecognized arguments: .cler) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 707.8236 cm +q +BT 1 0 0 1 0 16.82 Tm 2.90561 Tw 12 TL /F1 10 Tf 0 0 0 rg (It is important to invoke the ) Tj /F3 10 Tf (.send ) Tj /F1 10 Tf (method inside the context manager, otherwise you will get a) Tj T* 0 Tw /F3 10 Tf (RuntimeError) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 665.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 28.82 Tm /F1 10 Tf 12 TL .29311 Tw (For instance, suppose you want to implement a graphical runner for a plac-based interpreter with two text) Tj T* 0 Tw 1.548221 Tw (widgets: one to enter the commands and one to display the results. Suppose you want to display the) Tj T* 0 Tw (errors with tracebacks in red. You will need to code something like that \(pseudocode follows\):) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 428.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 228 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 209.71 Tm /F3 10 Tf 12 TL (input_widget = WidgetReadingInput\(\)) Tj T* (output_widget = WidgetDisplayingOutput\(\)) Tj T* T* (def send\(interpreter, line\):) Tj T* ( out = interpreter.send\(line\)) Tj T* ( if out.tb: # there was an error) Tj T* ( output_widget.display\(out.tb, color='red'\)) Tj T* ( else:) Tj T* ( output_widget.display\(out.str\)) Tj T* T* (main = plac.import_main\(tool_path\) # get the main object) Tj T* T* (with plac.Interpreter\(main\) as i:) Tj T* ( def callback\(event\):) Tj T* ( if event.user_pressed_ENTER\(\):) Tj T* ( send\(i, input_widget.last_line\)) Tj T* ( input_widget.addcallback\(callback\)) Tj T* ( gui_mainloop.start\(\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 396.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL .102765 Tw (You can adapt the pseudocode to your GUI toolkit of choice and you can also change the file associations) Tj T* 0 Tw (in such a way that clicking on a plac tool file the graphical user interface starts.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 366.6236 cm +q +BT 1 0 0 1 0 16.82 Tm .259988 Tw 12 TL /F1 10 Tf 0 0 0 rg (An example of GUI program built on top of ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is given later on, in the paragraph ) Tj /F4 10 Tf (Managing the output of) Tj T* 0 Tw (concurrent commands ) Tj /F1 10 Tf (\(using Tkinter for simplicity and portability\).) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 312.6236 cm +q +BT 1 0 0 1 0 40.82 Tm 2.090651 Tw 12 TL /F1 10 Tf 0 0 0 rg (There is a final ) Tj /F4 10 Tf (caveat) Tj /F1 10 Tf (: since the plac interpreter loop is implemented via extended generators, plac) Tj T* 0 Tw .988651 Tw (interpreters are single threaded: you will get an error if you ) Tj /F3 10 Tf (.send ) Tj /F1 10 Tf (commands from separated threads.) Tj T* 0 Tw .947882 Tw (You can circumvent the problem by using a queue. If EXIT is a sentinel value to signal exiting from the) Tj T* 0 Tw (interpreter look, you can write code like this:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 255.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 48 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 29.71 Tm /F3 10 Tf 12 TL (with interpreter:) Tj T* ( for input_value in iter\(input_queue.get, EXIT\):) Tj T* ( output_queue.put\(interpreter.send\(input_value\)\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 223.4236 cm +q +BT 1 0 0 1 0 16.82 Tm .106098 Tw 12 TL /F1 10 Tf 0 0 0 rg (The same trick also work for processes; you could run the interpreter loop in a separate process and send) Tj T* 0 Tw (commands to it via the Queue class provided by the ) Tj 0 0 .501961 rg (multiprocessing ) Tj 0 0 0 rg (module.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 193.4236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Long running commands) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 151.4236 cm +q +BT 1 0 0 1 0 28.82 Tm 1.434431 Tw 12 TL /F1 10 Tf 0 0 0 rg (As we saw, by default a ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (interpreter blocks until the command terminates. This is an issue, in the) Tj T* 0 Tw 1.201318 Tw (sense that it makes the interactive experience quite painful for long running commands. An example is) Tj T* 0 Tw (better than a thousand words, so consider the following fake importer:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 94.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 48 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 29.71 Tm /F3 10 Tf 12 TL (import time) Tj T* (import plac) Tj T* 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 235.3849 0 Td (34) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R371': class PDFStream +371 0 obj +% page stream +<< /Length 4419 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 547.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 216 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 197.71 Tm /F3 10 Tf 12 TL (class FakeImporter\(object\):) Tj T* ( "A fake importer with an import_file command") Tj T* ( commands = ['import_file']) Tj T* ( def __init__\(self, dsn\):) Tj T* ( self.dsn = dsn) Tj T* ( def import_file\(self, fname\):) Tj T* ( "Import a file into the database") Tj T* ( try:) Tj T* ( for n in range\(10000\):) Tj T* ( time.sleep\(.01\)) Tj T* ( if n % 100 == 99:) Tj T* ( yield 'Imported %d lines' % \(n+1\)) Tj T* ( finally:) Tj T* ( print\('closing the file'\)) Tj T* T* (if __name__ == '__main__':) Tj T* ( plac.Interpreter.call\(FakeImporter\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 515.8236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.466457 Tw 12 TL /F1 10 Tf 0 0 0 rg (If you run the ) Tj /F3 10 Tf (import_file ) Tj /F1 10 Tf (command, you will have to wait for 200 seconds before entering a new) Tj T* 0 Tw (command:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 374.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 +BT 1 0 0 1 0 113.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ($ python importer1.py dsn) Tj T* (A fake importer with an import_file command) Tj T* (i) Tj (>) Tj ( import_file file1) Tj T* (... ) Tj (<) Tj (wait 3+ minutes) Tj (>) Tj T* (Imported 100 lines) Tj T* (Imported 200 lines) Tj T* (Imported 300 lines) Tj T* (...) Tj T* (Imported 10000 lines) Tj T* (closing the file) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 330.6236 cm +q +BT 1 0 0 1 0 28.82 Tm .96832 Tw 12 TL /F1 10 Tf 0 0 0 rg (Being unable to enter any other command is quite annoying: in such situation one would like to run the) Tj T* 0 Tw .941318 Tw (long running commands in the background, to keep the interface responsive. ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (provides two ways to) Tj T* 0 Tw (reach this goal: threads and processes.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 300.6236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Threaded commands) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 270.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL .317988 Tw (The most familiar way to execute a task in the background \(even if not necessarily the best way\) is to run) Tj T* 0 Tw (it into a separated thread. In our example it is sufficient to replace the line) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 264.6236 cm +Q +q +1 0 0 1 62.69291 252.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +BT 1 0 0 1 0 2 Tm T* ET +q +1 0 0 1 20 0 cm +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL (commands = ['import_file']) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 252.6236 cm +Q +q +1 0 0 1 62.69291 234.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (with) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 228.6236 cm +Q +q +1 0 0 1 62.69291 216.6236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +BT 1 0 0 1 0 2 Tm T* ET +q +1 0 0 1 20 0 cm +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL (thcommands = ['import_file']) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 216.6236 cm +Q +q +1 0 0 1 62.69291 186.6236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.38311 Tw 12 TL /F1 10 Tf 0 0 0 rg (to tell to the ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (interpreter that the command ) Tj /F3 10 Tf (import_file ) Tj /F1 10 Tf (should be run into a separated thread.) Tj T* 0 Tw (Here is an example session:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 141.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 36 re B* +Q +q +BT 1 0 0 1 0 17.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (i) Tj (>) Tj ( import_file file1) Tj T* (<) Tj (ThreadedTask 1 [import_file file1] RUNNING) Tj (>) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 109.4236 cm +q +BT 1 0 0 1 0 16.82 Tm .595777 Tw 12 TL /F1 10 Tf 0 0 0 rg (The import task started in a separated thread. You can see the progress of the task by using the special) Tj T* 0 Tw (command ) Tj /F3 10 Tf (.output) Tj /F1 10 Tf (:) 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 235.3849 0 Td (35) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R372': class PDFStream +372 0 obj +% page stream +<< /Length 4933 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 703.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 +BT 1 0 0 1 0 41.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (i) Tj (>) Tj ( .output 1) Tj T* (<) Tj (ThreadedTask 1 [import_file file1] RUNNING) Tj (>) Tj T* (Imported 100 lines) Tj T* (Imported 200 lines) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 683.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (If you look after a while, you will get more lines of output:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 590.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 84 re B* +Q +q +BT 1 0 0 1 0 65.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (i) Tj (>) Tj ( .output 1) Tj T* (<) Tj (ThreadedTask 1 [import_file file1] RUNNING) Tj (>) Tj T* (Imported 100 lines) Tj T* (Imported 200 lines) Tj T* (Imported 300 lines) Tj T* (Imported 400 lines) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 570.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (If you look after a time long enough, the task will be finished:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 525.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 36 re B* +Q +q +BT 1 0 0 1 0 17.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (i) Tj (>) Tj ( .output 1) Tj T* (<) Tj (ThreadedTask 1 [import_file file1] FINISHED) Tj (>) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 493.4236 cm +q +BT 1 0 0 1 0 16.82 Tm 1.045868 Tw 12 TL /F1 10 Tf 0 0 0 rg (You can even skip the number argument: then ) Tj /F3 10 Tf (.output ) Tj /F1 10 Tf (will the return the output of the last launched) Tj T* 0 Tw (command \(the special commands like .output do not count\).) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 475.4236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (You can launch many tasks one after the other:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 406.2236 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 +BT 1 0 0 1 0 41.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (i) Tj (>) Tj ( import_file file2) Tj T* (<) Tj (ThreadedTask 5 [import_file file2] RUNNING) Tj (>) Tj T* (i) Tj (>) Tj ( import_file file3) Tj T* (<) Tj (ThreadedTask 6 [import_file file3] RUNNING) Tj (>) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 386.2236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (The ) Tj /F3 10 Tf (.list ) Tj /F1 10 Tf (command displays all the running tasks:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 329.0236 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 +BT 1 0 0 1 0 29.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (i) Tj (>) Tj ( .list) Tj T* (<) Tj (ThreadedTask 5 [import_file file2] RUNNING) Tj (>) Tj T* (<) Tj (ThreadedTask 6 [import_file file3] RUNNING) Tj (>) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 309.0236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (It is even possible to kill a task:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 215.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 (i) Tj (>) Tj ( .kill 5) Tj T* (<) Tj (ThreadedTask 5 [import_file file2] TOBEKILLED) Tj (>) Tj T* (# wait a bit ...) Tj T* (closing the file) Tj T* (i) Tj (>) Tj ( .output 5) Tj T* (<) Tj (ThreadedTask 5 [import_file file2] KILLED) Tj (>) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 123.8236 cm +q +BT 1 0 0 1 0 76.82 Tm 1.100542 Tw 12 TL /F1 10 Tf 0 0 0 rg (You should notice that since at the Python level it is impossible to kill a thread, the ) Tj /F3 10 Tf (.kill ) Tj /F1 10 Tf (commands) Tj T* 0 Tw 1.793984 Tw (works by setting the status of the task to ) Tj /F3 10 Tf (TOBEKILLED) Tj /F1 10 Tf (. Internally the generator corresponding to the) Tj T* 0 Tw .632927 Tw (command is executed in the thread and the status is checked at each iteration: when the status become) Tj T* 0 Tw 2.578443 Tw /F3 10 Tf (TOBEKILLED ) Tj /F1 10 Tf (a ) Tj /F3 10 Tf (GeneratorExit ) Tj /F1 10 Tf (exception is raised and the thread terminates \(softly, so that the) Tj T* 0 Tw 2.328651 Tw /F3 10 Tf (finally ) Tj /F1 10 Tf (clause is honored\). In our example the generator is yielding back control once every 100) Tj T* 0 Tw 1.152619 Tw (iterations, i.e. every two seconds \(not much\). In order to get a responsive interface it is a good idea to) Tj T* 0 Tw (yield more often, for instance every 10 iterations \(i.e. 5 times per second\), as in the following code:) 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 235.3849 0 Td (36) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R373': class PDFStream +373 0 obj +% page stream +<< /Length 4291 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 487.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 276 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 257.71 Tm /F3 10 Tf 12 TL (import time) Tj T* (import plac) Tj T* T* (class FakeImporter\(object\):) Tj T* ( "A fake importer with an import_file command") Tj T* ( thcommands = ['import_file']) Tj T* ( def __init__\(self, dsn\):) Tj T* ( self.dsn = dsn) Tj T* ( def import_file\(self, fname\):) Tj T* ( "Import a file into the database") Tj T* ( try:) Tj T* ( for n in range\(10000\):) Tj T* ( time.sleep\(.02\)) Tj T* ( if n % 100 == 99: # every two seconds) Tj T* ( yield 'Imported %d lines' % \(n+1\)) Tj T* ( if n % 10 == 9: # every 0.2 seconds) Tj T* ( yield # go back and check the TOBEKILLED status) Tj T* ( finally:) Tj T* ( print\('closing the file'\)) Tj T* T* (if __name__ == '__main__':) Tj T* ( plac.Interpreter.call\(FakeImporter\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 457.8236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Running commands as external processes) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 403.8236 cm +q +BT 1 0 0 1 0 40.82 Tm 2.30686 Tw 12 TL /F1 10 Tf 0 0 0 rg (Threads are not loved much in the Python world and actually most people prefer to use processes) Tj T* 0 Tw 3.350697 Tw (instead. For this reason ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (provides the option to execute long running commands as external) Tj T* 0 Tw .632706 Tw (processes. Unfortunately the current implementation only works in Unix-like operating systems \(including) Tj T* 0 Tw (Mac OS X\) because it relies on fork via the ) Tj 0 0 .501961 rg (multiprocessing ) Tj 0 0 0 rg (module.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 385.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (In our example, to enable the feature it is sufficient to replace the line) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 379.8236 cm +Q +q +1 0 0 1 62.69291 367.8236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +BT 1 0 0 1 0 2 Tm T* ET +q +1 0 0 1 20 0 cm +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL (thcommands = ['import_file']) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 367.8236 cm +Q +q +1 0 0 1 62.69291 349.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (with) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 343.8236 cm +Q +q +1 0 0 1 62.69291 331.8236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +BT 1 0 0 1 0 2 Tm T* ET +q +1 0 0 1 20 0 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F3 10 Tf 0 0 0 rg (mpcommands = ['import_file']) Tj /F1 10 Tf (.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 331.8236 cm +Q +q +1 0 0 1 62.69291 301.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL .772619 Tw (The user experience is exactly the same as with threads and you will not see any difference at the user) Tj T* 0 Tw (interface level:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 160.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 +BT 1 0 0 1 0 113.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (i) Tj (>) Tj ( import_file file3) Tj T* (<) Tj (MPTask 1 [import_file file3] SUBMITTED) Tj (>) Tj T* (i) Tj (>) Tj ( .kill 1) Tj T* (<) Tj (MPTask 1 [import_file file3] RUNNING) Tj (>) Tj T* (closing the file) Tj T* (i) Tj (>) Tj ( .o 1) Tj T* (<) Tj (MPTask 1 [import_file file3] KILLED) Tj (>) Tj T* (Imported 100 lines) Tj T* (Imported 200 lines) Tj T* (i) Tj (>) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 104.6236 cm +q +0 0 0 rg +BT 1 0 0 1 0 40.82 Tm /F1 10 Tf 12 TL 1.201318 Tw (Still, using processes is quite different than using threads: in particular, when using processes you can) Tj T* 0 Tw 2.313318 Tw (only yield pickleable values and you cannot re-raise an exception first raised in a different process,) Tj T* 0 Tw 1.445697 Tw (because traceback objects are not pickleable. Moreover, you cannot rely on automatic sharing of your) Tj T* 0 Tw (objects.) 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 235.3849 0 Td (37) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R374': class PDFStream +374 0 obj +% page stream +<< /Length 4180 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 717.0236 cm +q +BT 1 0 0 1 0 40.82 Tm .128935 Tw 12 TL /F1 10 Tf 0 0 0 rg (On the plus side, when using processes you do not need to worry about killing a command: they are killed) Tj T* 0 Tw .466412 Tw (immediately using a SIGTERM signal, and there is not a ) Tj /F3 10 Tf (TOBEKILLED ) Tj /F1 10 Tf (mechanism. Moreover, the killing) Tj T* 0 Tw 1.50229 Tw (is guaranteed to be soft: internally a command receiving a SIGTERM raises a ) Tj /F3 10 Tf (TerminatedProcess) Tj T* 0 Tw /F1 10 Tf (exception which is trapped in the generator loop, so that the command is closed properly.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 687.0236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL .242927 Tw (Using processes allows to take full advantage of multicore machines and it is safer than using threads, so) Tj T* 0 Tw (it is the recommended approach unless you are working on Windows.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 657.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Managing the output of concurrent commands) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 555.0236 cm +q +BT 1 0 0 1 0 88.82 Tm 1.895542 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (acts as a command-line task launcher and can be used as the base to build a GUI-based task) Tj T* 0 Tw .38561 Tw (launcher and task monitor. To this aim the interpreter class provides a ) Tj /F3 10 Tf (.submit ) Tj /F1 10 Tf (method which returns a) Tj T* 0 Tw 1.792339 Tw (task object and a ) Tj /F3 10 Tf (.tasks ) Tj /F1 10 Tf (method returning the list of all the tasks submitted to the interpreter. The) Tj T* 0 Tw .373516 Tw /F3 10 Tf (submit ) Tj /F1 10 Tf (method does not start the task and thus it is nonblocking. Each task has an ) Tj /F3 10 Tf (.outlist ) Tj /F1 10 Tf (attribute) Tj T* 0 Tw .106098 Tw (which is a list storing the value yielded by the generator underlying the task \(the ) Tj /F3 10 Tf (None ) Tj /F1 10 Tf (values are skipped) Tj T* 0 Tw .633318 Tw (though\): the ) Tj /F3 10 Tf (.outlist ) Tj /F1 10 Tf (grows as the task runs and more values are yielded. Accessing the ) Tj /F3 10 Tf (.outlist) Tj T* 0 Tw 1.051654 Tw /F1 10 Tf (is nonblocking and can be done freely. Finally there is a ) Tj /F3 10 Tf (.result ) Tj /F1 10 Tf (property which waits for the task to) Tj T* 0 Tw (finish and returns the last yielded value or raises an exception.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 525.0236 cm +q +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL .014692 Tw (Here is some example code to visualize the output of the FakeImporter in Tkinter \(I chose Tkinter because) Tj T* 0 Tw (it is easy to use and it is in the standard library, but you can use any GUI\):) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 155.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 360 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 341.71 Tm /F3 10 Tf 12 TL (from Tkinter import *) Tj T* (from importer3 import FakeImporter) Tj T* T* (def taskwidget\(root, task, tick=500\):) Tj T* ( "A Label widget showing the output of a task every 500 ms") Tj T* ( sv = StringVar\(root\)) Tj T* ( lb = Label\(root, textvariable=sv\)) Tj T* ( def show_outlist\(\):) Tj T* ( try:) Tj T* ( out = task.outlist[-1]) Tj T* ( except IndexError: # no output yet) Tj T* ( out = '') Tj T* ( sv.set\('%s %s' % \(task, out\)\)) Tj T* ( root.after\(tick, show_outlist\)) Tj T* ( root.after\(0, show_outlist\)) Tj T* ( return lb) Tj T* T* (def monitor\(tasks\):) Tj T* ( root = Tk\(\)) Tj T* ( for task in tasks:) Tj T* ( task.run\(\)) Tj T* ( taskwidget\(root, task\).pack\(\)) Tj T* ( root.mainloop\(\)) Tj T* T* (if __name__ == '__main__':) Tj T* ( import plac) Tj T* ( with plac.Interpreter\(plac.call\(FakeImporter\)\) as i:) Tj T* ( tasks = [i.submit\('import_file f1'\), i.submit\('import_file f2'\)]) Tj T* ( monitor\(tasks\)) 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 235.3849 0 Td (38) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R375': class PDFStream +375 0 obj +% page stream +<< /Length 4659 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 747.0236 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Parallel computing with plac) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 633.0236 cm +q +BT 1 0 0 1 0 100.82 Tm 1.174751 Tw 12 TL /F1 10 Tf 0 0 .501961 rg (plac ) Tj 0 0 0 rg (is certainly not intended as a tool for parallel computing, but still you can use it to launch a set of) Tj T* 0 Tw .497984 Tw (commands and to collect the results, similarly to the MapReduce pattern popularized by Google. In order) Tj T* 0 Tw .669431 Tw (to give an example, I will consider the "Hello World" of parallel computing, i.e. the computation of pi with) Tj T* 0 Tw .537633 Tw (independent processes. There is a huge number of algorithms to compute pi; here I will describe a trivial) Tj T* 0 Tw .101567 Tw (one chosen for simplicity, not per efficienty. The trick is to consider the first quadrant of a circle with radius) Tj T* 0 Tw .602488 Tw (1 and to extract a number of points ) Tj /F3 10 Tf (\(x, y\) ) Tj /F1 10 Tf (with ) Tj /F3 10 Tf (x ) Tj /F1 10 Tf (and ) Tj /F3 10 Tf (y ) Tj /F1 10 Tf (random variables in the interval ) Tj /F3 10 Tf ([0,1]) Tj /F1 10 Tf (. The) Tj T* 0 Tw .928876 Tw (probability of extracting a number inside the quadrant \(i.e. with ) Tj /F3 10 Tf (x^2 + y^2 < 1) Tj /F1 10 Tf (\) is proportional to the) Tj T* 0 Tw .433145 Tw (area of the quadrant \(i.e. ) Tj /F3 10 Tf (pi/4) Tj /F1 10 Tf (\). The value of ) Tj /F3 10 Tf (pi ) Tj /F1 10 Tf (therefore can be extracted by multiplying by 4 the ratio) Tj T* 0 Tw (between the number of points in the quadrant versus the total number of points ) Tj /F3 10 Tf (N) Tj /F1 10 Tf (, for ) Tj /F3 10 Tf (N ) Tj /F1 10 Tf (large:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 527.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 96 re B* +Q +q +BT 1 0 0 1 0 77.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (def calc_pi\(N\):) Tj T* ( inside = 0) Tj T* ( for j in xrange\(N\):) Tj T* ( x, y = random\(\), random\(\)) Tj T* ( if x*x + y*y ) Tj (<) Tj ( 1:) Tj T* ( inside += 1) Tj T* ( return \(4.0 * inside\) / N) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 459.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 52.82 Tm /F1 10 Tf 12 TL .046654 Tw (The algorithm is trivially parallelizable: if you have n CPUs, you can compute pi n times with N/n iterations,) Tj T* 0 Tw 1.122488 Tw (sum the results and divide the total by n. I have a Macbook with two cores, therefore I would expect a) Tj T* 0 Tw 2.347984 Tw (speedup factor of 2 with respect to a sequential computation. Moreover, I would expect a threaded) Tj T* 0 Tw 2.827984 Tw (computation to be even slower than a sequential computation, due to the GIL and the scheduling) Tj T* 0 Tw (overhead.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 429.8236 cm +q +BT 1 0 0 1 0 16.82 Tm .313984 Tw 12 TL /F1 10 Tf 0 0 0 rg (Here is a script implementing the algorithm and working in three different modes \(parallel mode, threaded) Tj T* 0 Tw (mode and sequential mode\) depending on a ) Tj /F3 10 Tf (mode ) Tj /F1 10 Tf (option:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 96.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 324 re B* +Q +q +BT 1 0 0 1 0 305.71 Tm 12 TL /F3 10 Tf 0 0 0 rg (from __future__ import with_statement) Tj T* (from random import random) Tj T* (import multiprocessing) Tj T* (import plac) Tj T* T* (class PiCalculator\(object\):) Tj T* ( """Compute pi in parallel with threads or processes""") Tj T* ( ) Tj T* ( @plac.annotations\() Tj T* ( npoints=\('number of integration points', 'positional', None, int\),) Tj T* ( mode=\('sequential|parallel|threaded', 'option', 'm', str, 'SPT'\)\)) Tj T* ( def __init__\(self, npoints, mode='S'\):) Tj T* ( self.npoints = npoints) Tj T* ( if mode == 'P':) Tj T* ( self.mpcommands = ['calc_pi']) Tj T* ( elif mode == 'T':) Tj T* ( self.thcommands = ['calc_pi']) Tj T* ( elif mode == 'S':) Tj T* ( self.commands = ['calc_pi']) Tj T* ( self.n_cpu = multiprocessing.cpu_count\(\)) Tj T* ( ) Tj T* ( def submit_tasks\(self\):) Tj T* ( self.i = plac.Interpreter\(self\).__enter__\(\) ) Tj T* ( return [self.i.submit\('calc_pi %d' % \(self.npoints / self.n_cpu\)\)) Tj T* ( for _ in range\(self.n_cpu\)]) Tj T* 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 235.3849 0 Td (39) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R376': class PDFStream +376 0 obj +% page stream +<< /Length 3402 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 307.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 456 re B* +Q +q +BT 1 0 0 1 0 437.71 Tm 12 TL /F3 10 Tf 0 0 0 rg ( def close\(self\):) Tj T* ( self.i.close\(\)) Tj T* T* ( @plac.annotations\() Tj T* ( npoints=\('npoints', 'positional', None, int\)\)) Tj T* ( def calc_pi\(self, npoints\):) Tj T* ( counts = 0) Tj T* ( for j in xrange\(npoints\):) Tj T* ( n, r = divmod\(j, 1000000\)) Tj T* ( if r == 0:) Tj T* ( yield '%dM iterations' % n) Tj T* ( x, y = random\(\), random\(\)) Tj T* ( if x*x + y*y ) Tj (<) Tj ( 1:) Tj T* ( counts += 1) Tj T* ( yield \(4.0 * counts\)/npoints) Tj T* T* ( def run\(self\):) Tj T* ( tasks = self.i.tasks\(\)) Tj T* ( for t in tasks:) Tj T* ( t.run\(\)) Tj T* ( try:) Tj T* ( total = 0) Tj T* ( for task in tasks:) Tj T* ( total += task.result) Tj T* ( except: # the task was killed) Tj T* ( print tasks) Tj T* ( return) Tj T* ( return total / self.n_cpu) Tj T* T* (if __name__ == '__main__':) Tj T* ( pc = plac.call\(PiCalculator\)) Tj T* ( pc.submit_tasks\(\)) Tj T* ( try:) Tj T* ( import time; t0 = time.time\(\)) Tj T* ( print '%f in %f seconds ' % \(pc.run\(\), time.time\(\) - t0\)) Tj T* ( finally:) Tj T* ( pc.close\(\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 227.8236 cm +q +BT 1 0 0 1 0 64.82 Tm .381797 Tw 12 TL /F1 10 Tf 0 0 0 rg (Notice the ) Tj /F3 10 Tf (submit_tasks ) Tj /F1 10 Tf (method, which instantiates and initializes a ) Tj /F3 10 Tf (plac.Interpreter ) Tj /F1 10 Tf (object and) Tj T* 0 Tw 3.38152 Tw (submits a number of commands corresponding to the number of available CPUs. The ) Tj /F3 10 Tf (calc_pi) Tj T* 0 Tw 3.796651 Tw /F1 10 Tf (command yield a log message every million of interactions, just to monitor the progress of the) Tj T* 0 Tw 1.751318 Tw (computation. The ) Tj /F3 10 Tf (run ) Tj /F1 10 Tf (method starts all the submitted commands in parallel and sums the results. It) Tj T* 0 Tw 1.17104 Tw (returns the average value of ) Tj /F3 10 Tf (pi ) Tj /F1 10 Tf (after the slowest CPU has finished its job \(if the CPUs are equal and) Tj T* 0 Tw (equally busy they should finish more or less at the same time\).) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 209.8236 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL (Here are the results on my old Macbook with Ubuntu 10.04 and Python 2.6, for 10 million of iterations:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 116.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 84 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 65.71 Tm /F3 10 Tf 12 TL ($ python picalculator.py -mP 10000000 # two processes) Tj T* (3.141904 in 5.744545 seconds) Tj T* ($ python picalculator.py -mT 10000000 # two threads) Tj T* (3.141272 in 13.875645 seconds) Tj T* ($ python picalculator.py -mS 10000000 # sequential) Tj T* (3.141586 in 11.353841 seconds) 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 235.3849 0 Td (40) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R377': class PDFStream +377 0 obj +% page stream +<< /Length 5017 >> +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 +0 0 0 rg +BT 1 0 0 1 0 16.82 Tm /F1 10 Tf 12 TL 1.711751 Tw (As you see using processes one gets a 2x speedup indeed, where the threaded mode is some 20%) Tj T* 0 Tw (slower than the sequential mode.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 675.0236 cm +q +BT 1 0 0 1 0 52.82 Tm .167765 Tw 12 TL /F1 10 Tf 0 0 0 rg (Since the pattern submit a bunch of tasks, starts them and collect the results is so common, ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (provides) Tj T* 0 Tw .493828 Tw (an utility function ) Tj /F3 10 Tf (runp\(genseq, mode='p', start=True\) ) Tj /F1 10 Tf (to start a bunch a generators and return) Tj T* 0 Tw .587765 Tw (a list of task objects. By default ) Tj /F3 10 Tf (runp ) Tj /F1 10 Tf (use processes, but you can use threads by passing ) Tj /F3 10 Tf (mode='t') Tj /F1 10 Tf (. If) Tj T* 0 Tw .422765 Tw (you do not wont to start the tasks, you can say so \() Tj /F3 10 Tf (start=False) Tj /F1 10 Tf (\). With ) Tj /F3 10 Tf (runp ) Tj /F1 10 Tf (the parallel pi calculation) Tj T* 0 Tw (becomes a one-liner:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 642.4159 cm +q +q +.976496 0 0 .976496 0 0 cm +q +1 0 0 1 6.6 6.758862 cm +q +.662745 .662745 .662745 RG +.5 w +.960784 .960784 .862745 rg +n -6 -6 480 24 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 5.71 Tm /F3 10 Tf 12 TL (sum\(task.result for task in plac.runp\(calc_pi\(N\) for i in range\(ncpus\)\)\)/ncpus) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 612.4159 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (The plac server) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 498.4159 cm +q +BT 1 0 0 1 0 100.82 Tm 1.258443 Tw 12 TL /F1 10 Tf 0 0 0 rg (A command-line oriented interface can be easily converted into a socket-based interface. Starting from) Tj T* 0 Tw .930574 Tw (release 0.7 plac features a builtin server which is able to accept commands from multiple clients and to) Tj T* 0 Tw .994692 Tw (execute them. The server works by instantiating a separate interpreter for each client, so that if a client) Tj T* 0 Tw 1.08784 Tw (interpreter dies for any reason the other interpreters keep working. To avoid external dependencies the) Tj T* 0 Tw .872209 Tw (server is based on the ) Tj /F3 10 Tf (asynchat ) Tj /F1 10 Tf (module in the standard library, but it would not be difficult to replace) Tj T* 0 Tw 2.386412 Tw (the server with a different one \(for instance, a Twisted server\). Since ) Tj /F3 10 Tf (asynchat) Tj /F1 10 Tf (-based servers are) Tj T* 0 Tw .755984 Tw (asynchronous, any blocking command in the interpreter should be run in a separated process or thread.) Tj T* 0 Tw 1.157633 Tw (The default port for the ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (server is 2199, and the command to signal end-of-connection is EOF. For) Tj T* 0 Tw (instance, here is how you could manage remote import on a database \(say a SQLite db\):) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 369.2159 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 (import plac) Tj T* (from importer2 import FakeImporter) Tj T* T* (def main\(port=2199\):) Tj T* ( main = FakeImporter\('dsn'\)) Tj T* ( plac.Interpreter\(main\).start_server\(port\)) Tj T* ( ) Tj T* (if __name__ == '__main__':) Tj T* ( plac.call\(main\)) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 349.2159 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (You can connect to the server with ) Tj /F3 10 Tf (telnet ) Tj /F1 10 Tf (on port 2199, as follows:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 172.0159 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 ($ telnet localhost 2199) Tj T* (Trying ::1...) Tj T* (Trying 127.0.0.1...) Tj T* (Connected to localhost.) Tj T* (Escape character is '^]'.) Tj T* (i) Tj (>) Tj ( import_file f1) Tj T* (i) Tj (>) Tj ( .list) Tj T* (<) Tj (ThreadedTask 1 [import_file f1] RUNNING) Tj (>) Tj T* (i) Tj (>) Tj ( .out) Tj T* (Imported 100 lines) Tj T* (Imported 200 lines) Tj T* (i) Tj (>) Tj ( EOF) Tj T* (Connection closed by foreign host.) Tj T* ET +Q +Q +Q +Q +Q +q +1 0 0 1 62.69291 142.0159 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Summary) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 100.0159 cm +q +BT 1 0 0 1 0 28.82 Tm 2.203318 Tw 12 TL /F1 10 Tf 0 0 0 rg (Once ) Tj 0 0 .501961 rg (plac ) Tj 0 0 0 rg (claimed to be the easiest command-line arguments parser in the world. Having read this) Tj T* 0 Tw .673322 Tw (document you may think that it is not so easy after all. But it is a false impression. Actually the rules are) Tj T* 0 Tw (quite simple:) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 94.01593 cm +Q +q +1 0 0 1 62.69291 94.01593 cm +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 235.3849 0 Td (41) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R378': class PDFStream +378 0 obj +% page stream +<< /Length 6620 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 747.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (1.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (if you want to implement a command-line script, use ) Tj /F3 10 Tf (plac.call) Tj /F1 10 Tf (;) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 747.0236 cm +Q +q +1 0 0 1 62.69291 747.0236 cm +Q +q +1 0 0 1 62.69291 687.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 45 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (2.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 45 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (if you want to implement a command interpreter, use ) Tj /F3 10 Tf (plac.Interpreter) Tj /F1 10 Tf (:) Tj T* ET +Q +Q +q +1 0 0 1 23 39 cm +Q +q +1 0 0 1 23 39 cm +Q +q +1 0 0 1 23 21 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (for an interactive interpreter, call the ) Tj /F3 10 Tf (.interact ) Tj /F1 10 Tf (method;) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 23 21 cm +Q +q +1 0 0 1 23 21 cm +Q +q +1 0 0 1 23 3 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 3 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 10.5 0 Td (\177) Tj T* -10.5 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (for an batch interpreter, call the ) Tj /F3 10 Tf (.execute ) Tj /F1 10 Tf (method;) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 23 3 cm +Q +q +1 0 0 1 23 3 cm +Q +q +Q +Q +q +1 0 0 1 62.69291 687.0236 cm +Q +q +1 0 0 1 62.69291 687.0236 cm +Q +q +1 0 0 1 62.69291 657.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 15 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (3.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 16.82 Tm 5.126647 Tw 12 TL /F1 10 Tf 0 0 0 rg (for testing call the ) Tj /F3 10 Tf (Interpreter.check ) Tj /F1 10 Tf (method in the appropriate context or use the) Tj T* 0 Tw /F3 10 Tf (Interpreter.doctest ) Tj /F1 10 Tf (feature;) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 657.0236 cm +Q +q +1 0 0 1 62.69291 657.0236 cm +Q +q +1 0 0 1 62.69291 627.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 15 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (4.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 16.82 Tm 1.356457 Tw 12 TL /F1 10 Tf 0 0 0 rg (if you need to go at a lower level, you may need to call the ) Tj /F3 10 Tf (Interpreter.send ) Tj /F1 10 Tf (method which) Tj T* 0 Tw (returns a \(finished\) ) Tj /F3 10 Tf (Task ) Tj /F1 10 Tf (object.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 627.0236 cm +Q +q +1 0 0 1 62.69291 627.0236 cm +Q +q +1 0 0 1 62.69291 597.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 15 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (5.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 16.82 Tm 1.469269 Tw 12 TL /F1 10 Tf 0 0 0 rg (long running command can be executed in the background as threads or processes: just declare) Tj T* 0 Tw (them in the lists ) Tj /F3 10 Tf (thcommands ) Tj /F1 10 Tf (and ) Tj /F3 10 Tf (mpcommands ) Tj /F1 10 Tf (respectively.) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 597.0236 cm +Q +q +1 0 0 1 62.69291 597.0236 cm +Q +q +1 0 0 1 62.69291 567.0236 cm +0 0 0 rg +BT /F1 10 Tf 12 TL ET +q +1 0 0 1 6 15 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 5.66 0 Td (6.) Tj T* -5.66 0 Td ET +Q +Q +q +1 0 0 1 23 3 cm +q +BT 1 0 0 1 0 16.82 Tm 2.171647 Tw 12 TL /F1 10 Tf 0 0 0 rg (the ) Tj /F3 10 Tf (.start_server ) Tj /F1 10 Tf (method starts an asynchronous server on the given port number \(default) Tj T* 0 Tw (2199\)) Tj T* ET +Q +Q +q +Q +Q +q +1 0 0 1 62.69291 567.0236 cm +Q +q +1 0 0 1 62.69291 567.0236 cm +Q +q +1 0 0 1 62.69291 549.0236 cm +q +BT 1 0 0 1 0 4.82 Tm 12 TL /F1 10 Tf 0 0 0 rg (Moreover, remember that ) Tj /F3 10 Tf (plac_runner.py ) Tj /F1 10 Tf (is your friend.) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 520.6772 cm +n 0 14.17323 m 469.8898 14.17323 l S +Q +q +1 0 0 1 62.69291 490.6772 cm +q +BT 1 0 0 1 0 7.23 Tm 18 TL /F2 15 Tf 0 0 0 rg (Appendix: custom annotation objects) Tj T* ET +Q +Q +q +1 0 0 1 62.69291 460.6772 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 430.6772 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 301.4772 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 281.4772 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 104.2772 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 56.69291 56.69291 cm +q +0 0 0 rg +BT 1 0 0 1 0 4.82 Tm /F1 10 Tf 12 TL 235.3849 0 Td (42) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R379': class PDFStream +379 0 obj +% page stream +<< /Length 1367 >> +stream +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 62.69291 753.0236 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 623.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 120 re B* +Q +q +0 0 0 rg +BT 1 0 0 1 0 101.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* 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 579.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 235.3849 0 Td (43) Tj T* -235.3849 0 Td ET +Q +Q + +endstream + +endobj +% 'R380': class PDFPageLabels +380 0 obj +% Document Root +<< /Nums [ 0 + 381 0 R + 1 + 382 0 R + 2 + 383 0 R + 3 + 384 0 R + 4 + 385 0 R + 5 + 386 0 R + 6 + 387 0 R + 7 + 388 0 R + 8 + 389 0 R + 9 + 390 0 R + 10 + 391 0 R + 11 + 392 0 R + 12 + 393 0 R + 13 + 394 0 R + 14 + 395 0 R + 15 + 396 0 R + 16 + 397 0 R + 17 + 398 0 R + 18 + 399 0 R + 19 + 400 0 R + 20 + 401 0 R + 21 + 402 0 R + 22 + 403 0 R + 23 + 404 0 R + 24 + 405 0 R + 25 + 406 0 R + 26 + 407 0 R + 27 + 408 0 R + 28 + 409 0 R + 29 + 410 0 R + 30 + 411 0 R + 31 + 412 0 R + 32 + 413 0 R + 33 + 414 0 R + 34 + 415 0 R + 35 + 416 0 R + 36 + 417 0 R + 37 + 418 0 R + 38 + 419 0 R + 39 + 420 0 R + 40 + 421 0 R + 41 + 422 0 R + 42 + 423 0 R ] >> +endobj +% 'R381': class PDFPageLabel +381 0 obj +% None +<< /S /D + /St 1 >> +endobj +% 'R382': class PDFPageLabel +382 0 obj +% None +<< /S /D + /St 2 >> +endobj +% 'R383': class PDFPageLabel +383 0 obj +% None +<< /S /D + /St 3 >> +endobj +% 'R384': class PDFPageLabel +384 0 obj +% None +<< /S /D + /St 4 >> +endobj +% 'R385': class PDFPageLabel +385 0 obj +% None +<< /S /D + /St 5 >> +endobj +% 'R386': class PDFPageLabel +386 0 obj +% None +<< /S /D + /St 6 >> +endobj +% 'R387': class PDFPageLabel +387 0 obj +% None +<< /S /D + /St 7 >> +endobj +% 'R388': class PDFPageLabel +388 0 obj +% None +<< /S /D + /St 8 >> +endobj +% 'R389': class PDFPageLabel +389 0 obj +% None +<< /S /D + /St 9 >> +endobj +% 'R390': class PDFPageLabel +390 0 obj +% None +<< /S /D + /St 10 >> +endobj +% 'R391': class PDFPageLabel +391 0 obj +% None +<< /S /D + /St 11 >> +endobj +% 'R392': class PDFPageLabel +392 0 obj +% None +<< /S /D + /St 12 >> +endobj +% 'R393': class PDFPageLabel +393 0 obj +% None +<< /S /D + /St 13 >> +endobj +% 'R394': class PDFPageLabel +394 0 obj +% None +<< /S /D + /St 14 >> +endobj +% 'R395': class PDFPageLabel +395 0 obj +% None +<< /S /D + /St 15 >> +endobj +% 'R396': class PDFPageLabel +396 0 obj +% None +<< /S /D + /St 16 >> +endobj +% 'R397': class PDFPageLabel +397 0 obj +% None +<< /S /D + /St 17 >> +endobj +% 'R398': class PDFPageLabel +398 0 obj +% None +<< /S /D + /St 18 >> +endobj +% 'R399': class PDFPageLabel +399 0 obj +% None +<< /S /D + /St 19 >> +endobj +% 'R400': class PDFPageLabel +400 0 obj +% None +<< /S /D + /St 20 >> +endobj +% 'R401': class PDFPageLabel +401 0 obj +% None +<< /S /D + /St 21 >> +endobj +% 'R402': class PDFPageLabel +402 0 obj +% None +<< /S /D + /St 22 >> +endobj +% 'R403': class PDFPageLabel +403 0 obj +% None +<< /S /D + /St 23 >> +endobj +% 'R404': class PDFPageLabel +404 0 obj +% None +<< /S /D + /St 24 >> +endobj +% 'R405': class PDFPageLabel +405 0 obj +% None +<< /S /D + /St 25 >> +endobj +% 'R406': class PDFPageLabel +406 0 obj +% None +<< /S /D + /St 26 >> +endobj +% 'R407': class PDFPageLabel +407 0 obj +% None +<< /S /D + /St 27 >> +endobj +% 'R408': class PDFPageLabel +408 0 obj +% None +<< /S /D + /St 28 >> +endobj +% 'R409': class PDFPageLabel +409 0 obj +% None +<< /S /D + /St 29 >> +endobj +% 'R410': class PDFPageLabel +410 0 obj +% None +<< /S /D + /St 30 >> +endobj +% 'R411': class PDFPageLabel +411 0 obj +% None +<< /S /D + /St 31 >> +endobj +% 'R412': class PDFPageLabel +412 0 obj +% None +<< /S /D + /St 32 >> +endobj +% 'R413': class PDFPageLabel +413 0 obj +% None +<< /S /D + /St 33 >> +endobj +% 'R414': class PDFPageLabel +414 0 obj +% None +<< /S /D + /St 34 >> +endobj +% 'R415': class PDFPageLabel +415 0 obj +% None +<< /S /D + /St 35 >> +endobj +% 'R416': class PDFPageLabel +416 0 obj +% None +<< /S /D + /St 36 >> +endobj +% 'R417': class PDFPageLabel +417 0 obj +% None +<< /S /D + /St 37 >> +endobj +% 'R418': class PDFPageLabel +418 0 obj +% None +<< /S /D + /St 38 >> +endobj +% 'R419': class PDFPageLabel +419 0 obj +% None +<< /S /D + /St 39 >> +endobj +% 'R420': class PDFPageLabel +420 0 obj +% None +<< /S /D + /St 40 >> +endobj +% 'R421': class PDFPageLabel +421 0 obj +% None +<< /S /D + /St 41 >> +endobj +% 'R422': class PDFPageLabel +422 0 obj +% None +<< /S /D + /St 42 >> +endobj +% 'R423': class PDFPageLabel +423 0 obj +% None +<< /S /D + /St 43 >> +endobj +xref +0 424 +0000000000 65535 f +0000000113 00000 n +0000000246 00000 n +0000000411 00000 n +0000000598 00000 n +0000000850 00000 n +0000001100 00000 n +0000001358 00000 n +0000001517 00000 n +0000001849 00000 n +0000002089 00000 n +0000002330 00000 n +0000002572 00000 n +0000002814 00000 n +0000003056 00000 n +0000003299 00000 n +0000003543 00000 n +0000003787 00000 n +0000004031 00000 n +0000004275 00000 n +0000004519 00000 n +0000004763 00000 n +0000005007 00000 n +0000005251 00000 n +0000005495 00000 n +0000005739 00000 n +0000005983 00000 n +0000006227 00000 n +0000006471 00000 n +0000006715 00000 n +0000006959 00000 n +0000007203 00000 n +0000007447 00000 n +0000007691 00000 n +0000007935 00000 n +0000008179 00000 n +0000008423 00000 n +0000008667 00000 n +0000008911 00000 n +0000009155 00000 n +0000009399 00000 n +0000009643 00000 n +0000009887 00000 n +0000010131 00000 n +0000010375 00000 n +0000010619 00000 n +0000010863 00000 n +0000011107 00000 n +0000011351 00000 n +0000011595 00000 n +0000011839 00000 n +0000012083 00000 n +0000012327 00000 n +0000012571 00000 n +0000012815 00000 n +0000013059 00000 n +0000013303 00000 n +0000013547 00000 n +0000013791 00000 n +0000014035 00000 n +0000014279 00000 n +0000014523 00000 n +0000014767 00000 n +0000015011 00000 n +0000015255 00000 n +0000015499 00000 n +0000015743 00000 n +0000015987 00000 n +0000016231 00000 n +0000016475 00000 n +0000016719 00000 n +0000016963 00000 n +0000017207 00000 n +0000017451 00000 n +0000017695 00000 n +0000017939 00000 n +0000018183 00000 n +0000018427 00000 n +0000018671 00000 n +0000018899 00000 n +0000019837 00000 n +0000020099 00000 n +0000020362 00000 n +0000020612 00000 n +0000020861 00000 n +0000021127 00000 n +0000021379 00000 n +0000021629 00000 n +0000021881 00000 n +0000022131 00000 n +0000022383 00000 n +0000022633 00000 n +0000022885 00000 n +0000023136 00000 n +0000023375 00000 n +0000023555 00000 n +0000023990 00000 n +0000024251 00000 n +0000024515 00000 n +0000024764 00000 n +0000025016 00000 n +0000025269 00000 n +0000025521 00000 n +0000025772 00000 n +0000026025 00000 n +0000026263 00000 n +0000026659 00000 n +0000026909 00000 n +0000027162 00000 n +0000027400 00000 n +0000027741 00000 n +0000027995 00000 n +0000028249 00000 n +0000028521 00000 n +0000028862 00000 n +0000029116 00000 n +0000029406 00000 n +0000029696 00000 n +0000029950 00000 n +0000030188 00000 n +0000030533 00000 n +0000030832 00000 n +0000031086 00000 n +0000031324 00000 n +0000031655 00000 n +0000031909 00000 n +0000032163 00000 n +0000032415 00000 n +0000032667 00000 n +0000032919 00000 n +0000033173 00000 n +0000033410 00000 n +0000033776 00000 n +0000034075 00000 n +0000034329 00000 n +0000034577 00000 n +0000034841 00000 n +0000035182 00000 n +0000035436 00000 n +0000035674 00000 n +0000036005 00000 n +0000036244 00000 n +0000036565 00000 n +0000036817 00000 n +0000037056 00000 n +0000037387 00000 n +0000037640 00000 n +0000037892 00000 n +0000038144 00000 n +0000038396 00000 n +0000038650 00000 n +0000038902 00000 n +0000039154 00000 n +0000039408 00000 n +0000039662 00000 n +0000039914 00000 n +0000040168 00000 n +0000040420 00000 n +0000040658 00000 n +0000041099 00000 n +0000041353 00000 n +0000041636 00000 n +0000041890 00000 n +0000042144 00000 n +0000042426 00000 n +0000042709 00000 n +0000042963 00000 n +0000043215 00000 n +0000043533 00000 n +0000043787 00000 n +0000044076 00000 n +0000044328 00000 n +0000044580 00000 n +0000044819 00000 n +0000045270 00000 n +0000045524 00000 n +0000045778 00000 n +0000046037 00000 n +0000046294 00000 n +0000046546 00000 n +0000046800 00000 n +0000047058 00000 n +0000047310 00000 n +0000047568 00000 n +0000047822 00000 n +0000048075 00000 n +0000048336 00000 n +0000048590 00000 n +0000048844 00000 n +0000049098 00000 n +0000049352 00000 n +0000049604 00000 n +0000049856 00000 n +0000050109 00000 n +0000050363 00000 n +0000050617 00000 n +0000050871 00000 n +0000051125 00000 n +0000051408 00000 n +0000051661 00000 n +0000051951 00000 n +0000052205 00000 n +0000052516 00000 n +0000052768 00000 n +0000053020 00000 n +0000053309 00000 n +0000053561 00000 n +0000053798 00000 n +0000054439 00000 n +0000054697 00000 n +0000054951 00000 n +0000055205 00000 n +0000055459 00000 n +0000055713 00000 n +0000055967 00000 n +0000056221 00000 n +0000056466 00000 n +0000056720 00000 n +0000056974 00000 n +0000057228 00000 n +0000057482 00000 n +0000057741 00000 n +0000058167 00000 n +0000058466 00000 n +0000058705 00000 n +0000059026 00000 n +0000059280 00000 n +0000059519 00000 n +0000059850 00000 n +0000060104 00000 n +0000060366 00000 n +0000060620 00000 n +0000060883 00000 n +0000061136 00000 n +0000061399 00000 n +0000061653 00000 n +0000061907 00000 n +0000062160 00000 n +0000062398 00000 n +0000062809 00000 n +0000063083 00000 n +0000063337 00000 n +0000063591 00000 n +0000063830 00000 n +0000064181 00000 n +0000064435 00000 n +0000064689 00000 n +0000064935 00000 n +0000065261 00000 n +0000065545 00000 n +0000065844 00000 n +0000066098 00000 n +0000066356 00000 n +0000066610 00000 n +0000066863 00000 n +0000067214 00000 n +0000067468 00000 n +0000067722 00000 n +0000067983 00000 n +0000068244 00000 n +0000068496 00000 n +0000068749 00000 n +0000069001 00000 n +0000069255 00000 n +0000069509 00000 n +0000069763 00000 n +0000070017 00000 n +0000070257 00000 n +0000070673 00000 n +0000070972 00000 n +0000071224 00000 n +0000071463 00000 n +0000071779 00000 n +0000072078 00000 n +0000072316 00000 n +0000072637 00000 n +0000072891 00000 n +0000073164 00000 n +0000073403 00000 n +0000073744 00000 n +0000073998 00000 n +0000074237 00000 n +0000074553 00000 n +0000074852 00000 n +0000075106 00000 n +0000075364 00000 n +0000075695 00000 n +0000075934 00000 n +0000076255 00000 n +0000076494 00000 n +0000076800 00000 n +0000077099 00000 n +0000077353 00000 n +0000077607 00000 n +0000077846 00000 n +0000078187 00000 n +0000078426 00000 n +0000078732 00000 n +0000079017 00000 n +0000079181 00000 n +0000079395 00000 n +0000079524 00000 n +0000079778 00000 n +0000079975 00000 n +0000080189 00000 n +0000080403 00000 n +0000080629 00000 n +0000080831 00000 n +0000081040 00000 n +0000081237 00000 n +0000081440 00000 n +0000081641 00000 n +0000081859 00000 n +0000082060 00000 n +0000082274 00000 n +0000082469 00000 n +0000082667 00000 n +0000082903 00000 n +0000083083 00000 n +0000083307 00000 n +0000083517 00000 n +0000083716 00000 n +0000083918 00000 n +0000084126 00000 n +0000084331 00000 n +0000084531 00000 n +0000084730 00000 n +0000084940 00000 n +0000085153 00000 n +0000085359 00000 n +0000085561 00000 n +0000085784 00000 n +0000086011 00000 n +0000086224 00000 n +0000086424 00000 n +0000086616 00000 n +0000086801 00000 n +0000087329 00000 n +0000090428 00000 n +0000100125 00000 n +0000106213 00000 n +0000110764 00000 n +0000114917 00000 n +0000118921 00000 n +0000124043 00000 n +0000128464 00000 n +0000133483 00000 n +0000139758 00000 n +0000144116 00000 n +0000148517 00000 n +0000152182 00000 n +0000156582 00000 n +0000163280 00000 n +0000169647 00000 n +0000177312 00000 n +0000185617 00000 n +0000191577 00000 n +0000195240 00000 n +0000200911 00000 n +0000207099 00000 n +0000213658 00000 n +0000218191 00000 n +0000222635 00000 n +0000226971 00000 n +0000231792 00000 n +0000236967 00000 n +0000242072 00000 n +0000246418 00000 n +0000251865 00000 n +0000255862 00000 n +0000261612 00000 n +0000266754 00000 n +0000271276 00000 n +0000276312 00000 n +0000280706 00000 n +0000284989 00000 n +0000289751 00000 n +0000293256 00000 n +0000298376 00000 n +0000305099 00000 n +0000306573 00000 n +0000307290 00000 n +0000307369 00000 n +0000307448 00000 n +0000307527 00000 n +0000307606 00000 n +0000307685 00000 n +0000307764 00000 n +0000307843 00000 n +0000307922 00000 n +0000308001 00000 n +0000308081 00000 n +0000308161 00000 n +0000308241 00000 n +0000308321 00000 n +0000308401 00000 n +0000308481 00000 n +0000308561 00000 n +0000308641 00000 n +0000308721 00000 n +0000308801 00000 n +0000308881 00000 n +0000308961 00000 n +0000309041 00000 n +0000309121 00000 n +0000309201 00000 n +0000309281 00000 n +0000309361 00000 n +0000309441 00000 n +0000309521 00000 n +0000309601 00000 n +0000309681 00000 n +0000309761 00000 n +0000309841 00000 n +0000309921 00000 n +0000310001 00000 n +0000310081 00000 n +0000310161 00000 n +0000310241 00000 n +0000310321 00000 n +0000310401 00000 n +0000310481 00000 n +0000310561 00000 n +0000310641 00000 n +trailer +<< /ID + % ReportLab generated PDF document -- digest (http://www.reportlab.com) + [(\207i\\\(\331\(e\246\374\374\337\204\202\207\020\203) (\207i\\\(\331\(e\246\374\374\337\204\202\207\020\203)] + + /Info 299 0 R + /Root 298 0 R + /Size 424 >> +startxref +310690 +%%EOF diff --git a/doc/plac.txt b/doc/plac.txt new file mode 100644 index 0000000..e7083f8 --- /dev/null +++ b/doc/plac.txt @@ -0,0 +1,2 @@ +.. include:: plac_core.txt +.. include:: plac_adv.txt diff --git a/doc/plac_adv.txt b/doc/plac_adv.txt new file mode 100644 index 0000000..792f6ce --- /dev/null +++ b/doc/plac_adv.txt @@ -0,0 +1,1221 @@ +Advanced usages of plac +========================================================= + +Introduction +----------------------------------------------------- + +One of the design goals of plac_ is to make it dead easy to write a +scriptable and testable interface for an application. You can use +plac_ whenever you have an API with strings in input and strings in +output, and that includes a *huge* domain of applications. + +A string-oriented interface is a scriptable interface by +construction. That means that you can define a command language for +your application and that it is possible to write scripts which are +interpretable by plac_ and can be run as batch scripts. + +Actually, at the most general level, you can see plac_ as a generic tool to +write domain specific languages (DSL). With plac_ you +can test your application interactively as well as with batch +scripts, and even with the analogous of Python doctests for your +defined language. + +You can easily replace the ``cmd`` module of the standard library and +you could easily write an application like twill_ with plac_. Or you +could use it to script your building procedure. plac_ also supports +parallel execution of multiple commands and can be used as +task manager and monitor. It is also quite easy to build a GUI +or a Web application on top of plac_. When speaking of things +you can do with plac_, your imagination is the only limit! + +From scripts to interactive applications +------------------------------------------------------------ + +Command-line scripts have many advantages, but they are no substitute +for interactive applications. +In particular, if you have a script with a large startup time which must be run +multiple times, it is best to turn it into an interactive application, +so that the startup is performed only once. ``plac`` provides an +``Interpreter`` class just for this purpose. + +The ``Interpreter`` class wraps the main function of a script and +provides an ``.interact`` method to start an interactive interpreter +reading commands from the console. + +For instance, you can define an interactive interpreter on top of the +``ishelve`` script introduced in the `basic documentation`_ as +follows: + +.. include:: shelve_interpreter.py + :literal: + +A trick has been used here: the ishelve command-line interface has been +hidden inside an external interface. They are distinct: for instance +the external interface recognizes the ``-h/--help`` flag whereas the +internal interface only recognizes the ``.help`` command:: + + $ python shelve_interpreter.py -h + +.. include:: shelve_interpreter.help + :literal: + +Thanks to this ingenuous trick, the script can be run both interactively +and non-interactively:: + + $ python shelve_interpreter.py .clear # non-interactive use + cleared the shelve + +Here is an usage session:: + + $ python shelve_interpreter.py -i # interactive use + A simple interface to a shelve. Use .help to see the available commands. + i> .help + Commands: .help, .showall, .clear, .delete + ... + ... + i> a=1 + setting a=1 + i> a + 1 + i> b=2 + setting b=2 + i> a b + 1 + 2 + i> .del a + deleted a + i> a + a: not found + i> .show + b=2 + i> [CTRL-D] + +The ``.interact`` method +reads commands from the console and send them to the +underlying interpreter, until the user send a CTRL-D +command (CTRL-Z in Windows). There is a default +argument ``prompt='i> '`` which +can be used to change the prompt. The text displayed at the beginning +of the interactive session is the docstring of the main function. +``plac`` also understands command abbreviations: in this example +``del`` is an abbreviation for ``delete``. In case of ambiguous +abbreviations plac_ raises a ``NameError``. + +Finally I must notice that the ``plac.Interpreter`` is available only if you +are using a recent version of Python (>= 2.5), because it is a context +manager object which uses extended generators internally. + +Testing a plac application +----------------------------------------------------------- + +You can conveniently test your application in interactive mode. +However manual testing is a poor substitute for automatic testing. + +In principle, one could write automatic tests for the +``ishelve`` application by using ``plac.call`` directly: + +.. include:: test_ishelve.py + :literal: + +However, using ``plac.call`` is not especially nice. The big +issue is that ``plac.call`` responds to invalid input by printing an +error message on stderr and by raising a ``SystemExit``: this is +certainly not a nice thing to do in a test. + +As a consequence of this behavior it is impossible to test for invalid +commands, unless you wrap the ``SystemExit`` exception by +hand each time (a possibly you do something with the error message in +stderr too). Luckily, ``plac`` offers a better testing support through +the ``check`` method of ``Interpreter`` objects: + +.. include:: test_ishelve_more.py + :literal: + +The method ``.check(given_input, expected_output)`` works on strings +and raises an ``AssertionError`` if the output produced by the +interpreter is different from the expected output for the given input. +Notice that ``AssertionError`` is catched by tools like ``py.test`` and +``nosetests`` and actually ``plac`` tests are intended to be run with +such tools. + +Interpreters offer a minor syntactic advantage with respect to calling +``plac.call`` directly, but they offer a *major* semantic advantage when things +go wrong (read exceptions): an ``Interpreter`` object internally invokes +something like ``plac.call``, but it wraps all exceptions, so that ``i.check`` +is guaranteed not to raise any exception except ``AssertionError``. + +Even the ``SystemExit`` exception is captured and you can write your test as + + ``i.check('-cler', 'SystemExit: unrecognized arguments: -cler')`` + +without risk of exiting from the Python interpreter. + +There is a second advantage of interpreters: if the main function contains some +initialization code and finalization code +(``__enter__`` and ``__exit__`` functions) they will be run only +once at the beginning and at the end of the interpreter loop. +``plac.call`` instead ignores the initialization/finalization code. + +Plac easy tests +--------------------------------------------------------- + +Writing your tests in terms of ``Interpreter.check`` is certainly an +improvement over writing them in terms of ``plac.call``, but they +are still too low-level for my taste. The ``Interpreter`` class provides +support for doctest-style tests, a.k.a. *plac easy tests*. + +By using plac easy tests you can cut and paste your interactive session and +turn it into a runnable automatics test. +Consider for instance the following file ``ishelve.placet`` (the ``.placet`` +extension is a mnemonic for plac easy tests): + +.. include:: ishelve.placet + :literal: + +Notice the precence of the shebang line containing the name of the +plac_ tool to test (a plac_ tool is just a Python module with a +function called ``main``). The shebang is ignored by the interpreter +(it looks like a comment to it) but it is there so that external +tools (say a test runner) can infer the plac interpreter +to use to test the file. + +You can test ``ishelve.placet`` file by calling the +``.doctest`` method of the interpreter, as in this example:: + + $ python -c"import plac, ishelve + plac.Interpreter(ishelve.main).doctest(open('ishelve.placet'), verbose=True)" + +Internally ``Interpreter.doctests`` invokes something like ``Interpreter.check`` +multiple times inside the same context and compare the output with the +expected output: if even a check fails, the whole test fail. + +You should realize tha the easy tests supported by ``plac`` are *not* +unittests: they are functional tests. They model then user interaction and the +order of the operations generally matters. The single subtests in a +``.placet`` file are not independent and it makes sense to exit +immediately at the first failure. + +The support for doctests in plac_ comes nearly for free, thanks to the +shlex_ module in the standard library, which is able to parse simple +languages as the ones you can implement with plac_. In particular, +thanks to shlex_, plac_ is able to recognize comments (the default +comment character is ``#``), escape sequences and more. Look at the +shlex_ documentation if you need to customize how the language is +interpreted. For more flexibility, it is even possible to pass to the +interpreter a custom split function with signature ``split(line, +commentchar)``. + +In addition, I have implemented from scratch some support for line number +recognition, so that if a test fail you get the line number of the +failing command. This is especially useful if your tests are +stored in external files, even if plac easy tests does not need to be in +a file: you can just pass to the ``.doctest`` method a list of +strings corresponding to the lines of the file. + +At the present plac_ does not use any code from the doctest +module, but the situation may change in the future (it would be nice +if plac_ could reuse doctests directives like ELLIPSIS). + +It is straighforward to integrate your ``.placet`` tests with standard +testing tools. For instance, you can integrate your doctests with ``nose`` +or ``py.test`` as follow:: + + import os, shlex, plac + + def test_doct(): + """ + Find all the doctests in the current directory and run them with the + corresponding plac interpreter (the shebang rules!) + """ + placets = [f for f in os.listdir('.') if f.endswith('.placet')] + for placet in placets: + lines = list(open(placet)) + assert lines[0].startswith('#!'), 'Missing or incorrect shebang line!' + firstline = lines[0][2:] # strip the shebang + main = plac.import_main(*shlex.split(firstline)) + yield plac.Interpreter(main).doctest, lines[1:] + +Here you should notice that usage of ``plac.import_main``, an utility +which is able to import the main function of the script specified in +the shebang line. You can use both the full path name of the +tool, or a relative path name. In this case the runner look at the +environment variable ``PLACPATH`` and it searches +the plac tool in the directories specified there (``PLACPATH`` is just +a string containing directory names separated by colons). If the variable +``PLACPATH`` is not defined, it just looks in the current directory. +If the plac tool is not found, an ``ImportError`` is raised. + +Plac batch scripts +-------------------------------------------------- + +It is pretty easy to realize that an interactive interpreter can +also be used to run batch scripts: instead of reading the commands from +the console, it is enough to read the commands from a file. +plac_ interpreters provide an ``.execute`` method to perform just that. + +There is just a subtle point to notice: whereas in an interactive loop +one wants to manage all exceptions, a batch script should not in the +background in case of unexpected errors. The implementation of +``Interpreter.execute`` makes sure that any error raised by +``plac.call`` internally is re-raised. In other words, plac_ +interpreters *wrap the errors, but does not eat them*: the errors are +always accessible and can be re-raised on demand. + +The exception is the case of invalid commands, which are skipped. +Consider for instance the following batch file, which contains a +mispelled command (``.dl`` instead of ``.del``): + +.. include:: ishelve.plac + :literal: + +If you execute the batch file, the interpreter will print a ``.dl: not found`` +at the ``.dl`` line and will continue:: + + $ python -c "import plac, ishelve + plac.Interpreter(ishelve.main).execute(open('ishelve.plac'), verbose=True)" + i> .clear + cleared the shelve + i> a=1 b=2 + setting a=1 + setting b=2 + i> .show + b=2 + a=1 + i> .del a + deleted a + i> .dl b + 2 + .dl: not found + i> .show + b=2 + +The ``verbose`` flag is there to show the lines which are being interpreted +(prefixed by ``i>``). This is done on purpose, so that you can cut and paste +the output of the batch script and turn it into a ``.placet`` test +(cool, isn't it?). + +Implementing subcommands +---------------------------------------- + +When I discussed the ``ishelve`` implementation in the `basic +documentation`_, I said that it looked like a poor man implementation +of an object system as a chain of elifs; I also said that plac_ was +able to do much better than that. Here I will substantiate my claim. + +plac_ is actually able to infer a set of subparsers from a +generic container of commands. This is useful if you want to +implement *subcommands* (a familiar example of a command-line +application featuring subcommands is subversion). + +Technically a container of commands is any object with a ``.commands`` attribute +listing a set of functions or methods which are valid commands. A command +container may have initialization/finalization hooks (``__enter__/__exit__``) +and dispatch hooks (``__missing__``, invoked for invalid command names). +Moreover, only when using command containers plac_ is able to provide +automatic autocompletion of commands. + +The shelve interface can be rewritten in an object-oriented way as follows: + +.. include:: ishelve2.py + :literal: + +``plac.Interpreter`` objects wrap context manager objects +consistently. In other words, if you wrap an object with +``__enter__`` and ``__exit__`` methods, they are invoked in the right +order (``__enter__`` before the interpreter loop starts and +``__exit__`` after the interpreter loop ends, both in the regular and +in the exceptional case). In our example, the methods ``__enter__`` +and ``__exit__`` make sure the the shelve is opened and closed +correctly even in the case of exceptions. Notice that I have not +implemented any error checking in the ``show`` and ``delete`` methods +on purpose, to verify that plac_ works correctly in the presence of +exceptions. + +When working with command containers, plac_ automatically adds two +special commands to the set of provided commands: ``.help`` +and ``.last_tb``. The ``.help`` command is the easier to understand: +when invoked without arguments it displays the list of available commands +with the same formatting of the cmd_ module; when invoked with the name of +a command it displays the usage message for that command. +The ``.last_tb`` command is useful when debugging: in case of errors, +it allows you to display the traceback of the last executed command. + +Here is a session of usage on an Unix-like operating system:: + + $ python ishelve2.py + A minimal interface over a shelve object. + Operating on /home/micheles/conf.shelve. + .help to see the available commands. + i> .help + + special commands + ================ + .help .last_tb + + custom commands + =============== + delete set show showall + + i> delete + deleting everything + i> set a pippo + setting a=pippo + i> set b lippo + setting b=lippo + i> showall + b = lippo + a = pippo + i> show a b + a = pippo + b = lippo + i> del a + deleting a + i> showall + b = lippo + i> delete a + deleting a + KeyError: 'a' + i> .last_tb + File "/usr/local/lib/python2.6/dist-packages/plac-0.6.0-py2.6.egg/plac_ext.py", line 190, in _wrap + for value in genobj: + File "./ishelve2.py", line 37, in delete + del self.sh[name] # no error checking + File "/usr/lib/python2.6/shelve.py", line 136, in __delitem__ + del self.dict[key] + i> + +Notice that in interactive mode the traceback is hidden, unless +you pass the ``verbose`` flag to the ``Interpreter.interact`` method. + +plac.Interpreter.call +-------------------------------------------- + +At the core of ``plac`` there is the ``call`` function which invokes +a callable with the list of the arguments passed at the command-line +(``sys.argv[1:]``). Thanks to ``plac.call`` you can launch your module +by simply adding the lines:: + + if __name__ == '__main__': + plac.call(main) + +Everything works fine if ``main`` is a simple callable performing some +action; however, in many cases, one has a ``main`` "function" which +is a actually a factory returning a command container object. For +instance, in my second shelve example the main function is the class +``ShelveInterface``, and the two lines needed to run the module are +a bit ugly:: + + if __name__ == '__main__': + plac.Interpreter(plac.call(ShelveInterface)).interact() + +Moreover, now the program runs, but only in interactive mode, i.e. +it is not possible to run it as a script. It would be nice instead +to be able to specify the command to execute on the command-line +and have the interpreter start, execute the command and finish +properly (I mean by calling ``__enter__`` and ``__exit__``) +without needing user input. The the script could be called from +a batch shell script working in the background. +In order to provide such functionality ``plac.Interpreter`` provides +a classmethod named ``.call`` which takes the factory, instantiates +it with the arguments read from the command line, wraps the resulting +container object as an interpreter and runs it with the rest arguments +found in the command line. Here is the code to turn the ``ShelveInterface`` +into a script + +.. include:: ishelve3.py + :literal: + +and here are a few examples of usage:: + + $ python ishelve3.py -h + usage: ishelve3.py [-h] [-i] [-configfile CONFIGFILE] [args [args ...]] + + positional arguments: + args + + optional arguments: + -h, --help show this help message and exit + -i, --interact start interactive interpreter + -configfile CONFIGFILE + path name of the shelve + + $ python ishelve3.py set a 1 + setting a=1 + $ python ishelve3.py show a + a = 1 + +If you pass the ``-i`` flag in the command line, then the +script will enter in interactive mode and ask the user +for the commands to execute:: + + $ python ishelve3.py -i + A minimal interface over a shelve object. + Operating on /home/micheles/conf.shelve. + .help to see the available commands. + + i> + +In a sense, I have closed the circle: at the beginning of this +document I discussed how to turn a script into an interactive +application (the ``shelve_interpreter.py`` example), whereas here I +have show how to turn an interactive application into a script. + +The complete signature of ``plac.Interpreter.call`` is the following:: + + call(factory, arglist=sys.argv[1:], + commentchar='#', split=shlex.split, + stdin=sys.stdin, prompt='i> ', verbose=False) + +The factory must have a fixed number of positional arguments (no +default arguments, no varargs, no kwargs), otherwise a ``TypeError`` +is raised: the reason is that we want to be able to distinguish the +command-line arguments needed to instantiate the factory from the rest +arguments that must be sent to the corresponding interpreter object. +It is also possible to specify a list of arguments different from +``sys.argv[1:]`` (useful in tests), the character to be recognized as +a comment, the splitting function, the input source and the prompt to +use while in interactive mode, and a verbose flag. + +Readline support +--------------------------------------- + +Starting from release 0.6 plac_ offers full readline support. That +means that if your Python was compiled with readline support you get +autocompletion and persistent command history for free. By default +all commands are autocomplete in a case sensitive way. If you want to +add new words to the autocompletion set, or you want to change the +location of the ``.history`` file, or to change the case sensitivity, +the way to go is to pass a ``plac.ReadlineInput`` object to the +interpreter. Here is an example, assuming you want to build a +database interface understanding SQL commands: + +.. include:: sql_interface.py + :literal: + +Here is an example of usage:: + + $ python sql_interface.py + sql> SELECT a.* FROM TABLE1 AS a INNER JOIN TABLE2 AS b ON a.id = b.id + ... + +You can check that entering just ``sel`` and pressing TAB the readline library +completes the ``SELECT`` keyword for you and makes it upper case; idem for +``FROM``, ``INNER``, ``JOIN`` and even for the names of the tables. An +obvious improvement is to read the names of the tables by introspecting +the database: actually you can even read the names of the views and of +the columns, and have full autocompletion. All the entered commands +and recorded and saved in the file ``~/.sql_interface.history`` when +exiting from the command-line interface. + +If the readline library is not available, my suggestion is to use the +rlwrap_ tool which provides similar features, at least on Unix-like +platforms. plac_ should also work fine on Windows with the pyreadline_ +library (I do not use Windows, so this part is very little tested: I +tried it only once and it worked, but your mileage may vary). +For people worried about licenses, I will notice that plac_ uses the +readline library only if available, it does not include it and it does +not rely on it in any fundamental way, so that the plac_ licence does +not need to be the GPL (actually it is a BSD +do-whatever-you-want-with-it licence). + +The interactive mode of ``plac`` can be used as a replacement of the +cmd_ module in the standard library. It is actually better than cmd_: +for instance, the ``.help`` command is more powerful, since it +provides information about the arguments accepted by the given command:: + + i> .help set + usage: set name value + + set name value + + positional arguments: + name + value + + i> .help delete + usage: delete [name] + + delete given parameter (or everything) + + positional arguments: + name + + i> .help show + usage: show [names [names ...]] + + show given parameters + + positional arguments: + names + +As you can imagine, the help message is provided by the underlying argparse_ +subparser (there is a subparser for each command). plac_ commands accept +options, flags, varargs, keyword arguments, arguments with defaults, +arguments with a fixed number of choices, type conversion and all the +features provided of argparse_ which should be reimplemented from scratch +using plac_. + +Moreover at the moment ``plac`` also understands command abbreviations. +However, this feature may disappear in +future releases. It was meaningful in the past, when plac_ did not support +readline. + +Notice that if an abbreviation is ambiguous, plac_ warns you:: + + i> sh + NameError: Ambiguous command 'sh': matching ['showall', 'show'] + +The plac runner +-------------------------------------------------------- + +The distribution of plac_ includes a runner script named ``plac_runner.py``, +which will be installed in a suitable directory in your system by distutils_ +(say in ``\usr\local\bin\plac_runner.py`` in a Unix-like operative system). +The runner provides many facilities to run ``.plac`` scripts and +``.placet`` files, as well as Python modules containg a ``main`` +object, which can be a function, a command container object or +even a command container class. + +For instance, suppose you want to execute a script containing commands +defined in the ``ishelve2`` module like the following one: + +.. include:: ishelve2.plac + :literal: + +The first line of the ``.plac`` script contains the name of the +python module containing the plac interpreter and the arguments +which must be passed to its main function in order to be able +to instantiate an interpreter object. In this case I appended +``:ShelveInterface`` to the name of the module to specify the +object that must be imported: if not specified, by default the +object named 'main' is imported. +The other lines contains commands. +You can run the script as follows:: + + $ plac_runner.py --batch ishelve2.plac + setting a=1 + deleting a + Traceback (most recent call last): + ... + _bsddb.DBNotFoundError: (-30988, 'DB_NOTFOUND: No matching key/data pair found') + +The last command intentionally contained an error, to show that the +plac runner does not eat the traceback. + +The runner can also be used to run Python modules in interactive +mode and non-interactive mode. If you put this alias in your bashrc + + ``alias plac="plac_runner.py"`` + +(or you define a suitable ``plac.bat`` script in Windows) you can +run the ``ishelve2.py`` script in interactive mode as +follows:: + + $ plac -i ishelve2.py:ShelveInterface + A minimal interface over a shelve object. + Operating on /home/micheles/conf.shelve. + .help to see the available commands. + + i> del + deleting everything + i> set a 1 + setting a=1 + i> set b 2 + setting b=2 + i> show b + b = 2 + +Now you can cut and paste the interactive session an turns into into +a ``.placet`` file like the following: + +.. include:: ishelve2.placet + :literal: + +Notice that the first line specifies a test database +``~/test.shelve``, to avoid clobbering your default shelve. If you +mispell the arguments in the first line plac will give you an +argparse_ error message (just try). + +You can run placets following the shebang convention directly with +the plac runner:: + + $ plac --test ishelve2.placet + run 1 plac test(s) + +If you want to see the output of the tests, pass the ``-v/--verbose`` flag. +Notice that he runner ignore the extension, so you can actually use any +extension your like, but *it relies on the first line of the file to invoke +the corresponding plac tool with the given arguments*. + +The plac runner does not provide any test discovery facility, +but you can use standard Unix tools to help. For instance, you can +run all the ``.placet`` files into a directory and its subdirectories +as follows:: + + $ find . -name \*.placet | xargs plac_runner.py -t + +The plac runner expects the main function of your script to +return a plac tool, i.e. a function or an object with a ``.commands`` +attribute. It this is not the case the runner gracefully exits. + +It also works in non-interactive mode, if you call it as + + ``$ plac module.py args ...`` + +Here is an example:: + + $ plac ishelve.py a=1 + setting a=1 + $ plac ishelve.py .show + a=1 + +Notice that in non-interactive mode the runner just invokes ``plac.call`` +on the ``main`` object of the Python module. + +A non class-based example +-------------------------------------------------------- + +plac_ does not force you to use classes to define command containers. +Even a simple function can be a valid command container, it is +enough to add to it a ``.commands`` attribute and possibly +``__enter__`` and/or ``__exit__`` attributes. + +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: + +Notice that I have defined both an ``__exit__`` hook and a ``__missing__`` +hook, invoked for non-existing commands. +The real trick here is the line ``main = __import__(__name__)``, which +define ``main`` to be an alias for the current module. + +The ``vcs`` module does not contain an ``if __name__ == '__main__'`` +block, but you can still run it through the plac runner +(try ``plac vcs.py -h``): + +.. include:: vcs.help + :literal: + +You can get help for the subcommands by postponing ``-h`` after the +name of the command:: + + $ plac vcs.py status -h + usage: vcs.py status [-h] [-q] + + A fake status command + + optional arguments: + -h, --help show this help message and exit + -q, --quiet summary information + +Notice how the docstring of the command is automatically shown in +usage message, as well as the documentation for the sub flag ``-q``. + +Here is an example of a non-interactive session:: + + $ plac vcs.py check url + checkout + url + $ plac vcs.py st -q + status + True + $ plac vcs.py co + commit + None + +and here is an interactive session:: + + $ plac -i vcs.py + usage: plac_runner.py vcs.py [-h] {status,commit,checkout} ... + i> check url + checkout + url + i> st -q + status + True + i> co + commit + None + i> sto + Command 'sto' does not exist + i> [CTRL-D] + ok + +Notice the invocation of the ``__missing__`` hook for non-existing commands. +Notice also that the ``__exit__`` hook gets called only in interactive +mode. + +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. + +Writing your own plac runner +---------------------------------------------------------- + +The runner included in the plac_ distribution is intentionally kept +small (around 50 lines of code) so that you can study it and write +your own runner if want to. If you need to go to such level +of detail, you should know that the most important method of +the ``Interpreter`` class is the ``.send`` method, which takes +strings in input and returns a four-tuple with attributes +``.str``, ``.etype``, ``.exc`` and ``.tb``: + +- ``.str`` is the output of the command, if successful (a string); +- ``.etype`` is the class of the exception, if the command fail; +- ``.exc`` is the exception instance; +- ``.tb`` is the traceback. + +Moreover the ``__str__`` representation of the output object is redefined +to return the output string if the command was successful or the error +message if the command failed (actually it returns the error message +preceded by the name of the exception class). + +For instance, if you send a mispelled option to +the interpreter a ``SystemExit`` will be trapped: + +>>> import plac +>>> from ishelve import ishelve +>>> with plac.Interpreter(ishelve) as i: +... print(i.send('.cler')) +... +SystemExit: unrecognized arguments: .cler + +It is important to invoke the ``.send`` method inside the context manager, +otherwise you will get a ``RuntimeError``. + +For instance, suppose you want to implement a graphical runner for a +plac-based interpreter with two text widgets: one to enter the commands +and one to display the results. Suppose you want to display the errors +with tracebacks in red. You will need to code something like that +(pseudocode follows):: + + input_widget = WidgetReadingInput() + output_widget = WidgetDisplayingOutput() + + def send(interpreter, line): + out = interpreter.send(line) + if out.tb: # there was an error + output_widget.display(out.tb, color='red') + else: + output_widget.display(out.str) + + main = plac.import_main(tool_path) # get the main object + + with plac.Interpreter(main) as i: + def callback(event): + if event.user_pressed_ENTER(): + send(i, input_widget.last_line) + input_widget.addcallback(callback) + gui_mainloop.start() + +You can adapt the pseudocode to your GUI toolkit of choice and you can +also change the file associations in such a way that clicking on a +plac tool file the graphical user interface starts. + +An example of GUI program built on top of plac_ is given later on, in the +paragraph *Managing the output of concurrent commands* (using Tkinter +for simplicity and portability). + +There is a final *caveat*: since the plac interpreter loop is +implemented via extended generators, plac interpreters are single threaded: you +will get an error if you ``.send`` commands from separated threads. +You can circumvent the problem by using a queue. If EXIT is a sentinel +value to signal exiting from the interpreter look, you can write code +like this:: + + with interpreter: + for input_value in iter(input_queue.get, EXIT): + output_queue.put(interpreter.send(input_value)) + +The same trick also work for processes; you could run the interpreter +loop in a separate process and send commands to it via the Queue +class provided by the multiprocessing_ module. + +Long running commands +--------------------------------------- + +As we saw, by default a plac_ interpreter blocks until +the command terminates. This is an issue, in the sense that it makes +the interactive experience quite painful for long running commands. An +example is better than a thousand words, so consider the following +fake importer: + +.. include:: importer1.py + :literal: + +If you run the ``import_file`` command, you will have to wait for 200 seconds +before entering a new command:: + + $ python importer1.py dsn + A fake importer with an import_file command + i> import_file file1 + ... + Imported 100 lines + Imported 200 lines + Imported 300 lines + ... + Imported 10000 lines + closing the file + +Being unable to enter any other command is quite annoying: in such situation one +would like to run the long running commands in the background, to keep +the interface responsive. plac_ provides two ways to reach this goal: threads +and processes. + +Threaded commands +----------------------------------------- + +The most familiar way to execute a task in the background (even if not +necessarily the best way) is to run it into a separated thread. In our +example it is sufficient to replace the line + + ``commands = ['import_file']`` + +with + + ``thcommands = ['import_file']`` + +to tell to the plac_ interpreter that the command ``import_file`` should be +run into a separated thread. Here is an example session:: + + i> import_file file1 + + +The import task started in a separated thread. You can see the +progress of the task by using the special command ``.output``:: + + i> .output 1 + + Imported 100 lines + Imported 200 lines + +If you look after a while, you will get more lines of output:: + + i> .output 1 + + Imported 100 lines + Imported 200 lines + Imported 300 lines + Imported 400 lines + +If you look after a time long enough, the task will be finished:: + + i> .output 1 + + +You can even skip the number argument: then ``.output`` will the return +the output of the last launched command (the special commands like .output +do not count). + +You can launch many tasks one after the other:: + + i> import_file file2 + + i> import_file file3 + + +The ``.list`` command displays all the running tasks:: + + i> .list + + + +It is even possible to kill a task:: + + i> .kill 5 + + # wait a bit ... + closing the file + i> .output 5 + + +You should notice that since at the Python level it is impossible to kill +a thread, the ``.kill`` commands works by setting the status of the task to +``TOBEKILLED``. Internally the generator corresponding to the command +is executed in the thread and the status is checked at each iteration: +when the status become ``TOBEKILLED`` a ``GeneratorExit`` exception is +raised and the thread terminates (softly, so that the ``finally`` clause +is honored). In our example the generator is yielding +back control once every 100 iterations, i.e. every two seconds (not much). +In order to get a responsive interface it is a good idea to yield more +often, for instance every 10 iterations (i.e. 5 times per second), +as in the following code: + +.. include:: importer2.py + :literal: + +Running commands as external processes +----------------------------------------- + +Threads are not loved much in the Python world and actually most people +prefer to use processes instead. For this reason plac_ provides the +option to execute long running commands as external processes. Unfortunately +the current implementation only works in Unix-like operating systems +(including Mac OS X) because it relies on fork via the multiprocessing_ +module. + +In our example, to enable the feature it is sufficient to replace the line + + ``thcommands = ['import_file']`` + +with + + ``mpcommands = ['import_file']``. + +The user experience is exactly the same as with threads and you will not see any +difference at the user interface level:: + + i> import_file file3 + + i> .kill 1 + + closing the file + i> .o 1 + + Imported 100 lines + Imported 200 lines + i> + +Still, using processes is quite different than using threads: in +particular, when using processes you can only yield pickleable values +and you cannot re-raise an exception first raised in a different +process, because traceback objects are not pickleable. Moreover, +you cannot rely on automatic sharing of your objects. + +On the plus side, when using processes you do not need to worry about +killing a command: they are killed immediately using a SIGTERM signal, +and there is not a ``TOBEKILLED`` mechanism. Moreover, the killing is +guaranteed to be soft: internally a command receiving a SIGTERM raises +a ``TerminatedProcess`` exception which is trapped in the generator +loop, so that the command is closed properly. + +Using processes allows to take full advantage of multicore machines +and it is safer than using threads, so it is the recommended approach +unless you are working on Windows. + +Managing the output of concurrent commands +--------------------------------------------- + +plac_ acts as a command-line task launcher and can be used as the base +to build a GUI-based task launcher and task monitor. To this aim the +interpreter class provides a ``.submit`` method which returns a task +object and a ``.tasks`` method returning the list of all the tasks +submitted to the interpreter. The ``submit`` method does not start the task +and thus it is nonblocking. +Each task has an ``.outlist`` attribute which is a list +storing the value yielded by the generator underlying the task (the +``None`` values are skipped though): the ``.outlist`` grows as the +task runs and more values are yielded. Accessing the ``.outlist`` is +nonblocking and can be done freely. +Finally there is a ``.result`` +property which waits for the task to finish and returns the last yielded +value or raises an exception. + +Here is some example code to visualize the output of the FakeImporter +in Tkinter (I chose Tkinter because it is easy to use and it is +in the standard library, but you can use any GUI): + +.. include:: importer_ui.py + :literal: + +Parallel computing with plac +--------------------------------------------- + +plac_ is certainly not intended as a tool for parallel computing, but +still you can use it to launch a set of commands and to collect the +results, similarly to the MapReduce pattern popularized by +Google. In order to give an example, I will consider the "Hello +World" of parallel computing, i.e. the computation of pi with +independent processes. There is a huge number of algorithms to +compute pi; here I will describe a trivial one chosen for simplicity, +not per efficienty. The trick is to consider the first quadrant of a +circle with radius 1 and to extract a number of points ``(x, y)`` with +``x`` and ``y`` random variables in the interval ``[0,1]``. The +probability of extracting a number inside the quadrant (i.e. with +``x^2 + y^2 < 1``) is proportional to the area of the quadrant +(i.e. ``pi/4``). The value of ``pi`` therefore can be extracted by +multiplying by 4 the ratio between the number of points in the +quadrant versus the total number of points ``N``, for ``N`` large:: + + def calc_pi(N): + inside = 0 + for j in xrange(N): + x, y = random(), random() + if x*x + y*y < 1: + inside += 1 + return (4.0 * inside) / N + +The algorithm is trivially parallelizable: if you have n CPUs, you can +compute pi n times with N/n iterations, sum the results and divide the total +by n. I have a Macbook with two cores, therefore I would expect a speedup +factor of 2 with respect to a sequential computation. Moreover, I would +expect a threaded computation to be even slower than a sequential +computation, due to the GIL and the scheduling overhead. + +Here is a script implementing the algorithm and working in three different +modes (parallel mode, threaded mode and sequential mode) depending on a +``mode`` option: + +.. include:: picalculator.py + :literal: + +Notice the ``submit_tasks`` method, which instantiates and initializes a +``plac.Interpreter`` object and submits a number of commands corresponding +to the number of available CPUs. The ``calc_pi`` command yield a log +message every million of interactions, just to monitor the progress of +the computation. The ``run`` method starts all the submitted commands +in parallel and sums the results. It returns the average value of ``pi`` +after the slowest CPU has finished its job (if the CPUs are equal and +equally busy they should finish more or less at the same time). + +Here are the results on my old Macbook with Ubuntu 10.04 and Python 2.6, +for 10 million of iterations:: + + $ python picalculator.py -mP 10000000 # two processes + 3.141904 in 5.744545 seconds + $ python picalculator.py -mT 10000000 # two threads + 3.141272 in 13.875645 seconds + $ python picalculator.py -mS 10000000 # sequential + 3.141586 in 11.353841 seconds + +As you see using processes one gets a 2x speedup indeed, where the threaded +mode is some 20% slower than the sequential mode. + +Since the pattern submit a bunch of tasks, starts them and collect the +results is so common, plac_ provides an utility function +``runp(genseq, mode='p', start=True)`` to start +a bunch a generators and return a list of task objects. By default +``runp`` use processes, but you can use threads by passing ``mode='t'``. +If you do not wont to start the tasks, you can say so (``start=False``). +With ``runp`` the parallel pi calculation becomes a one-liner:: + + sum(task.result for task in plac.runp(calc_pi(N) for i in range(ncpus)))/ncpus + +The plac server +------------------------------------------------------- + +A command-line oriented interface can be easily converted into a +socket-based interface. Starting from release 0.7 plac features +a builtin server which is able to accept commands from multiple +clients and to execute them. The server works by instantiating +a separate interpreter for each client, so that if a client interpreter +dies for any reason the other interpreters keep working. +To avoid external dependencies the server is based on the ``asynchat`` +module in the standard library, but it would not be difficult to +replace the server with a different one (for instance, a Twisted server). +Since ``asynchat``-based servers are asynchronous, any blocking command +in the interpreter should be run in a separated process or thread. +The default port for the plac_ server is 2199, and the command to +signal end-of-connection is EOF. +For instance, here is how you could manage remote import on a database +(say a SQLite db): + +.. include:: server_ex.py + :literal: + +You can connect to the server with ``telnet`` on port 2199, as follows:: + + $ telnet localhost 2199 + Trying ::1... + Trying 127.0.0.1... + Connected to localhost. + Escape character is '^]'. + i> import_file f1 + i> .list + + i> .out + Imported 100 lines + Imported 200 lines + i> EOF + Connection closed by foreign host. + +Summary +------------------------------------------------------- + +Once plac_ claimed to be the easiest command-line arguments parser +in the world. Having read this document you may think that it is not +so easy after all. But it is a false impression. Actually the +rules are quite simple: + +1. + if you want to implement a command-line script, use ``plac.call``; + +2. + if you want to implement a command interpreter, use ``plac.Interpreter``: + + - for an interactive interpreter, call the ``.interact`` method; + - for an batch interpreter, call the ``.execute`` method; + +3. for testing call the ``Interpreter.check`` method in the appropriate context + or use the ``Interpreter.doctest`` feature; + +4. if you need to go at a lower level, you may need to call the + ``Interpreter.send`` method which returns a (finished) ``Task`` object. + +5. long running command can be executed in the background as threads or + processes: just declare them in the lists ``thcommands`` and ``mpcommands`` + respectively. + +6. the ``.start_server`` method starts an asynchronous server on the + given port number (default 2199) + +Moreover, remember that ``plac_runner.py`` is your friend. + +------ + +Appendix: 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 +.. _twill: http://twill.idyll.org/ +.. _basic documentation: http://micheles.googlecode.com/hg/plac/doc/plac.html +.. _shlex: http://docs.python.org/library/shlex.html +.. _multiprocessing: http://docs.python.org/library/multiprocessing.html +.. _distutils: http://docs.python.org/distutils/ +.. _cmd: http://docs.python.org/library/cmd.html +.. _rlwrap: http://freshmeat.net/projects/rlwrap/ +.. _pyreadline: http://ipython.scipy.org/moin/PyReadline/Intro diff --git a/doc/plac_core.html b/doc/plac_core.html new file mode 100644 index 0000000..a8b3c26 --- /dev/null +++ b/doc/plac_core.html @@ -0,0 +1,1354 @@ + + + + + + +Plac: Parsing the Command Line the Easy Way + + + + + +
+

Plac: Parsing the Command Line the Easy Way

+ +++ + + + + + + + + + + + + + + + + + +
Author:Michele Simionato
E-mail:michele.simionato@gmail.com
Date:July 2010
Download page:http://pypi.python.org/pypi/plac
Project page:http://micheles.googlecode.com/hg/plac/doc/plac.html
Requires:Python 2.3+
Installation:easy_install -U plac
License:BSD license
+ +
+

The importance of scaling down

+

There is no want of command line arguments parsers in the Python +world. The standard library alone contains three different modules: +getopt (from the stone age), +optparse (from Python 2.3) and argparse (from Python 2.7). All of +them are quite powerful and especially argparse is an industrial +strength solution; unfortunately, all of them feature a non-zero learning +curve and a certain verbosity. They do not scale down well, at +least in my opinion.

+

It should not be necessary to stress the importance scaling down; +nevertheless, a lot of people are obsessed with features and concerned with +the possibility of scaling up, forgetting the equally important +issue of scaling down. This is an old meme in +the computing world: programs should address the common cases simply and +simple things should be kept simple, while at the same keeping +difficult things possible. plac adhere as much as possible to this +philosophy and it is designed to handle well the simple cases, while +retaining the ability to handle complex cases by relying on the +underlying power of argparse.

+

Technically plac is just a simple wrapper over argparse which hides +most of its complexity by using a declarative interface: the argument +parser is inferred rather than written down by imperatively. Still, plac is +surprisingly scalable upwards, even without using the underlying +argparse. I have been using Python for 8 years and in my experience +it is extremely unlikely that you will ever need to go beyond the +features provided by the declarative interface of plac: they should +be more than enough for 99.9% of the use cases.

+

plac is targetting especially unsophisticated users, +programmers, sys-admins, scientists and in general people writing +throw-away scripts for themselves, choosing the command line +interface because it is the quick and simple. Such users are not +interested in features, they are interested in a small learning curve: +they just want to be able to write a simple command line tool from a +simple specification, not to build a command-line parser by +hand. Unfortunately, the modules in the standard library forces them +to go the hard way. They are designed to implement power user tools +and they have a non-trivial learning curve. On the contrary, plac +is designed to be simple to use and extremely concise, as the examples +below will show.

+
+
+

Scripts with required arguments

+

Let me start with the simplest possible thing: a script that takes a +single argument and does something to it. It cannot get simpler +than that, unless you consider a script without command-line +arguments, where there is nothing to parse. Still, it is a use +case extremely common: I need to write scripts like that nearly +every day, I wrote hundreds of them in the last few years and I have +never been happy. Here is a typical example of code I have been +writing by hand for years:

+
+# example1.py
+def main(dsn):
+    "Do something with the database"
+    print(dsn)
+    # ...
+
+if __name__ == '__main__':
+    import sys
+    n = len(sys.argv[1:])
+    if n == 0:
+        sys.exit('usage: python %s dsn' % sys.argv[0])
+    elif n == 1:
+        main(sys.argv[1])
+    else:
+        sys.exit('Unrecognized arguments: %s' % ' '.join(sys.argv[2:]))
+
+
+

As you see the whole if __name__ == '__main__' block (nine lines) +is essentially boilerplate that should not exist. Actually I think +the language should recognize the main function and pass to it the +command-line arguments automatically; unfortunaly this is unlikely to +happen. I have been writing boilerplate like this in hundreds of +scripts for years, and every time I hate it. The purpose of using a +scripting language is convenience and trivial things should be +trivial. Unfortunately the standard library does not help for this +incredibly common use case. Using getopt and optparse does not help, +since they are intended to manage options and not positional +arguments; the argparse module helps a bit and it is able to reduce +the boilerplate from nine lines to six lines:

+
+# example2.py
+def main(dsn):
+    "Do something on the database"
+    print(dsn)
+    # ...
+
+if __name__ == '__main__':
+    import argparse
+    p = argparse.ArgumentParser()
+    p.add_argument('dsn')
+    arg = p.parse_args()
+    main(arg.dsn)
+
+
+

However saving three lines does not justify introducing the external +dependency: most people will not switch to Python 2.7, which at the time of +this writing is just about to be released, for many years. +Moreover, it just feels too complex to instantiate a class and to +define a parser by hand for such a trivial task.

+

The plac module is designed to manage well such use cases, and it is able +to reduce the original nine lines of boiler plate to two lines. With the +plac module all you need to write is

+
+# example3.py
+def main(dsn):
+    "Do something with the database"
+    print(dsn)
+    # ...
+ 
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+

The plac module provides for free (actually the work is done by the +underlying argparse module) a nice usage message:

+
+$ python example3.py -h
+
+
+usage: example3.py [-h] dsn
+
+Do something with the database
+
+positional arguments:
+  dsn
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+

Moreover plac manages the case of missing arguments and of too many arguments. +This is only the tip of the iceberg: plac is able to do much more than that.

+
+
+

Scripts with default arguments

+

The need to have suitable defaults for command-line scripts is quite +common. For instance I have encountered this use case at work hundreds +of times:

+
+# example4.py
+from datetime import datetime
+
+def main(dsn, table='product', today=datetime.today()):
+    "Do something on the database"
+    print(dsn, table, today)
+
+if __name__ == '__main__':
+    import sys
+    args = sys.argv[1:]
+    if not args:
+        sys.exit('usage: python %s dsn' % sys.argv[0])
+    elif len(args) > 2:
+        sys.exit('Unrecognized arguments: %s' % ' '.join(argv[2:]))
+    main(*args)
+
+
+

Here I want to perform a query on a database table, by extracting the +most recent data: it makes sense for today to be a default argument. +If there is a most used table (in this example a table called 'product') +it also makes sense to make it a default argument. Performing the parsing +of the command-line arguments by hand takes 8 ugly lines of boilerplate +(using argparse would require about the same number of lines). +With plac the entire __main__ block reduces to the usual two lines:

+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+

In other words, six lines of boilerplate have been removed, and we get +the usage message for free:

+
+usage: example5.py [-h] dsn [table] [today]
+
+Do something on the database
+
+positional arguments:
+  dsn
+  table
+  today
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+

plac manages transparently even the case when you want to pass a +variable number of arguments. Here is an example, a script running +on a database a series of SQL scripts:

+
+# example7.py
+from datetime import datetime
+
+def main(dsn, *scripts):
+    "Run the given scripts on the database"
+    for script in scripts:
+        print('executing %s' % script)
+        # ...
+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+

Here is the usage message:

+
+usage: example7.py [-h] dsn [scripts [scripts ...]]
+
+Run the given scripts on the database
+
+positional arguments:
+  dsn
+  scripts
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+

The examples here should have made clear that plac is able to figure out +the command-line arguments parser to use from the signature of the main +function. This is the whole idea behind plac: if the intent is clear, +let's the machine take care of the details.

+

plac is inspired to an old Python Cookbook recipe (optionparse), in +the sense that it delivers the programmer from the burden of writing +the parser, but is less of a hack: instead of extracting the parser +from the docstring of the module, it extracts it from the signature of +the main function.

+

The idea comes from the function annotations concept, a new +feature of Python 3. An example is worth a thousand words, so here +it is:

+
+# 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)
+
+
+

Here the arguments of the main function have been annotated with +strings which are intented to be used in the help message:

+
+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
+
+
+

plac is able to recognize much more complex annotations, as +I will show in the next paragraphs.

+
+
+

Scripts with options (and smart options)

+

It is surprising how few command-line scripts with options I have +written over the years (probably less than a hundred), compared to the +number of scripts with positional arguments I wrote (certainly more +than a thousand of them). Still, this use case cannot be neglected. +The standard library modules (all of them) are quite verbose when it +comes to specifying the options and frankly I have never used them +directly. Instead, I have always relied on the +optionparse recipe, which provides a convenient wrapper over +optionparse. Alternatively, in the simplest cases, I have just +performed the parsing by hand. In plac the parser is inferred by the +function annotations. Here is an example:

+
+# example8.py
+def main(command: ("SQL query", 'option', 'c'), dsn):
+    if command:
+        print('executing %s on %s' % (command, dsn))
+        # ...
+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+

Here the argument command has been annotated with the tuple +("SQL query", 'option', 'c'): the first string is the help string +which will appear in the usage message, the second string tells plac +that command is an option and the third string that there is also +a short form of the option -c, the long form being --command. +The usage message is the following:

+
+usage: example8.py [-h] [-c COMMAND] dsn
+
+positional arguments:
+  dsn
+
+optional arguments:
+  -h, --help            show this help message and exit
+  -c COMMAND, --command COMMAND
+                        SQL query
+
+
+

Here are two examples of usage:

+
+$ python3 example8.py -c"select * from table" dsn
+executing select * from table on dsn
+
+$ python3 example8.py --command="select * from table" dsn
+executing select * from table on dsn
+
+

The third argument in the function annotation can be omitted: in such +case it will be assumed to be None. The consequence is that +the usual dichotomy between long and short options (GNU-style options) +disappears: we get smart options, which have the single character prefix +of short options and behave like both long and short options, since +they can be abbreviated. Here is an example featuring smart options:

+
+# example6.py
+def main(dsn, command: ("SQL query", 'option')):
+    print('executing %r on %s' % (command, dsn))
+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+
+usage: example6.py [-h] [-command COMMAND] dsn
+
+positional arguments:
+  dsn
+
+optional arguments:
+  -h, --help        show this help message and exit
+  -command COMMAND  SQL query
+
+
+

The following are all valid invocations ot the script:

+
+$ python3 example6.py -c "select" dsn
+executing 'select' on dsn
+$ python3 example6.py -com "select" dsn
+executing 'select' on dsn
+$ python3 example6.py -command="select" dsn
+executing 'select' on dsn
+
+

Notice that the form -command=SQL is recognized only for the full +option, not for its abbreviations:

+
+$ python3 example6.py -com="select" dsn
+usage: example6.py [-h] [-command COMMAND] dsn
+example6.py: error: unrecognized arguments: -com=select
+
+

If the option is not passed, the variable command +will get the value None. However, it is possible to specify a non-trivial +default. Here is an example:

+
+# example8_.py
+def main(dsn, command: ("SQL query", 'option')='select * from table'):
+    print('executing %r on %s' % (command, dsn))
+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+

Notice that the default value appears in the help message:

+
+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
+
+
+

When you run the script and you do not pass the -command option, the +default query will be executed:

+
+$ python3 example8_.py dsn
+executing 'select * from table' on dsn
+
+
+
+

Scripts with flags

+

plac is able to recognize flags, i.e. boolean options which are +True if they are passed to the command line and False +if they are absent. Here is an example:

+
+# example9.py
+
+def main(verbose: ('prints more info', 'flag', 'v'), dsn: 'connection string'):
+    if verbose:
+        print('connecting to %s' % dsn)
+    # ...
+
+if __name__ == '__main__':
+    import plac; plac.call(main)
+
+
+
+usage: example9.py [-h] [-v] dsn
+
+positional arguments:
+  dsn            connection string
+
+optional arguments:
+  -h, --help     show this help message and exit
+  -v, --verbose  prints more info
+
+
+
+$ python3 example9.py -v dsn
+connecting to dsn
+
+

Notice that it is an error trying to specify a default for flags: the +default value for a flag is always False. If you feel the need to +implement non-boolean flags, you should use an option with two +choices, as explained in the "more features" section.

+

For consistency with the way the usage message is printed, I suggest +you to follow the Flag-Option-Required-Default (FORD) convention: in +the main function write first the flag arguments, then the option +arguments, then the required arguments and finally the default +arguments. This is just a convention and you are not forced to use it, +except for the default arguments (including the varargs) which must +stay at the end as it is required by the Python syntax.

+

I also suggests to specify a one-character abbreviation for flags: in +this way you can use the GNU-style composition of flags (i.e. -zxvf +is an abbreviation of -z -x -v -f). I usually do not provide +the one-character abbreviation for options, since it does not make sense +to compose them.

+
+
+

plac for Python 2.X users

+

I do not use Python 3. At work we are just starting to think about +migrating to Python 2.6. It will take years before we +think to migrate to Python 3. I am pretty much sure most Pythonistas +are in the same situation. Therefore plac provides a way to work +with function annotations even in Python 2.X (including Python 2.3). +There is no magic involved; you just need to add the annotations +by hand. For instance the annotated function declaration

+
+def main(dsn: "Database dsn", *scripts: "SQL scripts"):
+    ...
+
+

is equivalent to the following code:

+
+def main(dsn, *scripts):
+    ...
+main.__annotations__ = dict(
+    dsn="Database dsn",
+    scripts="SQL scripts")
+
+

One should be careful to match the keys of the annotation dictionary +with the names of the arguments in the annotated function; for lazy +people with Python 2.4 available the simplest way is to use the +plac.annotations decorator that performs the check for you:

+
+@plac.annotations(
+    dsn="Database dsn",
+    scripts="SQL scripts")
+def main(dsn, *scripts):
+    ...
+
+

In the rest of this article I will assume that you are using Python 2.X with +X >= 4 and I will use the plac.annotations decorator. Notice however +that the core features of plac run even on Python 2.3.

+
+
+

More features

+

One of the goals of plac is to have a learning curve of minutes for +its core features, compared to the learning curve of hours of +argparse. In order to reach this goal, I have not sacrificed all +the features of argparse. Actually a lot of argparse power persists +in plac. Until now, I have only showed simple annotations, but in +general an annotation is a 6-tuple of the form

+
+(help, kind, abbrev, type, choices, metavar)
+

where help is the help message, kind is a string in the set { +"flag", "option", "positional"}, abbrev is a +one-character string or None, type is a callable taking a +string in input, +choices is a discrete sequence of values and metavar is a string.

+

type is used to automagically convert the command line arguments +from the string type to any Python type; by default there is no +conversion and type=None.

+

choices is used to restrict the number of the valid +options; by default there is no restriction i.e. choices=None.

+

metavar is used to change the argument name in the usage message +(and only there); by default the metavar is None: this means that +the name in the usage message is the same as the argument name, +unless the argument has a default and in such a case is +equal to the stringified form of the default.

+

Here is an example showing many of the features (copied from the +argparse documentation):

+
+# example10.py
+import plac
+
+@plac.annotations(
+operator=("The name of an operator", 'positional', None, str, ['add', 'mul']),
+numbers=("A number", 'positional', None, float, None, "n"))
+def main(operator, *numbers):
+    "A script to add and multiply numbers"
+    if operator == 'mul':
+        op = float.__mul__
+        result = 1.0
+    else: # operator == 'add'
+        op = float.__add__
+        result = 0.0
+    for n in numbers:
+        result = op(result, n)
+    return result
+
+if __name__ == '__main__':
+    print(plac.call(main))
+
+
+

Here is the usage:

+
+usage: example10.py [-h] {add,mul} [n [n ...]]
+
+A script to add and multiply numbers
+
+positional arguments:
+  {add,mul}   The name of an operator
+  n           A number
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+

Notice that the docstring of the main function has been automatically added +to the usage message. Here are a couple of examples of use:

+
+$ python example10.py add 1 2 3 4
+10.0
+$ python example10.py mul 1 2 3 4
+24.0
+$ python example10.py ad 1 2 3 4 # a mispelling error
+usage: example10.py [-h] {add,mul} [n [n ...]]
+example10.py: error: argument operator: invalid choice: 'ad' (choose from 'add', 'mul')
+
+

plac.call can also be used in doctests like this:

+
+>>> import plac, example10
+>>> plac.call(example10.main, ['add', '1', '2'])
+3.0
+
+

plac.call works for generators too:

+
+>>> def main(n):
+...     for i in range(int(n)):
+...         yield i
+>>> plac.call(main, ['3'])
+[0, 1, 2]
+
+

Internally plac.call tries to convert the output of the main function +into a list, if possible. If the output is not iterable or it is a +string, it is left unchanged, but if it is iterable it is converted. +In particular, generator objects are exhausted by plac.call.

+

This behavior avoids mistakes like forgetting of applying +list(result) to the result of plac.call; moreover it makes +errors visible early, and avoids mistakes in code like the following:

+
+try:
+    result = plac.call(main, args)
+except:
+   # do something
+
+

Without the "listify" functionality, a main function returning a +generator object would not raise any exception until the generator +is iterated over.

+

If you are a fan of lazyness, you can still have it by setting the eager +flag to False, as in the following example:

+
+for line in plac.call(main, args, eager=False):
+    print(line)
+
+

If main returns a generator object this example will print each +line as soon as available, whereas the default behaviour is to print +all the lines together and the end of the computation.

+
+
+

A realistic example

+

Here is a more realistic script using most of the features of plac to +run SQL queries on a database by relying on SQLAlchemy. Notice the usage +of the type feature to automagically convert a SQLAlchemy connection +string into a SqlSoup object:

+
+# dbcli.py
+import plac
+from sqlalchemy.ext.sqlsoup import SqlSoup
+
+@plac.annotations(
+    db=("Connection string", 'positional', None, SqlSoup),
+    header=("Header", 'flag', 'H'),
+    sqlcmd=("SQL command", 'option', 'c', str, None, "SQL"),
+    delimiter=("Column separator", 'option', 'd'),
+    scripts="SQL scripts",
+    )
+def main(db, header, sqlcmd, delimiter="|", *scripts):
+    "A script to run queries and SQL scripts on a database"
+    yield 'Working on %s' % db.bind.url
+
+    if sqlcmd:
+        result = db.bind.execute(sqlcmd)
+        if header: # print the header
+            yield delimiter.join(result.keys())
+        for row in result: # print the rows
+            yield delimiter.join(map(str, row))
+
+    for script in scripts:
+        db.bind.execute(file(script).read())
+        yield 'executed %s' % script
+
+if __name__ == '__main__':
+    for output in plac.call(main):
+        print(output)
+
+
+

You can see the yield-is-print pattern here: instead of using +print in the main function, I use yield, and I perform the +print in the __main__ block. The advantage of the pattern is that +tests invoking plac.call and checking the result become trivial: +had I performed the printing in the main function, the test would have +involved an ugly hack like redirecting sys.stdout to a +StringIO object.

+

Here is the usage message:

+
+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
+
+optional arguments:
+  -h, --help            show this help message and exit
+  -H, --header          Header
+  -c SQL, --sqlcmd SQL  SQL command
+  -d |, --delimiter |   Column separator
+
+
+

You can check for yourself that the script works.

+
+
+

Keyword arguments

+

Starting from release 0.4, plac supports keyword arguments. In +practice that means that if your main function has keyword arguments, +plac treats specially arguments of the form "name=value" in the +command line. Here is an example:

+
+# example12.py
+import plac
+
+@plac.annotations(
+   opt=('some option', 'option'),
+   args='default arguments',
+   kw='keyword arguments')
+def main(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__':
+    for output in plac.call(main):
+       print(output)
+
+
+

Here is the generated usage message:

+
+usage: example12.py [-h] [-opt OPT] [args [args ...]] [kw [kw ...]]
+
+positional arguments:
+  args        default arguments
+  kw          keyword arguments
+
+optional arguments:
+  -h, --help  show this help message and exit
+  -opt OPT    some option
+
+
+

Here is how you call the script:

+
+$ python example12.py -o X a1 a2 name=value
+opt=X
+args=('a1', 'a2')
+kw={'name': 'value'}
+
+

When using keyword arguments, one must be careful to use names which +are not alreay taken; for instance in this examples the name opt +is taken:

+
+$ python example12.py 1 2 kw1=1 kw2=2 opt=0
+usage: example12.py [-h] [-o OPT] [args [args ...]] [kw [kw ...]]
+example12.py: error: colliding keyword arguments: opt
+
+

The names taken are the names of the flags, of the options, and of the +positional arguments, excepted varargs and keywords. This limitation +is a consequence of the way the argument names are managed in function calls +by the Python language.

+
+
+

Final example: a shelve interface

+

Here is a less trivial example for the keyword arguments feature. +The use case is the following: suppose we have stored the +configuration parameters of a given application into a Python shelve +and we need a command-line tool to edit the shelve. +A possible implementation using plac could be the following:

+
+# ishelve.py
+import os, shelve, plac
+
+DEFAULT_SHELVE = os.path.expanduser('~/conf.shelve')
+
+@plac.annotations(
+    help=('show help', 'flag'),
+    showall=('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 main(help, showall, clear, delete, filename=DEFAULT_SHELVE,
+         *params, **setters):
+    "A simple interface to a shelve. Use .help to see the available commands."
+    sh = shelve.open(filename)
+    try:
+        if not any([help, showall, clear, delete, params, setters]):
+            yield 'no arguments passed, use .help to see the available commands'
+        elif help: # custom help
+            yield 'Commands: .help, .showall, .clear, .delete'
+            yield '<param> ...'
+            yield '<param=value> ...'
+        elif showall:
+            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()
+
+main.add_help = False # there is a custom help, remove the default one
+main.prefix_chars = '.' # use dot-prefixed commands
+
+if __name__ == '__main__':
+    for output in plac.call(main):
+        print(output)
+
+
+

A few notes are in order:

+
    +
  1. I have disabled the ordinary help provided by argparse and I have +implemented a custom help command.
  2. +
  3. I have changed the prefix character used to recognize the options +to a dot.
  4. +
  5. Keyword arguments recognition (in the **setters) is used to make it +possible to store a value in the shelve with the syntax +param_name=param_value.
  6. +
  7. *params are used to retrieve parameters from the shelve and some +error checking is performed in the case of missing parameters
  8. +
  9. A command to clear the shelve is implemented as a flag (.clear).
  10. +
  11. A command to delete a given parameter is implemented as an option +(.delete).
  12. +
  13. There is an option with default (.filename=conf.shelve) to store +the filename of the shelve.
  14. +
  15. All things considered, the code looks like a poor man object oriented +interface implemented with a chain of elifs instead of methods. Of course, +plac can do better than that, but let me start from a low-level approach +first.
  16. +
+

If you run ishelve.py without arguments you get the following +message:

+
+$ python ishelve.py
+no arguments passed, use .help to see the available commands
+
+

If you run ishelve.py with the option .h (or any abbreviation +of .help) you get:

+
+$ python ishelve.py .h
+Commands: .help, .showall, .clear, .delete
+<param> ...
+<param=value> ...
+
+

You can check by hand that the tool work:

+
+$ python ishelve.py .clear # start from an empty shelve
+cleared the shelve
+$ python ishelve.py a=1 b=2
+setting a=1
+setting b=2
+$ python ishelve.py .showall
+b=2
+a=1
+$ python ishelve.py .del b # abbreviation for .delete
+deleted b
+$ python ishelve.py a
+1
+$ python ishelve.py b
+b: not found
+$ python ishelve.py .cler # mispelled command
+usage: ishelve.py [.help] [.showall] [.clear] [.delete DELETE]
+                  [.filename /home/micheles/conf.shelve]
+                  [params [params ...]] [setters [setters ...]]
+ishelve.py: error: unrecognized arguments: .cler
+
+
+
+

plac vs argparse

+

plac is opinionated and by design it does not try to make available +all of the features of argparse in an easy way. In particular you +should be aware of the following limitations/differences (the +following assumes knowledge of argparse):

+
    +
  • plac does not support the destination concept: the destination +coincides with the name of the argument, always. This restriction +has some drawbacks. For instance, suppose you want to define a long +option called --yield. In this case the destination would be yield, +which is a Python keyword, and since you cannot introduce an +argument with that name in a function definition, it is impossible +to implement it. Your choices are to change the name of the long +option, or to use argparse with a suitable destination.
  • +
  • plac does not support "required options". As the argparse +documentation puts it: Required options are generally considered bad +form - normal users expect options to be optional. You should avoid +the use of required options whenever possible. Notice that since +argparse supports them, plac can manage them too, but not directly.
  • +
  • plac supports only regular boolean flags. argparse has the ability to +define generalized two-value flags with values different from True +and False. An earlier version of plac had this feature too, but +since you can use options with two choices instead, and in any case +the conversion from {True, False} to any couple of values +can be trivially implemented with a ternary operator +(value1 if flag else value2), I have removed it (KISS rules!).
  • +
  • plac does not support nargs options directly (it uses them internally, +though, to implement flag recognition). The reason it that all the use +cases of interest to me are covered by plac and did not feel the need +to increase the learning curve by adding direct support for nargs.
  • +
  • plac does support subparsers, but you must read the advanced usage +document to see how it works.
  • +
  • plac does not support actions directly. This also +looks like a feature too advanced for the goals of plac. Notice however +that the ability to define your own annotation objects (again, see +the advanced usage document) may mitigate the need for custom actions.
  • +
+

plac can leverage directly on many argparse features.

+

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
+
+

disables the recognition of the help flag -h, --help. This +mechanism does not look particularly elegant, but it works well +enough. I assume that 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='/-'
+
+

The first prefix char (/) is used +as the default for the recognition of options and flags; +the second prefix char (-) is kept to keep the -h/--help option +working: however you can disable it and reimplement it, if you like, +as seen in the ishelve example.

+

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)) #doctest: +ELLIPSIS
+ArgumentParser(prog=...)
+
+

Internally plac.call uses plac.parser_from and adds the parser +to the main function as an attribute. When plac.call(func) is +invoked multiple time, the parser is re-used and not rebuilt from scratch again.

+

I use plac.parser_from in the unit tests of the module, but regular +users should not need to use it, unless they want to access all +of the features of argparse directly without calling the main function.

+

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 except the addition of the .p +attribute.

+
+
+

plac vs the rest of the world

+

Originally plac boasted about being "the easiest command-line +arguments parser in the world". Since then, people started pointing +out to me various projects which are based on the same idea +(extracting the parser from the main function signature) and are +arguably even easier than plac:

+ +

Luckily for me none of such projects had the idea of using +function annotations and argparse; as a consequence, they are +no match for the capabilities of plac.

+

Of course, there are tons of other libraries to parse the command +line. For instance Clap by Matthew Frazier which appeared on PyPI +just the day before plac; Clap is fine but it is certainly not +easier than plac.

+

plac can also be used as a replacement of the cmd module in the standard +library and as such it shares many features with the module cmd2 by +Catherine Devlin. However, this is completely coincidental, since I became +aware of the cmd2 module only after writing plac.

+
+
+

The future

+

Currently the core of plac is around 200 lines of code, not counting blanks, +comments and docstrings. I do not plan to extend the core much in the +future. The idea is to keep the module short: it is and it should +remain a little wrapper over argparse. Actually I have thought about +contributing the core back to argparse if plac becomes successfull +and gains a reasonable number of users. For the moment it should be +considered in alpha status.

+

Notice that even if plac has been designed to be simple to use for +simple stuff, its power should not be underestimated; it is actually a +quite advanced tool with a domain of applicability which far exceeds +the realm of command-line arguments parsers.

+

Version 0.5 of plac doubled the code base and the documentation: it is +based on the idea of using plac to implement command-line interpreters, +i.e. something akin to the cmd module in the standard library, only better. +The new features of plac are described in the advanced usage document . +They are implemented in a separated module (plac_ext.py), since +they require Python 2.5 to work, whereas plac_core.py only requires +Python 2.3.

+
+
+

Trivia: the story behind the name

+

The plac project started very humbly: I just wanted to make +easy_installable my old optionparse recipe, and to publish it on PyPI. +The original name of plac was optionparser and the idea behind it was +to build an OptionParser object from the docstring of the module. +However, before doing that, I decided to check out the argparse module, +since I knew it was going into Python 2.7 and Python 2.7 was coming out. +Soon enough I realized two things:

+
    +
  1. the single greatest idea of argparse was unifying the positional arguments +and the options in a single namespace object;
  2. +
  3. parsing the docstring was so old-fashioned, considering the existence +of functions annotations in Python 3.
  4. +
+

Putting together these two observations with the original idea of inferring the +parser I decided to build an ArgumentParser object from function +annotations. The optionparser name was ruled out, since I was +now using argparse; a name like argparse_plus was also ruled out, +since the typical usage was completely different from the argparse usage.

+

I made a research on PyPI and the name clap (Command Line Arguments Parser) +was not taken, so I renamed everything to clap. After two days +a Clap module appeared on PyPI <expletives deleted>!

+

Having little imagination, I decided to rename everything again to plac, +an anagram of clap: since it is a non-existing English name, I hope nobody +will steal it from me!

+

That's all, I hope you will enjoy working with plac!

+
+
+ + diff --git a/doc/plac_core.txt b/doc/plac_core.txt new file mode 100644 index 0000000..1509c94 --- /dev/null +++ b/doc/plac_core.txt @@ -0,0 +1,782 @@ +Plac: Parsing the Command Line the Easy Way +===================================================================== + +:Author: Michele Simionato +:E-mail: michele.simionato@gmail.com +:Date: August 2010 +:Download page: http://pypi.python.org/pypi/plac +:Project page: http://micheles.googlecode.com/hg/plac/doc/plac.html +:Requires: Python 2.3+ +:Installation: ``easy_install -U plac`` +:License: BSD license + +.. contents:: + +The importance of scaling down +------------------------------------------------ + +There is no want of command line arguments parsers in the Python +world. The standard library alone contains three different modules: +getopt_ (from the stone age), +optparse_ (from Python 2.3) and argparse_ (from Python 2.7). All of +them are quite powerful and especially argparse_ is an industrial +strength solution; unfortunately, all of them feature a non-zero learning +curve and a certain verbosity. They do not scale down well, at +least in my opinion. + +It should not be necessary to stress the importance `scaling down`_; +nevertheless, a lot of people are obsessed with features and concerned with +the possibility of scaling up, forgetting the equally important +issue of scaling down. This is an old meme in +the computing world: programs should address the common cases simply and +simple things should be kept simple, while at the same keeping +difficult things possible. plac_ adhere as much as possible to this +philosophy and it is designed to handle well the simple cases, while +retaining the ability to handle complex cases by relying on the +underlying power of argparse_. + +Technically plac_ is just a simple wrapper over argparse_ which hides +most of its complexity by using a declarative interface: the argument +parser is inferred rather than written down by imperatively. Still, plac_ is +surprisingly scalable upwards, even without using the underlying +argparse_. I have been using Python for 8 years and in my experience +it is extremely unlikely that you will ever need to go beyond the +features provided by the declarative interface of plac_: they should +be more than enough for 99.9% of the use cases. + +plac_ is targetting especially unsophisticated users, +programmers, sys-admins, scientists and in general people writing +throw-away scripts for themselves, choosing the command line +interface because it is the quick and simple. Such users are not +interested in features, they are interested in a small learning curve: +they just want to be able to write a simple command line tool from a +simple specification, not to build a command-line parser by +hand. Unfortunately, the modules in the standard library forces them +to go the hard way. They are designed to implement power user tools +and they have a non-trivial learning curve. On the contrary, plac_ +is designed to be simple to use and extremely concise, as the examples +below will show. + +Scripts with required arguments +--------------------------------------------- + +Let me start with the simplest possible thing: a script that takes a +single argument and does something to it. It cannot get simpler +than that, unless you consider a script without command-line +arguments, where there is nothing to parse. Still, it is a use +case *extremely common*: I need to write scripts like that nearly +every day, I wrote hundreds of them in the last few years and I have +never been happy. Here is a typical example of code I have been +writing by hand for years: + +.. include:: example1.py + :literal: + +As you see the whole ``if __name__ == '__main__'`` block (nine lines) +is essentially boilerplate that should not exist. Actually I think +the language should recognize the main function and pass to it the +command-line arguments automatically; unfortunaly this is unlikely to +happen. I have been writing boilerplate like this in hundreds of +scripts for years, and every time I *hate* it. The purpose of using a +scripting language is convenience and trivial things should be +trivial. Unfortunately the standard library does not help for this +incredibly common use case. Using getopt_ and optparse_ does not help, +since they are intended to manage options and not positional +arguments; the argparse_ module helps a bit and it is able to reduce +the boilerplate from nine lines to six lines: + +.. include:: example2.py + :literal: + +However saving three lines does not justify introducing the external +dependency: most people will not switch to Python 2.7, which at the time of +this writing is just about to be released, for many years. +Moreover, it just feels too complex to instantiate a class and to +define a parser by hand for such a trivial task. + +The plac_ module is designed to manage well such use cases, and it is able +to reduce the original nine lines of boiler plate to two lines. With the +plac_ module all you need to write is + +.. include:: example3.py + :literal: + +The plac_ module provides for free (actually the work is done by the +underlying argparse_ module) a nice usage message:: + + $ python example3.py -h + +.. include:: example3.help + :literal: + +Moreover plac_ manages the case of missing arguments and of too many arguments. +This is only the tip of the iceberg: plac_ is able to do much more than that. + +Scripts with default arguments +-------------------------------------------------- + +The need to have suitable defaults for command-line scripts is quite +common. For instance I have encountered this use case at work hundreds +of times: + +.. include:: example4.py + :literal: + +Here I want to perform a query on a database table, by extracting the +most recent data: it makes sense for ``today`` to be a default argument. +If there is a most used table (in this example a table called ``'product'``) +it also makes sense to make it a default argument. Performing the parsing +of the command-line arguments by hand takes 8 ugly lines of boilerplate +(using argparse_ would require about the same number of lines). +With plac_ the entire ``__main__`` block reduces to the usual two lines:: + + if __name__ == '__main__': + import plac; plac.call(main) + +In other words, six lines of boilerplate have been removed, and we get +the usage message for free: + +.. include:: example5.help + :literal: + +plac_ manages transparently even the case when you want to pass a +variable number of arguments. Here is an example, a script running +on a database a series of SQL scripts: + +.. include:: example7.py + :literal: + +Here is the usage message: + +.. include:: example7.help + :literal: + +The examples here should have made clear that *plac is able to figure out +the command-line arguments parser to use from the signature of the main +function*. This is the whole idea behind plac_: if the intent is clear, +let's the machine take care of the details. + +plac_ is inspired to an old Python Cookbook recipe (optionparse_), in +the sense that it delivers the programmer from the burden of writing +the parser, but is less of a hack: instead of extracting the parser +from the docstring of the module, it extracts it from the signature of +the ``main`` function. + +The idea comes from the `function annotations` concept, a new +feature of Python 3. An example is worth a thousand words, so here +it is: + +.. include:: example7_.py + :literal: + +Here the arguments of the ``main`` function have been annotated with +strings which are intented to be used in the help message: + +.. include:: example7_.help + :literal: + +plac_ is able to recognize much more complex annotations, as +I will show in the next paragraphs. + + +Scripts with options (and smart options) +----------------------------------------- + +It is surprising how few command-line scripts with options I have +written over the years (probably less than a hundred), compared to the +number of scripts with positional arguments I wrote (certainly more +than a thousand of them). Still, this use case cannot be neglected. +The standard library modules (all of them) are quite verbose when it +comes to specifying the options and frankly I have never used them +directly. Instead, I have always relied on the +optionparse_ recipe, which provides a convenient wrapper over +optionparse_. Alternatively, in the simplest cases, I have just +performed the parsing by hand. In plac_ the parser is inferred by the +function annotations. Here is an example: + +.. include:: example8.py + :literal: + +Here the argument ``command`` has been annotated with the tuple +``("SQL query", 'option', 'c')``: the first string is the help string +which will appear in the usage message, the second string tells plac_ +that ``command`` is an option and the third string that there is also +a short form of the option ``-c``, the long form being ``--command``. +The usage message is the following: + +.. include:: example8.help + :literal: + +Here are two examples of usage:: + + $ python3 example8.py -c"select * from table" dsn + executing select * from table on dsn + + $ python3 example8.py --command="select * from table" dsn + executing select * from table on dsn + +The third argument in the function annotation can be omitted: in such +case it will be assumed to be ``None``. The consequence is that +the usual dichotomy between long and short options (GNU-style options) +disappears: we get *smart options*, which have the single character prefix +of short options and behave like both long and short options, since +they can be abbreviated. Here is an example featuring smart options: + +.. include:: example6.py + :literal: + +.. include:: example6.help + :literal: + +The following are all valid invocations ot the script:: + + $ python3 example6.py -c "select" dsn + executing 'select' on dsn + $ python3 example6.py -com "select" dsn + executing 'select' on dsn + $ python3 example6.py -command="select" dsn + executing 'select' on dsn + +Notice that the form ``-command=SQL`` is recognized only for the full +option, not for its abbreviations:: + + $ python3 example6.py -com="select" dsn + usage: example6.py [-h] [-command COMMAND] dsn + example6.py: error: unrecognized arguments: -com=select + +If the option is not passed, the variable ``command`` +will get the value ``None``. However, it is possible to specify a non-trivial +default. Here is an example: + +.. include:: example8_.py + :literal: + +Notice that the default value appears in the help message: + +.. include:: example8_.help + :literal: + +When you run the script and you do not pass the ``-command`` option, the +default query will be executed:: + + $ python3 example8_.py dsn + executing 'select * from table' on dsn + +Scripts with flags +-------------------- + +plac_ is able to recognize flags, i.e. boolean options which are +``True`` if they are passed to the command line and ``False`` +if they are absent. Here is an example: + +.. include:: example9.py + :literal: + +.. include:: example9.help + :literal: + +:: + + $ python3 example9.py -v dsn + connecting to dsn + +Notice that it is an error trying to specify a default for flags: the +default value for a flag is always ``False``. If you feel the need to +implement non-boolean flags, you should use an option with two +choices, as explained in the "more features" section. + +For consistency with the way the usage message is printed, I suggest +you to follow the Flag-Option-Required-Default (FORD) convention: in +the ``main`` function write first the flag arguments, then the option +arguments, then the required arguments and finally the default +arguments. This is just a convention and you are not forced to use it, +except for the default arguments (including the varargs) which must +stay at the end as it is required by the Python syntax. + +I also suggests to specify a one-character abbreviation for flags: in +this way you can use the GNU-style composition of flags (i.e. ``-zxvf`` +is an abbreviation of ``-z -x -v -f``). I usually do not provide +the one-character abbreviation for options, since it does not make sense +to compose them. + +plac for Python 2.X users +-------------------------------------------------- + +I do not use Python 3. At work we are just starting to think about +migrating to Python 2.6. It will take years before we +think to migrate to Python 3. I am pretty much sure most Pythonistas +are in the same situation. Therefore plac_ provides a way to work +with function annotations even in Python 2.X (including Python 2.3). +There is no magic involved; you just need to add the annotations +by hand. For instance the annotated function declaration + +:: + + def main(dsn: "Database dsn", *scripts: "SQL scripts"): + ... + +is equivalent to the following code:: + + def main(dsn, *scripts): + ... + main.__annotations__ = dict( + dsn="Database dsn", + scripts="SQL scripts") + +One should be careful to match the keys of the annotation dictionary +with the names of the arguments in the annotated function; for lazy +people with Python 2.4 available the simplest way is to use the +``plac.annotations`` decorator that performs the check for you:: + + @plac.annotations( + dsn="Database dsn", + scripts="SQL scripts") + def main(dsn, *scripts): + ... + +In the rest of this article I will assume that you are using Python 2.X with +X >= 4 and I will use the ``plac.annotations`` decorator. Notice however +that the core features of plac_ run even on Python 2.3. + +More features +-------------------------------------------------- + +One of the goals of plac_ is to have a learning curve of *minutes* for +its core features, compared to the learning curve of *hours* of +argparse_. In order to reach this goal, I have *not* sacrificed all +the features of argparse_. Actually a lot of argparse_ power persists +in plac_. Until now, I have only showed simple annotations, but in +general an annotation is a 6-tuple of the form + + ``(help, kind, abbrev, type, choices, metavar)`` + +where ``help`` is the help message, ``kind`` is a string in the set { +``"flag"``, ``"option"``, ``"positional"``}, ``abbrev`` is a +one-character string or ``None``, ``type`` is a callable taking a +string in input, +``choices`` is a discrete sequence of values and ``metavar`` is a string. + +``type`` is used to automagically convert the command line arguments +from the string type to any Python type; by default there is no +conversion and ``type=None``. + +``choices`` is used to restrict the number of the valid +options; by default there is no restriction i.e. ``choices=None``. + +``metavar`` has two meanings. For a positional argument it is used to +change the argument name in the usage message (and only there). By +default the metavar is ``None`` and the name in the usage message is +the same as the argument name. For an option +the ``metavar`` is used differently in the usage message, which has +now the form ``[--option-name METAVAR]``. If the ``metavar`` is ``None``, +then it is equal to the uppercased name of the argument, unless the +argument has a default and in such a case is equal to the stringified +form of the default. + +Here is an example showing many of the features (copied from the +argparse_ documentation): + +.. include:: example10.py + :literal: + +Here is the usage: + +.. include:: example10.help + :literal: + +Notice that the docstring of the ``main`` function has been automatically added +to the usage message. Here are a couple of examples of use:: + + $ python example10.py add 1 2 3 4 + 10.0 + $ python example10.py mul 1 2 3 4 + 24.0 + $ python example10.py ad 1 2 3 4 # a mispelling error + usage: example10.py [-h] {add,mul} [n [n ...]] + example10.py: error: argument operator: invalid choice: 'ad' (choose from 'add', 'mul') + +``plac.call`` can also be used in doctests like this: + +>>> import plac, example10 +>>> plac.call(example10.main, ['add', '1', '2']) +3.0 + +``plac.call`` works for generators too: + +>>> def main(n): +... for i in range(int(n)): +... yield i +>>> plac.call(main, ['3']) +[0, 1, 2] + +Internally ``plac.call`` tries to convert the output of the main function +into a list, if possible. If the output is not iterable or it is a +string, it is left unchanged, but if it is iterable it is converted. +In particular, generator objects are exhausted by ``plac.call``. + +This behavior avoids mistakes like forgetting of applying +``list(result)`` to the result of ``plac.call``; moreover it makes +errors visible early, and avoids mistakes in code like the following:: + + try: + result = plac.call(main, args) + except: + # do something + +Without the "listify" functionality, a main function returning a +generator object would not raise any exception until the generator +is iterated over. + +If you are a fan of lazyness, you can still have it by setting the ``eager`` +flag to ``False``, as in the following example:: + + for line in plac.call(main, args, eager=False): + print(line) + +If ``main`` returns a generator object this example will print each +line as soon as available, whereas the default behaviour is to print +all the lines together and the end of the computation. + +A realistic example +--------------------------------------- + +Here is a more realistic script using most of the features of plac_ to +run SQL queries on a database by relying on SQLAlchemy_. Notice the usage +of the ``type`` feature to automagically convert a SQLAlchemy connection +string into a SqlSoup_ object: + +.. include:: dbcli.py + :literal: + +You can see the *yield-is-print* pattern here: instead of using +``print`` in the main function, I use ``yield``, and I perform the +print in the ``__main__`` block. The advantage of the pattern is that +tests invoking ``plac.call`` and checking the result become trivial: +had I performed the printing in the main function, the test would have +involved an ugly hack like redirecting ``sys.stdout`` to a +``StringIO`` object. + +Here is the usage message: + +.. include:: dbcli.help + :literal: + +You can check for yourself that the script works. + +Keyword arguments +--------------------------------------- + +Starting from release 0.4, plac_ supports keyword arguments. In +practice that means that if your main function has keyword arguments, +plac_ treats specially arguments of the form ``"name=value"`` in the +command line. Here is an example: + +.. include:: example12.py + :literal: + +Here is the generated usage message: + +.. include:: example12.help + :literal: + +Here is how you call the script:: + + $ python example12.py -o X a1 a2 name=value + opt=X + args=('a1', 'a2') + kw={'name': 'value'} + +When using keyword arguments, one must be careful to use names which +are not alreay taken; for instance in this examples the name ``opt`` +is taken:: + + $ python example12.py 1 2 kw1=1 kw2=2 opt=0 + usage: example12.py [-h] [-o OPT] [args [args ...]] [kw [kw ...]] + example12.py: error: colliding keyword arguments: opt + +The names taken are the names of the flags, of the options, and of the +positional arguments, excepted varargs and keywords. This limitation +is a consequence of the way the argument names are managed in function calls +by the Python language. + +Final example: a shelve interface +---------------------------------------------------------- + +Here is a less trivial example for the keyword arguments feature. +The use case is the following: suppose we have stored the +configuration parameters of a given application into a Python shelve +and we need a command-line tool to edit the shelve. +A possible implementation using plac_ could be the following: + +.. include:: ishelve.py + :literal: + +A few notes are in order: + +1. I have disabled the ordinary help provided by argparse_ and I have + implemented a custom help command. +2. I have changed the prefix character used to recognize the options + to a dot. +3. Keyword arguments recognition (in the ``**setters``) is used to make it + possible to store a value in the shelve with the syntax + ``param_name=param_value``. +4. ``*params`` are used to retrieve parameters from the shelve and some + error checking is performed in the case of missing parameters +5. A command to clear the shelve is implemented as a flag (``.clear``). +6. A command to delete a given parameter is implemented as an option + (``.delete``). +7. There is an option with default (``.filename=conf.shelve``) to store + the filename of the shelve. +8. All things considered, the code looks like a poor man object oriented + interface implemented with a chain of elifs instead of methods. Of course, + plac_ can do better than that, but let me start from a low-level approach + first. + +If you run ``ishelve.py`` without arguments you get the following +message:: + + $ python ishelve.py + no arguments passed, use .help to see the available commands + +If you run ``ishelve.py`` with the option ``.h`` (or any abbreviation +of ``.help``) you get:: + + $ python ishelve.py .h + Commands: .help, .showall, .clear, .delete + ... + ... + +You can check by hand that the tool work:: + + $ python ishelve.py .clear # start from an empty shelve + cleared the shelve + $ python ishelve.py a=1 b=2 + setting a=1 + setting b=2 + $ python ishelve.py .showall + b=2 + a=1 + $ python ishelve.py .del b # abbreviation for .delete + deleted b + $ python ishelve.py a + 1 + $ python ishelve.py b + b: not found + $ python ishelve.py .cler # mispelled command + usage: ishelve.py [.help] [.showall] [.clear] [.delete DELETE] + [.filename /home/micheles/conf.shelve] + [params [params ...]] [setters [setters ...]] + ishelve.py: error: unrecognized arguments: .cler + +plac vs argparse +--------------------------------------------- + +plac_ is opinionated and by design it does not try to make available +all of the features of argparse_ in an easy way. In particular you +should be aware of the following limitations/differences (the +following assumes knowledge of argparse_): + +- plac does not support the destination concept: the destination + coincides with the name of the argument, always. This restriction + has some drawbacks. For instance, suppose you want to define a long + option called ``--yield``. In this case the destination would be ``yield``, + which is a Python keyword, and since you cannot introduce an + argument with that name in a function definition, it is impossible + to implement it. Your choices are to change the name of the long + option, or to use argparse_ with a suitable destination. + +- plac_ does not support "required options". As the argparse_ + documentation puts it: *Required options are generally considered bad + form - normal users expect options to be optional. You should avoid + the use of required options whenever possible.* Notice that since + argparse_ supports them, plac_ can manage them too, but not directly. + +- plac_ supports only regular boolean flags. argparse_ has the ability to + define generalized two-value flags with values different from ``True`` + and ``False``. An earlier version of plac_ had this feature too, but + since you can use options with two choices instead, and in any case + the conversion from ``{True, False}`` to any couple of values + can be trivially implemented with a ternary operator + (``value1 if flag else value2``), I have removed it (KISS rules!). + +- plac_ does not support ``nargs`` options directly (it uses them internally, + though, to implement flag recognition). The reason it that all the use + cases of interest to me are covered by plac_ and did not feel the need + to increase the learning curve by adding direct support for ``nargs``. + +- plac_ does support subparsers, but you must read the `advanced usage + document`_ to see how it works. + +- plac_ does not support actions directly. This also + looks like a feature too advanced for the goals of plac_. Notice however + that the ability to define your own annotation objects (again, see + the `advanced usage document`_) may mitigate the need for custom actions. + +plac_ can leverage directly on many argparse_ features. + +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 + +disables the recognition of the help flag ``-h, --help``. This +mechanism does not look particularly elegant, but it works well +enough. I assume that 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='/-' + +The first prefix char (``/``) is used +as the default for the recognition of options and flags; +the second prefix char (``-``) is kept to keep the ``-h/--help`` option +working: however you can disable it and reimplement it, if you like, +as seen in the ``ishelve`` example. + +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)) #doctest: +ELLIPSIS +ArgumentParser(prog=...) + +Internally ``plac.call`` uses ``plac.parser_from`` and adds the parser +to the main function as an attribute. When ``plac.call(func)`` is +invoked multiple time, the parser is re-used and not rebuilt from scratch again. + +I use ``plac.parser_from`` in the unit tests of the module, but regular +users should not need to use it, unless they want to access *all* +of the features of argparse_ directly without calling the main function. + +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 except the addition of the ``.p`` +attribute. + +plac vs the rest of the world +------------------------------------------ + +Originally plac_ boasted about being "the easiest command-line +arguments parser in the world". Since then, people started pointing +out to me various projects which are based on the same idea +(extracting the parser from the main function signature) and are +arguably even easier than plac_: + +- opterator_ by Dusty Phillips +- CLIArgs_ by Pavel Panchekha + +Luckily for me none of such projects had the idea of using +function annotations and argparse_; as a consequence, they are +no match for the capabilities of plac_. + +Of course, there are tons of other libraries to parse the command +line. For instance Clap_ by Matthew Frazier which appeared on PyPI +just the day before plac_; Clap_ is fine but it is certainly not +easier than plac_. + +plac_ can also be used as a replacement of the cmd_ module in the standard +library and as such it shares many features with the module cmd2_ by +Catherine Devlin. However, this is completely coincidental, since I became +aware of the cmd2_ module only after writing plac_. + +The future +------------------------------- + +Currently the core of plac_ is around 200 lines of code, not counting blanks, +comments and docstrings. I do not plan to extend the core much in the +future. The idea is to keep the module short: it is and it should +remain a little wrapper over argparse_. Actually I have thought about +contributing the core back to argparse_ if plac_ becomes successfull +and gains a reasonable number of users. For the moment it should be +considered in alpha status. + +Notice that even if plac_ has been designed to be simple to use for +simple stuff, its power should not be underestimated; it is actually a +quite advanced tool with a domain of applicability which far exceeds +the realm of command-line arguments parsers. + +Version 0.5 of plac_ doubled the code base and the documentation: it is +based on the idea of using plac_ to implement command-line interpreters, +i.e. something akin to the ``cmd`` module in the standard library, only better. +The new features of plac_ are described in the `advanced usage document`_ . +They are implemented in a separated module (``plac_ext.py``), since +they require Python 2.5 to work, whereas ``plac_core.py`` only requires +Python 2.3. + +Trivia: the story behind the name +----------------------------------------- + +The plac_ project started very humbly: I just wanted to make +easy_installable my old optionparse_ recipe, and to publish it on PyPI. +The original name of plac_ was optionparser and the idea behind it was +to build an OptionParser_ object from the docstring of the module. +However, before doing that, I decided to check out the argparse_ module, +since I knew it was going into Python 2.7 and Python 2.7 was coming out. +Soon enough I realized two things: + +1. the single greatest idea of argparse_ was unifying the positional arguments + and the options in a single namespace object; +2. parsing the docstring was so old-fashioned, considering the existence + of functions annotations in Python 3. + +Putting together these two observations with the original idea of inferring the +parser I decided to build an ArgumentParser_ object from function +annotations. The ``optionparser`` name was ruled out, since I was +now using argparse_; a name like ``argparse_plus`` was also ruled out, +since the typical usage was completely different from the argparse_ usage. + +I made a research on PyPI and the name *clap* (Command Line Arguments Parser) +was not taken, so I renamed everything to clap. After two days +a Clap_ module appeared on PyPI ! + +Having little imagination, I decided to rename everything again to plac, +an anagram of clap: since it is a non-existing English name, I hope nobody +will steal it from me! + +That's all, I hope you will enjoy working with plac_! + +.. _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 +.. _cmd2: http://packages.python.org/cmd2/ +.. _cmd: http://docs.python.org/library/cmd.html diff --git a/doc/plac_real_life_example.py b/doc/plac_real_life_example.py new file mode 100644 index 0000000..945561f --- /dev/null +++ b/doc/plac_real_life_example.py @@ -0,0 +1,24 @@ +# literal translation from http://groups.google.com/group/comp.lang.python/msg/de7c188e705f8eb2?hl=en +# parser = optparse.OptionParser("usage: %lines [options] arg1") +# parser.add_option("-l", "--lines", dest="lines", +# default=10, type="int", +# help="number of lines") +# parser.add_option("-t", "--topbottom", dest="topbottom", +# default="T", type="str", +# help="T(op) or B(ottom)") +# (options, args) = parser.parse_args() +# if len(args) != 1: +# parser.error("incorrect number of arguments") +# lines=options.lines +# tb=options.topbottom + +import plac + +@plac.annotations( + lines=('number of lines', 'option', 'l', int), + topbottom=('T(op) or B(ottom)', 'option', 't', str, 'TB')) +def main(arg, lines=10, topbottom='T'): + print arg, lines, topbottom + +if __name__ == '__main__': + plac.call(main) diff --git a/doc/read_stdin.py b/doc/read_stdin.py new file mode 100644 index 0000000..584644f --- /dev/null +++ b/doc/read_stdin.py @@ -0,0 +1,13 @@ +""" +You can run this script as +$ python read_stdin.py < ishelve.bat +""" +from __future__ import with_statement +import sys +from ishelve import ishelve +import plac + +if __name__ == '__main__': + with plac.Interpreter(ishelve) as i: + for line in sys.stdin: + print(i.send(line)) diff --git a/doc/server_ex.py b/doc/server_ex.py new file mode 100644 index 0000000..e3d76ec --- /dev/null +++ b/doc/server_ex.py @@ -0,0 +1,9 @@ +import plac +from importer2 import FakeImporter + +def main(port=2199): + main = FakeImporter('dsn') + plac.Interpreter(main).start_server(port) + +if __name__ == '__main__': + plac.call(main) diff --git a/doc/shelve_interpreter.help b/doc/shelve_interpreter.help new file mode 100644 index 0000000..f666445 --- /dev/null +++ b/doc/shelve_interpreter.help @@ -0,0 +1,13 @@ +usage: shelve_interpreter.py [-h] [-interactive] + [subcommands [subcommands ...]] + + This script works both interactively and non-interactively. + Use .help to see the internal commands. + + +positional arguments: + subcommands the commands of the underlying ishelve interpreter + +optional arguments: + -h, --help show this help message and exit + -interactive start interactive interface diff --git a/doc/shelve_interpreter.py b/doc/shelve_interpreter.py new file mode 100644 index 0000000..b7fb688 --- /dev/null +++ b/doc/shelve_interpreter.py @@ -0,0 +1,19 @@ +# shelve_interpreter.py +import plac, ishelve + +@plac.annotations( + interactive=('start interactive interface', 'flag'), + subcommands='the commands of the underlying ishelve interpreter') +def main(interactive, *subcommands): + """ + This script works both interactively and non-interactively. + Use .help to see the internal commands. + """ + if interactive: + plac.Interpreter(ishelve.main).interact() + else: + for out in plac.call(ishelve.main, subcommands): + print(out) + +if __name__ == '__main__': + plac.call(main) diff --git a/doc/sql_interface.py b/doc/sql_interface.py new file mode 100644 index 0000000..34042dd --- /dev/null +++ b/doc/sql_interface.py @@ -0,0 +1,28 @@ +import os, plac +from sqlalchemy.ext.sqlsoup import SqlSoup + +SQLKEYWORDS = set(['select', 'from', 'inner', 'join', 'outer', 'left', 'right'] + ) # and many others +DBTABLES = set(['table1', 'table2']) # you can read them from the db schema + +COMPLETIONS = SQLKEYWORDS | DBTABLES + +class SqlInterface(object): + commands = ['SELECT'] + def __init__(self, dsn): + self.soup = SqlSoup(dsn) + def SELECT(self, argstring): + sql = 'SELECT ' + argstring + for row in self.soup.bind.execute(sql): + yield str(row) # the formatting can be much improved + +rl_input = plac.ReadlineInput( + COMPLETIONS, histfile=os.path.expanduser('~/.sql_interface.history'), + case_sensitive=False) + +def split_on_first_space(line, commentchar): + return line.strip().split(' ', 1) # ignoring comments + +if __name__ == '__main__': + plac.Interpreter.call(SqlInterface, split=split_on_first_space, + stdin=rl_input, prompt='sql> ') diff --git a/doc/syncproc.py b/doc/syncproc.py new file mode 100644 index 0000000..1770af1 --- /dev/null +++ b/doc/syncproc.py @@ -0,0 +1,11 @@ +from __future__ import with_statement +import plac, plac_ext + +def main(*args): + yield args + +if __name__ == '__main__': + proc = plac_ext.SyncProcess(['syncproc.py']) + while True: + inp = raw_input(proc.recv()) + proc.send(inp) diff --git a/doc/test_ishelve.py b/doc/test_ishelve.py new file mode 100644 index 0000000..a34809d --- /dev/null +++ b/doc/test_ishelve.py @@ -0,0 +1,12 @@ +# test_ishelve.py +import plac, ishelve + +def test(): + assert plac.call(ishelve.main, ['.clear']) == ['cleared the shelve'] + assert plac.call(ishelve.main, ['a=1']) == ['setting a=1'] + assert plac.call(ishelve.main, ['a']) == ['1'] + assert plac.call(ishelve.main, ['.delete=a']) == ['deleted a'] + assert plac.call(ishelve.main, ['a']) == ['a: not found'] + +if __name__ == '__main__': + test() diff --git a/doc/test_ishelve_more.py b/doc/test_ishelve_more.py new file mode 100644 index 0000000..234ce0a --- /dev/null +++ b/doc/test_ishelve_more.py @@ -0,0 +1,11 @@ +# test_ishelve_more.py +from __future__ import with_statement +import plac, ishelve + +def test(): + with plac.Interpreter(ishelve.main) as i: + i.check('.clear', 'cleared the shelve') + i.check('a=1', 'setting a=1') + i.check('a', '1') + i.check('.delete=a', 'deleted a') + i.check('a', 'a: not found') diff --git a/doc/test_plac.py b/doc/test_plac.py new file mode 100644 index 0000000..df6c060 --- /dev/null +++ b/doc/test_plac.py @@ -0,0 +1,223 @@ +""" +The tests are runnable with nose, with py.test, or even as standalone script +""" + +import os, sys, doctest, subprocess +import plac + +sys_argv0 = sys.argv[0] +os.chdir(os.path.dirname(__file__) or '.') # work in the current directory + +######################## helpers ####################### + +def expect(errclass, func, *args, **kw): + try: + func(*args, **kw) + except errclass: + pass + else: + raise RuntimeError('%s expected, got none!', errclass.__name__) + +def parser_from(f, **kw): + f.__annotations__ = kw + return plac.parser_from(f) + +def check_help(name): + sys.argv[0] = name + '.py' # avoid issue with nosetests + plac.parser_registry.clear() # makes different imports independent + try: + try: + main = plac.import_main(name + '.py') + except SyntaxError: + if sys.version < '3': # expected for Python 2.X + return + else: # not expected for Python 3.X + raise + p = plac.parser_from(main) + expected = open(name + '.help').read().strip() + got = p.format_help().strip() + assert got == expected, got + finally: + sys.argv[0] = sys_argv0 + +####################### tests ############################ + +def test_expected_help(): + for fname in os.listdir('.'): + if fname.endswith('.help'): + name = fname[:-5] + if name not in ('vcs', 'ishelve'): + yield check_help, fname[:-5] + +p1 = parser_from(lambda delete, *args: None, + delete=('delete a file', 'option')) + +def test_p1(): + arg = p1.parse_args(['-d', 'foo', 'arg1', 'arg2']) + assert arg.delete == 'foo' + assert arg.args == ['arg1', 'arg2'] + + arg = p1.parse_args([]) + assert arg.delete is None, arg.delete + assert arg.args == [], arg.args + +p2 = parser_from(lambda arg1, delete, *args: None, + delete=('delete a file', 'option', 'd')) + +def test_p2(): + arg = p2.parse_args(['-d', 'foo', 'arg1', 'arg2']) + assert arg.delete == 'foo', arg.delete + assert arg.arg1 == 'arg1', arg.arg1 + assert arg.args == ['arg2'], arg.args + + arg = p2.parse_args(['arg1']) + assert arg.delete is None, arg.delete + assert arg.args == [], arg.args + assert arg, arg + + expect(SystemExit, p2.parse_args, []) + +p3 = parser_from(lambda arg1, delete: None, + delete=('delete a file', 'option', 'd')) + +def test_p3(): + arg = p3.parse_args(['arg1']) + assert arg.delete is None, arg.delete + assert arg.arg1 == 'arg1', arg.args + + expect(SystemExit, p3.parse_args, ['arg1', 'arg2']) + expect(SystemExit, p3.parse_args, []) + +p4 = parser_from(lambda delete, delete_all, color="black": None, + delete=('delete a file', 'option', 'd'), + delete_all=('delete all files', 'flag', 'a'), + color=('color', 'option', 'c')) + +def test_p4(): + arg = p4.parse_args(['-a']) + assert arg.delete_all is True, arg.delete_all + + arg = p4.parse_args([]) + + arg = p4.parse_args(['--color=black']) + assert arg.color == 'black' + + arg = p4.parse_args(['--color=red']) + assert arg.color == 'red' + +def test_flag_with_default(): + expect(TypeError, parser_from, lambda yes_or_no='no': None, + yes_or_no=('A yes/no flag', 'flag', 'f')) + +def assert_usage(parser, expected): + usage = parser.format_usage() + assert usage == expected, usage + +def test_metavar_no_defaults(): + sys.argv[0] = 'test_plac.py' + + # positional + p = parser_from(lambda x: None, + x=('first argument', 'positional', None, str, [], 'METAVAR')) + assert_usage(p, 'usage: test_plac.py [-h] METAVAR\n') + + # option + p = parser_from(lambda x: None, + x=('first argument', 'option', None, str, [], 'METAVAR')) + assert_usage(p, 'usage: test_plac.py [-h] [-x METAVAR]\n') + sys.argv[0] = sys_argv0 + +def test_metavar_with_defaults(): + sys.argv[0] = 'test_plac.py' + + # positional + p = parser_from(lambda x='a': None, + x=('first argument', 'positional', None, str, [], 'METAVAR')) + assert_usage(p, 'usage: test_plac.py [-h] [METAVAR]\n') + + # option + p = parser_from(lambda x='a': None, + x=('first argument', 'option', None, str, [], 'METAVAR')) + assert_usage(p, 'usage: test_plac.py [-h] [-x METAVAR]\n') + + p = parser_from(lambda x='a': None, + x=('first argument', 'option', None, str, [])) + assert_usage(p, 'usage: test_plac.py [-h] [-x a]\n') + + sys.argv[0] = sys_argv0 + +def test_kwargs(): + def main(opt, arg1, *args, **kw): + print(opt, arg1) + return args, kw + main.__annotations__ = dict(opt=('Option', 'option')) + argskw = plac.call(main, ['arg1', 'arg2', 'a=1', 'b=2']) + assert argskw == [('arg2',), {'a': '1', 'b': '2'}], 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']) + +class Cmds(object): + add_help = False + commands = 'help', 'commit' + def help(self, name): + return 'help', name + def commit(self): + return 'commit' + +cmds = Cmds() + +def test_cmds(): + assert 'commit' == plac.call(cmds, ['commit']) + assert ['help', 'foo'] == plac.call(cmds, ['help', 'foo']) + expect(SystemExit, plac.call, cmds, []) + +def test_cmd_abbrevs(): + assert 'commit' == plac.call(cmds, ['comm']) + assert ['help', 'foo'] == plac.call(cmds, ['h', 'foo']) + expect(SystemExit, plac.call, cmds, ['foo']) + +def test_yield(): + def main(): + for i in (1, 2, 3): + yield i + assert plac.call(main, []) == [1, 2, 3] + +def test_doctest(): + failure, tot= doctest.testfile('plac.txt', module_relative=False) + assert not failure, failure + +failing_scripts = set(['ishelve2.plac']) + +def check_script(args): + if failing_scripts.intersection(args): + assert subprocess.call(args) > 0, ( # expected failure + 'Unexpected success for %s' % ' '.join(args)) + else: + assert subprocess.call(args) == 0 , 'Failed %s' % ' '.join(args) + +def test_batch(): + for batch in os.listdir('.'): + if batch.endswith('.plac'): + yield check_script, ['plac_runner.py', '-b', batch] + +def test_placet(): + for placet in os.listdir('.'): + if placet.endswith('.placet'): + yield check_script, ['plac_runner.py', '-t', placet] + +if __name__ == '__main__': + n = 0 + for name, test in sorted(globals().items()): + if name.startswith('test_'): + print('Running ' + name) + maybegen = test() + if hasattr(maybegen, '__iter__'): + for func, arg in maybegen: + func(arg) + n += 1 + else: + n +=1 + print('Executed %d tests OK' % n) diff --git a/doc/test_server.py b/doc/test_server.py new file mode 100644 index 0000000..a9904ee --- /dev/null +++ b/doc/test_server.py @@ -0,0 +1,35 @@ +import multiprocessing, subprocess, time, random +import plac +from ishelve2 import ShelveInterface + +i = plac.Interpreter(ShelveInterface(configfile=None)) + +COMMANDS = ['''\ +.help +set a 1 +''', +'''\ +set b 1 +wrong command +showall +'''] + +def client_send(commands, port): + time.sleep(.5) # wait a bit for the server to start + po = subprocess.Popen(['telnet', 'localhost', str(port)], + stdin=subprocess.PIPE) + for cmd in commands.splitlines(): + po.stdin.write(cmd + '\n') + time.sleep(.1) # wait a bit for the server to answer + +def test(): + port = random.choice(range(2000, 20000)) + clients = [] + for cmds in COMMANDS: + cl = multiprocessing.Process(target=client_send, args=(cmds, port)) + clients.append(cl) + cl.start() + i.stop_server(wait=1) + i.start_server(port, timeout=.1) + for cl in clients: + cl.join() diff --git a/doc/thread_ex.py b/doc/thread_ex.py new file mode 100644 index 0000000..72c1853 --- /dev/null +++ b/doc/thread_ex.py @@ -0,0 +1,40 @@ +import threading, Queue, time, random, Tkinter +import plac + +queue = Queue.Queue() + +def make_target(i): + def thunk(): + time.sleep(random.random()) + queue.put(str(i)) + return thunk + +def launch_threads(n): + for i in range(n): + th = threading.Thread(target=make_target(i)) + th.start() + yield th + +class ThreadContext(object): + def __init__(self, func, *args, **kw): + self.thread = threading.Thread(None, func, args=args, kwargs=kw) + def __enter__(self): + self.thread.start() + return self + def __exit__(self, etype, exc, tb): + self.thread.join() + +def interpret_queue(interpreter, queue): + with interpreter: + for value in iter(queue.get, 'exit'): + print(interpreter.send(value)) + +if __name__ == '__main__': + thlist = list(launch_threads(10)) + i = plac.Interpreter(lambda x: x) + root = Tkinter.Tk() + threading.Timer(3, lambda : [queue.put('exit'), root.quit()]).start() + with ThreadContext(interpret_queue, i, queue): + root.mainloop() + for th in thlist: + th.join() diff --git a/doc/vcs.help b/doc/vcs.help new file mode 100644 index 0000000..4af805f --- /dev/null +++ b/doc/vcs.help @@ -0,0 +1,10 @@ +usage: plac_runner.py 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/doc/vcs.py b/doc/vcs.py new file mode 100644 index 0000000..c423fb0 --- /dev/null +++ b/doc/vcs.py @@ -0,0 +1,30 @@ +"A Fake Version Control System" + +import plac + +commands = 'checkout', 'commit', 'status' + +@plac.annotations(url='url of the source code') +def checkout(url): + "A fake checkout command" + return ('checkout ', url) + +@plac.annotations(message=('commit message', 'option')) +def commit(message): + "A fake commit command" + return ('commit ', message) + +@plac.annotations(quiet=('summary information', 'flag', 'q')) +def status(quiet): + "A fake status command" + return ('status ', quiet) + +def __missing__(name): + return 'Command %r does not exist' % name + +def __exit__(etype, exc, tb): + "Will be called automatically at the end of the call/cmdloop" + if etype in (None, GeneratorExit): # success + print('ok') + +main = __import__(__name__) # the module imports itself! diff --git a/doc/x.py b/doc/x.py new file mode 100644 index 0000000..08c4870 --- /dev/null +++ b/doc/x.py @@ -0,0 +1,7 @@ +class Main(object): + commands = ['do'] + def do(self, x): + pass + +if __name__ == '__main__': + import plac; plac.Interpreter.call(Main) diff --git a/doc/x.txt b/doc/x.txt new file mode 100644 index 0000000..39dcd7f --- /dev/null +++ b/doc/x.txt @@ -0,0 +1,406 @@ +Traceback (most recent call last): + File "importer3.py", line 20, in + plac.Interpreter(plac.call(FakeImporter)).interact() + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 775, in interact + self._manage_input() + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 785, in _manage_input + write(str(task) + '\n') + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) + File "/usr/local/lib/python2.6/dist-packages/plac-0.7.0-py2.6.egg/plac_ext.py", line 266, in __str__ + return '\n'.join(map(str, self.outlist)) +RuntimeError: maximum recursion depth exceeded while calling a Python object diff --git a/plac.py b/plac.py new file mode 100644 index 0000000..211e43d --- /dev/null +++ b/plac.py @@ -0,0 +1,35 @@ +########################## LICENCE ############################### +## +## Copyright (c) 2010, Michele Simionato +## All rights reserved. +## +## Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## Redistributions in bytecode form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. + +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +## OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +## TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +## USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +## DAMAGE. + +""" +See doc/plac.pdf, doc/plac_adv.pdf for the documentation. +""" + +__version__ = '0.7.4' + +from plac_core import * + +if sys.version >= '2.5': + from plac_ext import Interpreter, import_main, ReadlineInput, stdout, runp diff --git a/plac_core.py b/plac_core.py new file mode 100644 index 0000000..fbd63b8 --- /dev/null +++ b/plac_core.py @@ -0,0 +1,303 @@ +# 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 +from gettext import gettext as _ + +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, basestring): + 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__, + formatter_class=argparse.RawDescriptionHelpFormatter) + for name in dir(obj): + if name in PARSER_CFG: # argument of ArgumentParser + cfg[name] = getattr(obj, name) + return cfg + +parser_registry = {} + +def parser_from(obj, **confparams): + """ + obj can be a callable or an object with a .commands attribute. + Returns an ArgumentParser. + """ + try: # the underlying parser has been generated already + return parser_registry[obj] + except KeyError: # generate a new parser + pass + conf = pconf(obj).copy() + conf.update(confparams) + parser_registry[obj] = parser = ArgumentParser(**conf) + parser.obj = obj + parser.case_sensitive = confparams.get( + 'case_sensitive', getattr(obj, 'case_sensitive', True)) + if hasattr(obj, 'commands') and obj.commands and not inspect.isclass(obj): + # a command container instance + parser.addsubcommands(obj.commands, obj, 'subcommands') + else: + parser.populate_from(obj) + return parser + +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 _match_cmd(abbrev, commands, case_sensitive=True): + "Extract the command name from an abbreviation or raise a NameError" + if not case_sensitive: + abbrev = abbrev.upper(); commands = [c.upper() for c in commands] + perfect_matches = [name for name in commands if name == abbrev] + if len(perfect_matches) == 1: + return perfect_matches[0] + matches = [name for name in commands if name.startswith(abbrev)] + n = len(matches) + if n == 1: + return matches[0] + elif n > 1: + raise NameError( + _('Ambiguous command %r: matching %s' % (abbrev, matches))) + +class ArgumentParser(argparse.ArgumentParser): + """ + An ArgumentParser with .func and .argspec attributes, and possibly + .commands and .subparsers. + """ + case_sensitive = True + + def consume(self, args): + """Call the underlying function with the args. Works also for + command containers, by dispatching to the right subparser.""" + arglist = list(args) + cmd = None + if hasattr(self, 'subparsers'): + subp, cmd = self._extract_subparser_cmd(arglist) + if subp is None and cmd is not None: + return cmd, self.missing(cmd) + elif subp is not None: # use the subparser + self = subp + if hasattr(self, 'argspec') and self.argspec.varkw: + arglist, kwargs = _extract_kwargs(arglist) # modify arglist! + else: + kwargs = {} + if hasattr(self, 'argspec') and self.argspec.varargs: + # ignore unrecognized arguments + ns, extraopts = self.parse_known_args(arglist) + else: + ns, extraopts = self.parse_args(arglist), [] # may raise an exit + args = [getattr(ns, a) for a in self.argspec.args] + varargs = getattr(ns, self.argspec.varargs or '', []) + collision = set(self.argspec.args) & set(kwargs) + if collision: + self.error( + _('colliding keyword arguments: %s') % ' '.join(collision)) + return cmd, self.func(*(args + varargs + extraopts), **kwargs) + + def _extract_subparser_cmd(self, arglist): + "Extract the right subparser from the first recognized argument" + optprefix = self.prefix_chars[0] + name_parser_map = self.subparsers._name_parser_map + for i, arg in enumerate(arglist): + if not arg.startswith(optprefix): + cmd = _match_cmd(arg, name_parser_map, self.case_sensitive) + del arglist[i] + return name_parser_map.get(cmd), cmd or arg + return None, None + + def addsubcommands(self, commands, obj, title=None, cmdprefix=''): + "Extract a list of subcommands from obj and add them to the parser" + if hasattr(obj, cmdprefix) and obj.cmdprefix in self.prefix_chars: + raise ValueError(_('The prefix %r is already taken!' % cmdprefix)) + if not hasattr(self, 'subparsers'): + self.subparsers = self.add_subparsers(title=title) + elif title: + self.add_argument_group(title=title) # populate ._action_groups + prefixlen = len(getattr(obj, 'cmdprefix', '')) + for cmd in commands: + func = getattr(obj, cmd[prefixlen:]) # strip the prefix + self.subparsers.add_parser( + cmd, add_help=False, **pconf(func)).populate_from(func) + + def _set_func_argspec(self, obj): + """Extracts the signature from a callable object and adds an .argspec + attribute to the parser. Also adds a .func reference to the object.""" + if inspect.isfunction(obj): + self.argspec = getfullargspec(obj) + elif inspect.ismethod(obj): + self.argspec = getfullargspec(obj) + del self.argspec.args[0] # remove first argument + elif inspect.isclass(obj): + if obj.__init__ is object.__init__: # to avoid an error + self.argspec = getfullargspec(lambda self: None) + else: + self.argspec = getfullargspec(obj.__init__) + del self.argspec.args[0] # remove first argument + elif hasattr(obj, '__call__'): + self.argspec = getfullargspec(obj.__call__) + del self.argspec.args[0] # remove first argument + else: + raise TypeError(_('Could not determine the signature of %r') % obj) + self.func = obj + parser_registry[obj] = self + + def populate_from(self, func): + """ + Extract the arguments from the attributes of the passed function + and return a populated ArgumentParser instance. + """ + self._set_func_argspec(func) + f = self.argspec + defaults = f.defaults or () + n_args = len(f.args) + n_defaults = len(defaults) + alldefaults = (NONE,) * (n_args - n_defaults) + defaults + prefix = self.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 + self.add_argument(name, help=a.help, type=a.type, + choices=a.choices, metavar=metavar) + else: # default argument + self.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) + self.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)) + self.add_argument(action='store_true', help=a.help, *shortlong) + if f.varargs: + a = Annotation.from_(f.annotations.get(f.varargs, ())) + self.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, ())) + self.add_argument(f.varkw, nargs='*', help=a.help, default={}, + type=a.type, metavar=a.metavar) + + def missing(self, name): + miss = getattr(self.obj, '__missing__', lambda name: + self.error('No command %r' % name)) + return miss(name) + + def help_cmd(self, cmd): + "Return the help message for a subcommand" + p = self.subparsers._name_parser_map.get(cmd) + if p is None: + return _('Unknown command %s' % cmd) + else: + return p.format_help() + + def print_actions(self): + "Useful for debugging" + print(self) + for a in self._actions: + print(a) + +def iterable(obj): + "Any object with an __iter__ method which is not a string" + return hasattr(obj, '__iter__') and not isinstance(obj, basestring) + +def call(obj, arglist=sys.argv[1:], eager=True): + """ + If obj is a function or a bound method, parse the given arglist + by using the parser inferred from the annotations of obj + and call obj with the parsed arguments. + If obj is an object with attribute .commands, dispatch to the + associated subparser. + """ + cmd, result = parser_from(obj).consume(arglist) + if iterable(result) and eager: # listify the result + return list(result) + return result diff --git a/plac_ext.py b/plac_ext.py new file mode 100644 index 0000000..1920f52 --- /dev/null +++ b/plac_ext.py @@ -0,0 +1,892 @@ +# this module requires Python 2.5+ +from __future__ import with_statement +from contextlib import contextmanager +from operator import attrgetter +from gettext import gettext as _ +import imp, inspect, os, sys, cmd, shlex, subprocess +import itertools, traceback, time, select, multiprocessing, signal, threading +import plac_core +try: + import readline +except ImportError: + readline = False + +############################# generic utils ################################ + +@contextmanager +def stdout(fileobj): + "usage: with stdout(file('out.txt', 'a')): do_something()" + orig_stdout = sys.stdout + sys.stdout = fileobj + try: + yield + finally: + sys.stdout = orig_stdout + +def write(x): + "Write str(x) on stdout and flush, no newline added" + sys.stdout.write(str(x)) + sys.stdout.flush() + +def gen_val(value): + "Return a generator object with a single element" + yield value + +def gen_exc(etype, exc, tb): + "Return a generator object raising an exception" + raise etype, exc, tb + yield + +def less(text): + "Send a text to less via a pipe" + # -c clear the screen before starting less + po = subprocess.Popen(['less', '-c'], stdin=subprocess.PIPE) + try: + po.stdin.write(text) + except IOError: + pass + po.stdin.close() + po.wait() + +use_less = (sys.platform != 'win32') # unices + +class TerminatedProcess(Exception): + pass + +def terminatedProcess(signum, frame): + raise TerminatedProcess + +########################### readline support ############################# + +def read_line(stdin, prompt=''): + "Read a line from stdin, using readline when possible" + if isinstance(stdin, ReadlineInput): + return stdin.readline(prompt) + else: + write(prompt) + return stdin.readline() + +def read_long_line(stdin, terminator): + """ + Read multiple lines from stdin until the terminator character is found, then + yield a single space-separated long line. + """ + while True: + lines = [] + while True: + line = stdin.readline() # ends with \n + if not line: # EOF + return + line = line.strip() + if not line: + continue + elif line[-1] == terminator: + lines.append(line[:-1]) + break + else: + lines.append(line) + yield ' '.join(lines) + +class ReadlineInput(object): + """ + An iterable with a .readline method reading from stdin. + """ + def __init__(self, completions, case_sensitive=True, histfile=None): + self.completions = completions + self.case_sensitive = case_sensitive + self.histfile = histfile + if not case_sensitive: + self.completions = map(str.upper, completions) + readline.parse_and_bind("tab: complete") + readline.set_completer(self.complete) + + def __enter__(self): + self.old_completer = readline.get_completer() + try: + if self.histfile: + readline.read_history_file(self.histfile) + except IOError: # the first time + pass + return self + + def __exit__(self, etype, exc, tb): + readline.set_completer(self.old_completer) + if self.histfile: + readline.write_history_file(self.histfile) + + def complete(self, kw, state): + # state is 0, 1, 2, ... and increases by hitting TAB + if not self.case_sensitive: + kw = kw.upper() + try: + return [k for k in self.completions if k.startswith(kw)][state] + except IndexError: # no completions + return # exit + + def readline(self, prompt=''): + try: + return raw_input(prompt) + '\n' + except EOFError: + return '' + + def __iter__(self): + return iter(self.readline, '') + +########################### import management ################################ + +try: + PLACDIRS = os.environ.get('PLACPATH', '.').split(':') +except: + raise ValueError(_('Ill-formed PLACPATH: got %PLACPATHs') % os.environ) + +def partial_call(factory, arglist): + "Call a container factory with the arglist and return a plac object" + + a = plac_core.parser_from(factory).argspec + if a.defaults or a.varargs or a.varkw: + raise TypeError('Interpreter.call must be invoked on ' + 'factories with required arguments only') + required_args = ', '.join(a.args) + if required_args: + required_args += ',' # trailing comma + code = '''def makeobj(interact, %s *args): + obj = factory(%s) + obj._interact_ = interact + obj._args_ = args + return obj\n'''% (required_args, required_args) + dic = dict(factory=factory) + exec code in dic + makeobj = dic['makeobj'] + if inspect.isclass(factory): + makeobj.__annotations__ = getattr( + factory.__init__, '__annotations__', {}) + else: + makeobj.__annotations__ = getattr( + factory, '__annotations__', {}) + makeobj.__annotations__['interact'] = ( + 'start interactive interpreter', 'flag', 'i') + return plac_core.call(makeobj, arglist) + +def import_main(path, *args, **pconf): + """ + An utility to import the main function of a plac tool. It also + works with command container factories. + """ + if ':' in path: # importing a factory + path, factory_name = path.split(':') + else: # importing the main function + factory_name = None + if not os.path.isabs(path): # relative path, look at PLACDIRS + for placdir in PLACDIRS: + fullpath = os.path.join(placdir, path) + if os.path.exists(fullpath): + break + else: # no break + raise ImportError(_('Cannot find %s' % path)) + else: + fullpath = path + name, ext = os.path.splitext(os.path.basename(fullpath)) + module = imp.load_module(name, open(fullpath), fullpath, (ext, 'U', 1)) + if factory_name: + tool = partial_call(getattr(module, factory_name), args) + else: + tool = module.main + # set the parser configuration + plac_core.parser_from(tool, **pconf) + return tool + +############################## Task classes ############################## + +# base class not instantiated directly +class BaseTask(object): + """ + A task is a wrapper over a generator object with signature + Task(no, arglist, genobj), attributes + .no + .arglist + .outlist + .str + .etype + .exc + .tb + .status + and methods .run and .kill. + """ + STATES = ('SUBMITTED', 'RUNNING', 'TOBEKILLED', 'KILLED', 'FINISHED', + 'ABORTED') + + def __init__(self, no, arglist, genobj): + self.no = no + self.arglist = arglist + self._genobj = self._wrap(genobj) + self.str, self.etype, self.exc, self.tb = '', None, None, None + self.status = 'SUBMITTED' + self.outlist = [] + + def _wrap(self, genobj, stringify_tb=False): + """ + Wrap the genobj into a generator managing the exceptions, + populating the .outlist, setting the .status and yielding None. + stringify_tb must be True if the traceback must be sent to a process. + """ + self.status = 'RUNNING' + try: + for value in genobj: + if self.status == 'TOBEKILLED': # exit from the loop + raise GeneratorExit + if value is not None: # add output + self.outlist.append(value) + yield + except (GeneratorExit, TerminatedProcess, KeyboardInterrupt): + # soft termination + self.status = 'KILLED' + except: # unexpected exception + self.etype, self.exc, tb = sys.exc_info() + self.tb = self.traceback if stringify_tb else tb + self.status = 'ABORTED' + else: # regular exit + self.status = 'FINISHED' + try: + self.str = '\n'.join(map(str, self.outlist)) + except IndexError: + self.str = 'no result' + + def run(self): + "Run the inner generator" + for none in self._genobj: + pass + + def kill(self): + "Set a TOBEKILLED status" + self.status = 'TOBEKILLED' + + def wait(self): + "Wait for the task to finish: to be overridden" + + @property + def traceback(self): + "Return the traceback as a (possibly empty) string" + if self.tb is None: + return '' + elif isinstance(self.tb, basestring): + return self.tb + else: + return ''.join(traceback.format_tb(self.tb)) + + @property + def result(self): + self.wait() + if self.exc: + raise self.etype, self.exc, self.tb or None + if not self.outlist: + return None + return self.outlist[-1] + + def __repr__(self): + "String representation containing class name, number, arglist, status" + return '<%s %d [%s] %s>' % ( + self.__class__.__name__, self.no, + ' '.join(self.arglist), self.status) + +nulltask = BaseTask(0, [], ('skip' for dummy in (1,))) + +########################## synchronous tasks ############################### + +class SynTask(BaseTask): + """ + Synchronous task running in the interpreter loop and displaying its + output as soon as available. + """ + def __str__(self): + "Return the output string or the error message" + if self.etype: # there was an error + return '%s: %s' % (self.etype.__name__, self.exc) + else: + return '\n'.join(map(str, self.outlist)) + +class ThreadedTask(BaseTask): + """ + A task running in a separated thread. + """ + def __init__(self, no, arglist, genobj): + BaseTask.__init__(self, no, arglist, genobj) + self.thread = threading.Thread(target=super(ThreadedTask, self).run) + + def run(self): + "Run the task into a thread" + self.thread.start() + + def wait(self): + "Block until the thread ends" + self.thread.join() + +######################### multiprocessing tasks ########################## + +def sharedattr(name, on_error): + "Return a property to be attached to an MPTask" + def get(self): + try: + return getattr(self.ns, name) + except: # the process was killed or died hard + return on_error + def set(self, value): + try: + setattr(self.ns, name, value) + except: # the process was killed or died hard + pass + return property(get, set) + +class MPTask(BaseTask): + """ + A task running as an external process. The current implementation + only works on Unix-like systems, where multiprocessing use forks. + """ + str = sharedattr('str', '') + etype = sharedattr('etype', None) + exc = sharedattr('exc', None) + tb = sharedattr('tb', None) + status = sharedattr('status', 'ABORTED') + + @property + def outlist(self): + try: + return self._outlist + except: # the process died hard + return [] + + def __init__(self, no, arglist, genobj, mp_manager): + self.no = no + self.arglist = arglist + self._genobj = self._wrap(genobj, stringify_tb=True) + self.mp_manager = mp_manager + self._outlist = self.mp_manager.list() + self.ns = self.mp_manager.Namespace() + self.status = 'SUBMITTED' + self.etype, self.exc, self.tb = None, None, None + self.str = repr(self) + self.proc = multiprocessing.Process(target=super(MPTask, self).run) + + def run(self): + "Run the task into an external process" + self.proc.start() + + def wait(self): + "Block until the external process ends or is killed" + self.proc.join() + + def kill(self): + """Kill the process with a SIGTERM inducing a TerminatedProcess + exception in the children""" + self.proc.terminate() + +######################### Task Manager ####################### + +class HelpSummary(object): + "Build the help summary consistently with the cmd module" + @classmethod + def make(cls, obj, specialcommands): + c = cmd.Cmd(stdout=cls()) + c.stdout.write('\n') + c.print_topics('special commands', + sorted(specialcommands), 15, 80) + c.print_topics('custom commands', + sorted(obj.syncommands), 15, 80) + c.print_topics('commands run in external processes', + sorted(obj.mpcommands), 15, 80) + c.print_topics('threaded commands', + sorted(obj.thcommands), 15, 80) + return c.stdout + def __init__(self): + self._ls = [] + def write(self, s): + self._ls.append(s) + def __str__(self): + return ''.join(self._ls) + +class TaskManager(object): + """ + Store the given commands into a task registry. Provides methods to + manage the submitted tasks. + """ + cmdprefix = '.' + specialcommands = set(['.help', '.last_tb']) + + def __init__(self, obj): + self.obj = obj + self.registry = {} # {taskno : task} + if obj.mpcommands or obj.thcommands: + self.specialcommands.update(['.kill', '.list', '.output']) + self.helpsummary = HelpSummary.make(obj, self.specialcommands) + self.mp_manager = multiprocessing.Manager() if obj.mpcommands else None + signal.signal(signal.SIGTERM, terminatedProcess) + + def close(self): + "Kill all the running tasks" + for task in self.registry.itervalues(): + try: + if task.status == 'RUNNING': + task.kill() + task.wait() + except: # task killed, nothing to wait + pass + if self.mp_manager: + self.mp_manager.shutdown() + + def _get_latest(self, taskno=-1, status=None): + "Get the latest submitted task from the registry" + assert taskno < 0, 'You must pass a negative number' + if status: + tasks = [t for t in self.registry.itervalues() + if t.status == status] + else: + tasks = [t for t in self.registry.itervalues()] + tasks.sort(key=attrgetter('no')) + if len(tasks) >= abs(taskno): + return tasks[taskno] + + ########################### special commands ######################### + + @plac_core.annotations( + taskno=('task to kill', 'positional', None, int)) + def kill(self, taskno=-1): + 'kill the given task (-1 to kill the latest running task)' + if taskno < 0: + task = self._get_latest(taskno, status='RUNNING') + if task is None: + yield 'Nothing to kill' + return + elif not taskno in self.registry: + yield 'Unknown task %d' % taskno + return + else: + task = self.registry[taskno] + if task.status in ('ABORTED', 'KILLED', 'FINISHED'): + yield 'Already finished %s' % task + return + task.kill() + yield task + + @plac_core.annotations( + status=('', 'positional', None, str, BaseTask.STATES)) + def list(self, status='RUNNING'): + 'list tasks with a given status' + for task in self.registry.values(): + if task.status == status: + yield task + + @plac_core.annotations( + taskno=('task number', 'positional', None, int)) + def output(self, taskno=-1): + 'show the output of a given task' + if taskno < 0: + task = self._get_latest(taskno) + if task is None: + yield 'Nothing to show' + return + elif taskno not in self.registry: + yield 'Unknown task %d' % taskno + return + else: + task = self.registry[taskno] + outstr = '\n'.join(map(str, task.outlist)) + yield task + if len(task.outlist) > 20 and use_less: + less(outstr) + else: + yield outstr + + @plac_core.annotations( + taskno=('task number', 'positional', None, int)) + def last_tb(self, taskno=-1): + "show the traceback of a given task, if any" + task = self._get_latest(taskno) + if task: + yield task.traceback + else: + yield 'Nothing to show' + + def help(self, cmd=None): + "show help about a given command" + if cmd is None: + yield str(self.helpsummary) + else: + yield plac_core.parser_from(self.obj).help_cmd(cmd) + +########################### SyncProcess ############################## + +class Process(subprocess.Popen): + "Start the interpreter specified by the params in a subprocess" + + def __init__(self, params): + code = '''import plac +plac.Interpreter(plac.import_main(*%s)).interact(prompt='i>\\n') +''' % params + subprocess.Popen.__init__( + self, [sys.executable, '-u', '-c', code], + stdin=subprocess.PIPE, stdout=subprocess.PIPE) + + def close(self): + "Close stdin and stdout" + self.stdin.close() + self.stdout.close() + + def recv(self): # char-by-char cannot work + "Return the output of the subprocess, line-by-line until the prompt" + lines = [] + while True: + lines.append(self.stdout.readline()) + if lines[-1] == 'i>\n': + out = ''.join(lines) + return out[:-1] + ' ' # remove last newline + + def send(self, line): + """Send a line (adding a newline) to the underlying subprocess + and wait for the answer""" + self.stdin.write(line + os.linesep) + return self.recv() + +########################## plac server ############################## + +import asyncore, asynchat, socket + +class _AsynHandler(asynchat.async_chat): + "asynchat handler starting a new interpreter loop for each connection" + + terminator = '\r\n' # the standard one for telnet + prompt = 'i> ' + + def __init__(self, socket, interpreter): + asynchat.async_chat.__init__(self, socket) + self.set_terminator(self.terminator) + self.i = interpreter + self.i.__enter__() + self.data = [] + self.write(self.prompt) + + def write(self, data, *args): + "Push a string back to the client" + if args: + data %= args + if data.endswith('\n') and not data.endswith(self.terminator): + data = data[:-1] + self.terminator # fix newlines + self.push(data) + + def collect_incoming_data(self, data): + "Collect one character at the time" + self.data.append(data) + + def found_terminator(self): + "Put in the queue the line received from the client" + line = ''.join(self.data) + self.log('Received line %r from %s' % (line, self.addr)) + if line == 'EOF': + self.i.__exit__() + self.handle_close() + else: + task = self.i.submit(line) + task.run() # synchronous or not + if task.etype: # manage exception + error = '%s: %s\nReceived: %s' % ( + task.etype.__name__, task.exc, ' '.join(task.arglist)) + self.log_info(task.traceback + error) # on the server + self.write(error + self.terminator) # back to the client + else: # no exception + self.write(task.str + self.terminator) + self.data = [] + self.write(self.prompt) + +class _AsynServer(asyncore.dispatcher): + "asyncore-based server spawning AsynHandlers" + + def __init__(self, interpreter, newhandler, port, listen=5): + self.interpreter = interpreter + self.newhandler = newhandler + self.port = port + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.bind(('', port)) + self.listen(listen) + + def handle_accept(self): + clientsock, clientaddr = self.accept() + self.log('Connected from %s' % str(clientaddr)) + i = self.interpreter.__class__(self.interpreter.obj) # new interpreter + self.newhandler(clientsock, i) # spawn a new handler + +########################### the Interpreter ############################# + +class Interpreter(object): + """ + A context manager with a .send method and a few utility methods: + execute, test and doctest. + """ + + @classmethod + def call(cls, factory, arglist=sys.argv[1:], + commentchar='#', split=shlex.split, + stdin=sys.stdin, prompt='i> ', verbose=False): + """ + Call a container factory with the arglist and instantiate an + interpreter object. If there are remaining arguments, send them to the + interpreter, else start an interactive session. + """ + obj = partial_call(factory, arglist) + i = cls(obj, commentchar, split) + if i.obj._args_: + with i: + task = i.send(i.obj._args_) # synchronous + if task.exc: + raise task.etype, task.exc, task.tb + print(task) + elif i.obj._interact_: + i.interact(stdin, prompt, verbose) + else: + i.parser.print_usage() + + def __init__(self, obj, commentchar='#', split=shlex.split): + self.obj = obj + try: + self.name = obj.__module__ + except AttributeError: + self.name = 'plac' + self.commentchar = commentchar + self.split = split + self._set_commands(obj) + self.tm = TaskManager(obj) + self.parser = plac_core.parser_from(obj, prog='', add_help=False) + if self.commands: + self.commands.update(self.tm.specialcommands) + self.parser.addsubcommands(self.tm.specialcommands, self.tm, + title='special commands') + if obj.mpcommands: + self.parser.addsubcommands(obj.mpcommands, obj, + title='commands run in external processes') + if obj.thcommands: + self.parser.addsubcommands(obj.thcommands, obj, + title='threaded commands') + self.parser.error = lambda msg: sys.exit(msg) # patch the parser + self._interpreter = None + + def _set_commands(self, obj): + "Make sure obj has the right command attributes as Python sets" + for attrname in ('commands', 'syncommands', 'mpcommands', 'thcommands'): + try: + sequence = getattr(obj, attrname) + except AttributeError: + sequence = [] + if not isinstance(sequence, set): + sequence = set(sequence) + setattr(obj, attrname, sequence) + obj.syncommands.update(obj.commands) + self.commands = obj.commands + self.commands.update(obj.syncommands) + self.commands.update(obj.mpcommands) + self.commands.update(obj.thcommands) + + def __enter__(self): + "Start the inner interpreter loop" + self._interpreter = self._make_interpreter() + self._interpreter.send(None) + return self + + def __exit__(self, *exc): + "Close the inner interpreter and the task manager" + self.close() + + def submit(self, line): + "Send a line to the underlying interpreter and return a task object" + if self._interpreter is None: + raise RuntimeError(_('%r not initialized: probably you forgot to ' + 'use the with statement') % self) + if isinstance(line, basestring): + arglist = self.split(line, self.commentchar) + else: # expects a list of strings + arglist = line + if not arglist: + return nulltask + task = self._interpreter.send(arglist) # nonblocking + if not plac_core._match_cmd(arglist[0], self.tm.specialcommands): + self.tm.registry[task.no] = task + return task + + def send(self, line): + "Send a line to the underlying interpreter and return the finished task" + task = self.submit(line) + BaseTask.run(task) # blocking + return task + + def tasks(self): + "The full lists of the submitted tasks" + return self.tm.registry.values() + + def close(self): + "Can be called to close the interpreter prematurely" + self.tm.close() + self._interpreter.close() + + def _make_interpreter(self): + "The interpreter main loop, from lists of arguments to task objects" + enter = getattr(self.obj, '__enter__', lambda : None) + exit = getattr(self.obj, '__exit__', lambda et, ex, tb: None) + enter() + task = None + try: + for no in itertools.count(1): + arglist = yield task + try: + cmd, result = self.parser.consume(arglist) + except: # i.e. SystemExit for invalid command + task = SynTask(no, arglist, gen_exc(*sys.exc_info())) + continue + if not plac_core.iterable(result): # atomic result + task = SynTask(no, arglist, gen_val(result)) + elif cmd in self.obj.mpcommands: + task = MPTask(no, arglist, result, self.tm.mp_manager) + elif cmd in self.obj.thcommands: + task = ThreadedTask(no, arglist, result) + else: # blocking task + task = SynTask(no, arglist, result) + except GeneratorExit: # regular exit + exit(None, None, None) + except: # exceptional exit + exit(*sys.exc_info()) + raise + + def check(self, given_input, expected_output): + "Make sure you get the expected_output from the given_input" + output = self.send(given_input).str # blocking + ok = (output == expected_output) + if not ok: + # the message here is not internationalized on purpose + msg = 'input: %s\noutput: %s\nexpected: %s' % ( + given_input, output, expected_output) + raise AssertionError(msg) + + def _parse_doctest(self, lineiter): + "Returns the lines of input, the lines of output, and the line number" + lines = [line.strip() for line in lineiter] + inputs = [] + positions = [] + for i, line in enumerate(lines): + if line.startswith('i> '): + inputs.append(line[3:]) + positions.append(i) + positions.append(len(lines) + 1) # last position + outputs = [] + for i, start in enumerate(positions[:-1]): + end = positions[i + 1] + outputs.append('\n'.join(lines[start+1:end])) + return zip(inputs, outputs, positions) + + def doctest(self, lineiter, verbose=False): + """ + Parse a text containing doctests in a context and tests of all them. + Raise an error even if a single doctest if broken. Use this for + sequential tests which are logically grouped. + """ + with self: + for input, output, no in self._parse_doctest(lineiter): + if verbose: + write('i> %s\n' % input) + write('-> %s\n' % output) + task = self.send(input) # blocking + if not str(task) == output: + msg = 'line %d: input: %s\noutput: %s\nexpected: %s\n' % ( + no + 1, input, task, output) + write(msg) + raise task.etype, task.exc, task.tb + + def execute(self, lineiter, verbose=False): + "Execute a lineiter of commands in a context and print the output" + with self: + for line in lineiter: + if verbose: + write('i> ' + line) + task = self.send(line) # finished task + if task.etype: # there was an error + raise task.etype, task.exc, task.tb + write('%s\n' % task.str) + + def multiline(self, stdin=sys.stdin, terminator=';', verbose=False): + "The multiline mode is especially suited for usage with emacs" + with self: + for line in read_long_line(stdin, terminator): + task = self.submit(line) + task.run() + write('%s\n' % task.str) + if verbose and task.traceback: + write(task.traceback) + + def interact(self, stdin=sys.stdin, prompt='i> ', verbose=False): + "Starts an interactive command loop reading commands from the consolle" + if stdin is sys.stdin and readline: # use readline + histfile = os.path.expanduser('~/.%s.history' % self.name) + self.stdin = ReadlineInput(self.commands, histfile=histfile) + else: + self.stdin = stdin + self.prompt = prompt + self.verbose = verbose + intro = self.obj.__doc__ or '' + write(intro + '\n') + with self: + if self.stdin is sys.stdin: # do not close stdin automatically + self._manage_input() + else: + with self.stdin: # close stdin automatically + self._manage_input() + + def _manage_input(self): + "Convert input lines into task which are then executed" + for line in iter(lambda : read_line(self.stdin, self.prompt), ''): + line = line.strip() + if not line: + continue + task = self.submit(line) + task.run() # synchronous or not + write(str(task) + '\n') + if self.verbose and task.etype: + write(task.traceback) + + def start_server(self, port=2199, **kw): + """Starts an asyncore server reading commands for clients and opening + a new interpreter for each connection.""" + _AsynServer(self, _AsynHandler, port) # register the server + try: + asyncore.loop(**kw) + except KeyboardInterrupt: + pass + finally: + asyncore.close_all() + + def stop_server(self, wait=0.0): + "Stops the asyncore server, possibly after a given number of seconds" + threading.Timer(wait, asyncore.socket_map.clear).start() + +#################################### runp ##################################### + +class _TaskLauncher(object): + "Helper for runp" + def __init__(self, genseq, mode): + if mode == 'p': + self.mpcommands = ['rungen'] + else: + self.thcommands = ['rungen'] + self.genlist = list(genseq) + def rungen(self, i): + for out in self.genlist[int(i) - 1]: + yield out + +def runp(genseq, mode='p', start=True): + """Run a sequence of generators in parallel. Mode can be 'p' (use processes) + or 't' (use threads). Return a list of running task objects. If start is + False, the tasks are only submitted and not automatically started. + """ + assert mode in 'pt', mode + launcher = _TaskLauncher(genseq, mode) + inter = Interpreter(launcher).__enter__() + for i in range(len(launcher.genlist)): + inter.submit('rungen %d' % (i + 1)) + if start: + for task in inter.tasks(): + task.run() + return inter.tasks() diff --git a/plac_runner.py b/plac_runner.py new file mode 100644 index 0000000..6502efc --- /dev/null +++ b/plac_runner.py @@ -0,0 +1,65 @@ +#!python +from __future__ import with_statement +import os, sys, shlex, inspect +import plac + +def run(fnames, cmd, verbose): + "Run batch scripts and tests" + for fname in fnames: + with open(fname) as f: + lines = list(f) + if not lines[0].startswith('#!'): + sys.exit('Missing or incorrect shebang line!') + firstline = lines[0][2:] # strip the shebang + init_args = shlex.split(firstline) + tool = plac.import_main(*init_args) + command = getattr(plac.Interpreter(tool), cmd) # doctest or execute + if verbose: + sys.stdout.write('Running %s with %s' % (fname, firstline)) + command(lines[1:], verbose=verbose) + +@plac.annotations( + verbose=('verbose mode', 'flag', 'v'), + interactive=('run plac tool in interactive mode', 'flag', 'i'), + multiline=('run plac tool in multiline mode', 'flag', 'm'), + serve=('run plac server', 'option', 's', int), + batch=('run plac batch files', 'flag', 'b'), + test=('run plac test files', 'flag', 't'), + fname='script to run (.py or .plac or .placet)', + extra='additional arguments', + ) +def main(verbose, interactive, multiline, serve, batch, test, fname=None, + *extra): + "Runner for plac tools, plac batch files and plac tests" + baseparser = plac.parser_from(main) + if fname is None: + baseparser.print_help() + elif sys.argv[1] == fname: # script mode + plactool = plac.import_main( + fname, prog=os.path.basename(sys.argv[0]) + ' ' + fname) + out = plac.call(plactool, sys.argv[2:], eager=False) + if plac.iterable(out): + for output in out: + print(output) + else: + print(out) + elif interactive or multiline or serve: + plactool = plac.import_main(fname, *extra, **{'prog': ''}) + i = plac.Interpreter(plactool) + if interactive: + i.interact(verbose=verbose) + elif multiline: + i.multiline(verbose=verbose) + elif serve: + i.start_server(serve) + elif batch: + run((fname,) + extra, 'execute', verbose) + elif test: + run((fname,) + extra, 'doctest', verbose) + print('run %s plac test(s)' % (len(extra) + 1)) + else: + baseparser.print_usage() +main.add_help = False + +if __name__ == '__main__': + plac.call(main) \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..861a9f5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..aafe43d --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +try: + from setuptools import setup +except ImportError: + from distutils.core import setup +import os.path + +def require(*modules): + """Check if the given modules are already available; if not add them to + the dependency list.""" + deplist = [] + for module in modules: + try: + __import__(module) + except ImportError: + deplist.append(module) + return deplist + +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', + version=getversion( + os.path.join(os.path.dirname(__file__), 'plac.py')), + description=('The smartest 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_core', 'plac_ext', 'plac'], + scripts = ['plac_runner.py'], + install_requires=require('argparse', 'multiprocessing'), + use_2to3=True, + keywords="command line arguments parser", + platforms=["All"], + classifiers=['Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries', + 'Topic :: Utilities'], + zip_safe=False)