diff options
Diffstat (limited to 'plac/doc/plac_adv.txt')
-rw-r--r-- | plac/doc/plac_adv.txt | 1266 |
1 files changed, 0 insertions, 1266 deletions
diff --git a/plac/doc/plac_adv.txt b/plac/doc/plac_adv.txt deleted file mode 100644 index 936f4af..0000000 --- a/plac/doc/plac_adv.txt +++ /dev/null @@ -1,1266 +0,0 @@ -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 - <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: - -.. 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. - Use help to see the available commands. - i> help - - special commands - ================ - 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. - -CHANGED IN VERSION 0.9: if you have an old version of plac_ the -``help`` command must be prefixed with a dot, i.e. you must write -``.help``. The old behavior was more consistent in my opinion, since -it made it clear that the ``help`` command was special and threated -differently from the regular commands. However many users complained against -the dot, so I changed it to make them happy. Starting from release 0.9 -the ``help`` command is just an alias for ``--help``, in the -sense that there is a preprocessor step replacing ``help`` with ``--help`` -in the argument list. -Notice that if you implement a custom ``help`` command in the commander class -the preprocessor will be automatically disabled: passing ``help`` -will call the custom help command, just as you would expect. - -plac.Interpreter.call --------------------------------------------- - -At the core of ``plac`` there is the ``call`` function which invokes -a callable with the list of 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. Instead, it would be nice -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. Then 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. - Use 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 <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 [None] - - 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 -i - 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> - -It is possible to store the output of a task into a file, to be read -later (this is useful for tasks with a large output):: - - i> .output 1 /tmp/out.txt - saved output of 1 into /tmp/out.txt - -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: - -.. 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 - <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. The code below provides an example of -how you could implement a GUI over the importer example: - -.. include:: importer_ui.py - :literal: - -Monitor support --------------------------------- - -Starting from release 0.8 plac_ provides builtin support for monitoring the -output of concurrent commands, at least for platforms where multiprocessing -is fully supported. You can define your own monitor -class, simply by inheriting from ``plac.Monitor`` and by -overriding the methods ``add_listener(self, no)``, -``del_listener(self, taskno)``, ``notify_listener(self, taskno, msg)``, -``schedule(self, seconds, func, arg)`` and ``run(self)``. -Then, you can a monitor object to any ``plac.Interpreter`` object -by simply calling the ``add_monitor`` method. -For convenience, -``plac`` comes with a very simple ``TkMonitor`` based on Tkinter -(I chose Tkinter because it is easy to use and it is -in the standard library, but you can use any GUI): you can just -look at how the ``TkMonitor`` is implemented in ``plac_tk.py`` -and adapt it. Here is an example of usage of the ``TkMonitor``: - -.. include:: tkmon.py - :literal: - -Try to give the ``hello`` command from the interactive interpreter: -each time a new text widget will be added displaying the output -of the command. Notice that if ``Tkinter`` is not installed correctly -on your system the ``TkMonitor`` class will not be available. - -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', monitors=(), 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 file ``test_runp`` in the ``doc`` directory of the plac distribution -shows another couple of examples of usage, including how to show the -results of the running computation on a ``TkMonitor``. - -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 - <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. - 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 -.. _advanced usage document: in-writing -.. _twill: http://twill.idyll.org/ -.. _basic documentation: http://plac.googlecode.com/hg/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 |