PageRenderTime 69ms CodeModel.GetById 36ms app.highlight 24ms RepoModel.GetById 1ms app.codeStats 1ms

/docs/tutorial.rst

https://bitbucket.org/neithere/argh/
ReStructuredText | 429 lines | 296 code | 133 blank | 0 comment | 0 complexity | aaecb9c0adfbba8fc9f15e8096104fe2 MD5 | raw file
  1Tutorial
  2~~~~~~~~
  3
  4`Argh` is a small library that provides several layers of abstraction on top
  5of `argparse`.  You are free to use any layer that fits given task best.
  6The layers can be mixed.  It is always possible to declare a command with
  7the  highest possible (and least flexible) layer and then tune the behaviour
  8with any of the lower layers including the native API of `argparse`.
  9
 10Dive In
 11-------
 12
 13Assume we need a CLI application which output is modulated by arguments:
 14
 15.. code-block:: bash
 16
 17    $ ./greet.py
 18    Hello unknown user!
 19
 20    $ ./greet.py --name John
 21    Hello John!
 22
 23This is our business logic:
 24
 25.. code-block:: python
 26
 27    def main(name='unknown user'):
 28        return 'Hello {0}!'.format(name)
 29
 30That was plain Python, nothing CLI-specific.
 31Let's convert the function into a complete CLI application::
 32
 33    argh.dispatch_command(main)
 34
 35Done.  Dead simple.
 36
 37What about multiple commands?  Easy::
 38
 39    argh.dispatch_commands([load, dump])
 40
 41And then call your script like this::
 42
 43    $ ./app.py dump
 44    $ ./app.py load fixture.json
 45    $ ./app.py load fixture.yaml --format=yaml
 46
 47I guess you get the picture.  The commands are **ordinary functions**
 48with ordinary signatures:
 49
 50* Declare them somewhere, dispatch them elsewhere.  This ensures **loose
 51  coupling** of components in your application.
 52* They are **natural** and pythonic. No fiddling with the parser and the related
 53  intricacies like ``action='store_true'`` which you could never remember.
 54
 55Still, there's much more to commands than this.
 56
 57The examples above raise some questions, including:
 58
 59* do we have to ``return``, or ``print`` and ``yield`` are also supported?
 60* what's the difference between ``dispatch_command()``
 61  and ``dispatch_commands()``?  What's going on under the hood?
 62* how do I add help for each argument?
 63* how do I access the parser to fine-tune its behaviour?
 64* how to keep the code as DRY as possible?
 65* how do I expose the function under custom name and/or define aliases?
 66* how do I have values converted to given type?
 67* can I use a namespace object instead of the natural way?
 68
 69Just read on.
 70
 71Declaring Commands
 72------------------
 73
 74The Natural Way
 75...............
 76
 77You've already learned the natural way of declaring commands before even
 78knowing about `argh`::
 79
 80    def my_command(alpha, beta=1, gamma=False, *delta):
 81        return
 82
 83When executed as ``app.py my-command --help``, such application prints::
 84
 85    usage: app.py my-command [-h] [-b BETA] [-g] alpha [delta [delta ...]]
 86
 87    positional arguments:
 88      alpha
 89      delta
 90
 91    optional arguments:
 92      -h, --help            show this help message and exit
 93      -b BETA, --beta BETA
 94      -g, --gamma
 95
 96The same result can be achieved with this chunk of `argparse` code (with the
 97exception that in `argh` you don't immediately modify a parser but rather
 98declare what's to be added to it later)::
 99
100    parser.add_argument('alpha')
101    parser.add_argument('-b', '--beta', default=1, type=int)
102    parser.add_argument('-g', '--gamma', default=False, action='store_true')
103    parser.add_argument('delta', nargs='*')
104
105Verbose, hardly readable, requires learning another API.
106
107`Argh` allows for more expressive and pythonic code because:
108
109* everything is inferred from the function signature;
110* arguments without default values are interpreted as required positional
111  arguments;
112* arguments with default values are interpreted as options;
113
114  * options with a `bool` as default value are considered flags and their
115    presence triggers the action `store_true` (or `store_false`);
116  * values of options that don't trigger actions are coerced to the same type
117    as the default value;
118
119* the ``*args`` entry (function's positional arguments) is interpreted as
120  a single argument with 0..n values.
121
122Hey, that's a lot for such a simple case!  But then, that's why the API feels
123natural: `argh` does a lot of work for you.
124
125Well, there's nothing more elegant than a simple function.  But simplicity
126comes at a cost in terms of flexibility.  Fortunately, `argh` doesn't stay in
127the way and offers less natural but more powerful tools.
128
129Documenting Your Commands
130.........................
131
132The function's docstring is automatically included in the help message.
133When the script is called as ``./app.py my-command --help``, the docstring
134is displayed along with a short overview of the arguments.
135
136However, in many cases it's a good idea do add extra documentation per argument.
137
138In Python 3 it's easy:
139
140.. code-block:: python
141
142    def load(path : 'file to load', format : 'json or yaml' = 'yaml'):
143        "Loads given file as YAML (unless other format is specified)"
144        return loaders[format].load(path)
145
146Python 2 does not support annotations so the above example would raise a
147`SyntaxError`.  You would need to add help via `argparse` API::
148
149    parser.add_argument('path', help='file to load')
150
151...which is far from DRY and very impractical if the functions are dispatched
152in a different place.  This is when extended declarations become useful.
153
154Extended Argument Declaration
155.............................
156
157When function signature isn't enough to fine-tune the argument declarations,
158the :class:`~argh.decorators.arg` decorator comes in handy::
159
160    @arg('path', help='file to load')
161    @arg('--format', help='json or yaml')
162    def load(path, format='yaml'):
163        return loaders[format].load(path)
164
165In this example we have declared a function with arguments `path` and `format`
166and then extended their declarations with help messages.
167
168The decorator mostly mimics `argparse`'s add_argument_.  The `name_or_flags`
169argument must match function signature, that is:
170
1711. ``path`` and ``--format`` map to ``func(path)`` and ``func(format='x')``
172   respectively (short name like ``-f`` can be omitted);
1732. a name that doesn't map to anything in function signature is not allowed.
174
175.. _add_argument: http://docs.python.org/dev/library/argparse.html#argparse.ArgumentParser.add_argument
176
177The decorator doesn't modify the function's behaviour in any way.
178
179Sometimes the function is not likely to be used other than as a CLI command
180and all of its arguments are duplicated with decorators.  Not very DRY.
181In this case ``**kwargs`` can be used as follows::
182
183    @arg('number', default=0, help='the number to increment')
184    def increment(**kwargs):
185        return kwargs['number'] + 1
186
187In other words, if ``**something`` is in the function signature, extra
188arguments are **allowed** to be specified via decorators; they all go into that
189very dictionary.
190
191Mixing ``**kwargs`` with straightforward signatures is also possible::
192
193    @arg('--bingo')
194    def cmd(foo, bar=1, *maybe, **extra):
195        return ...
196
197.. note::
198
199   It is not recommended to mix ``*args`` with extra *positional* arguments
200   declared via decorators because the results can be pretty confusing (though
201   predictable).  See `argh` tests for details.
202
203Namespace Objects
204.................
205
206The default approach of `argparse` is similar to ``**kwargs``: the function
207expects a single object and the CLI arguments are defined elsewhere.
208
209In order to dispatch such "argparse-style" command via `argh`, you need to
210tell the latter that the function expects a namespace object.  This is done by
211wrapping the function into the :func:`~argh.decorators.expects_obj` decorator::
212
213    @expects_obj
214    def cmd(args):
215        return args.foo
216
217This way arguments cannot be defined in the Natural Way but the
218:class:`~argh.decorators.arg` decorator works as usual.
219
220.. note::
221   In both cases — ``**kwargs``-only and `@expects_obj` — the arguments
222   **must** be declared via decorators or directly via the `argparse` API.
223   Otherwise the command has zero arguments (apart from ``--help``).
224
225Assembling Commands
226-------------------
227
228.. note::
229
230    `Argh` decorators introduce a declarative mode for defining commands. You
231    can access the `argparse` API after a parser instance is created.
232
233After the commands are declared, they should be assembled within a single
234argument parser.  First, create the parser itself::
235
236    parser = argparse.ArgumentParser()
237
238Add a couple of commands via :func:`~argh.assembling.add_commands`::
239
240    argh.add_commands(parser, [load, dump])
241
242The commands will be accessible under the related functions' names::
243
244    $ ./app.py {load,dump}
245
246Subcommands
247...........
248
249If the application has too many commands, they can be grouped into namespaces::
250
251    argh.add_commands(parser, [serve, ping], namespace='www',
252                      title='Web-related commands')
253
254The resulting CLI is as follows::
255
256    $ ./app.py www {serve,ping}
257
258See :doc:`subparsers` for the gory details.
259
260Dispatching Commands
261--------------------
262
263The last thing is to actually parse the arguments and call the relevant command
264(function) when our module is called as a script::
265
266    if __name__ == '__main__':
267        argh.dispatch(parser)
268
269The function :func:`~argh.dispatching.dispatch` uses the parser to obtain the
270relevant function and arguments; then it converts arguments to a form
271digestible by this particular function and calls it.  The errors are wrapped
272if required (see below); the output is processed and written to `stdout`
273or a given file object.  Special care is given to terminal encoding.  All this
274can be fine-tuned, see API docs.
275
276A set of commands can be assembled and dispatched at once with a shortcut
277:func:`~argh.dispatching.dispatch_commands` which isn't as flexible as the
278full version described above but helps reduce the code in many cases.
279Please refer to the API documentation for details.
280
281Modular Application
282...................
283
284As you can see, with `argh` the CLI application consists of three parts:
285
2861. declarations (functions and their arguments);
2872. assembling (a parser is constructed with these functions);
2883. dispatching (input → parser → function → output).
289
290This clear separation makes a simple script just a bit more readable,
291but for a large application this is extremely important.
292
293Also note that the parser is standard.
294It's OK to call :func:`~argh.dispatching.dispatch` on a custom subclass
295of `argparse.ArgumentParser`.
296
297By the way, `argh` ships with :class:`~argh.helpers.ArghParser` which
298integrates the assembling and dispatching functions for DRYness.
299
300Single-command application
301--------------------------
302
303There are cases when the application performs a single task and it perfectly
304maps to a single command. The method above would require the user to type a
305command like ``check_mail.py check --now`` while ``check_mail.py --now`` would
306suffice. In such cases :func:`~argh.assembling.add_commands` should be replaced
307with :func:`~argh.assembling.set_default_command`::
308
309    def main():
310        return 1
311
312    argh.set_default_command(parser, main)
313
314There's also a nice shortcut :func:`~argh.dispatching.dispatch_command`.
315Please refer to the API documentation for details.
316
317Generated help
318--------------
319
320`Argparse` takes care of generating nicely formatted help for commands and
321arguments. The usage information is displayed when user provides the switch
322``--help``. However `argparse` does not provide a ``help`` *command*.
323
324`Argh` always adds the command ``help`` automatically:
325
326    * ``help shell````shell --help``
327    * ``help web serve````web serve --help``
328
329See also `<#documenting-your-commands>`_.
330
331Returning results
332-----------------
333
334Most commands print something. The traditional straightforward way is this::
335
336    def foo():
337        print('hello')
338        print('world')
339
340However, this approach has a couple of flaws:
341
342    * it is difficult to test functions that print results: you are bound to
343      doctests or need to mess with replacing stdout;
344    * terminals and pipes frequently have different requirements for encoding,
345      so Unicode output may break the pipe (e.g. ``$ foo.py test | wc -l``). Of
346      course you don't want to do the checks on every `print` statement.
347
348Good news: if you return a string, `Argh` will take care of the encoding::
349
350    def foo():
351        return 'привет'
352
353But what about multiple print statements?  Collecting the output in a list
354and bulk-processing it at the end would suffice.  Actually you can simply
355return a list and `Argh` will take care of it::
356
357    def foo():
358        return ['hello', 'world']
359
360.. note::
361
362    If you return a string, it is printed as is.  A list or tuple is iterated
363    and printed line by line. This is how :func:`dispatcher
364    <argh.dispatching.dispatch>` works.
365
366This is fine, but what about non-linear code with if/else, exceptions and
367interactive prompts? Well, you don't need to manage the stack of results within
368the function. Just convert it to a generator and `Argh` will do the rest::
369
370    def foo():
371        yield 'hello'
372        yield 'world'
373
374Syntactically this is exactly the same as the first example, only with `yield`
375instead of `print`. But the function becomes much more flexible.
376
377.. hint::
378
379    If your command is likely to output Unicode and be used in pipes, you
380    should definitely use the last approach.
381
382Exceptions
383----------
384
385Usually you only want to display the traceback on unexpected exceptions. If you
386know that something can be wrong, you'll probably handle it this way::
387
388    def show_item(key):
389        try:
390            item = items[key]
391        except KeyError as error:
392            print(e)    # hide the traceback
393            sys.exit()  # bail out (unsafe!)
394        else:
395            ... do something ...
396            print(item)
397
398This works, but the print-and-exit tasks are repetitive; moreover, there are
399cases when you don't want to raise `SystemExit` and just need to collect the
400output in a uniform way. Use :class:`~argh.exceptions.CommandError`::
401
402    def show_item(key):
403        try:
404            item = items[key]
405        except KeyError as error:
406            raise CommandError(error)  # bail out, hide traceback
407        else:
408            ... do something ...
409            return item
410
411`Argh` will wrap this exception and choose the right way to display its
412message (depending on how :func:`~argh.dispatching.dispatch` was called).
413
414Decorator :func:`~argh.decorators.wrap_errors` reduces the code even further::
415
416    @wrap_errors([KeyError])  # catch KeyError, show the message, hide traceback
417    def show_item(key):
418        return items[key]     # raise KeyError
419
420Of course it should be used with care in more complex commands.
421
422The decorator accepts a list as its first argument, so multiple commands can be
423specified.  It also allows plugging in a preprocessor for the catched errors::
424
425    @wrap_errors(processor=lambda excinfo: 'ERR: {0}'.format(excinfo))
426    def func():
427        raise CommandError('some error')
428
429The command above will print `ERR: some error`.