/docs/tutorial.rst

https://bitbucket.org/neithere/argh/ · ReStructuredText · 429 lines · 296 code · 133 blank · 0 comment · 0 complexity · aaecb9c0adfbba8fc9f15e8096104fe2 MD5 · raw file

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