Skip to content

Commit

Permalink
docs: improve the tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
neithere committed Oct 22, 2023
1 parent 5883a45 commit c6549fc
Showing 1 changed file with 94 additions and 45 deletions.
139 changes: 94 additions & 45 deletions docs/source/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Assume we need a CLI application which output is modulated by arguments:
$ ./greet.py
Hello unknown user!
$ ./greet.py --name John
$ ./greet.py John
Hello John!
This is our business logic:
Expand All @@ -34,6 +34,19 @@ Let's convert the function into a complete CLI application::

Done. Dead simple.

You may want to make the name an "option" AKA named CLI argument, like this::

$ ./greet.py --name John

In that case it's enough to make the function argument `name` "keyword-only"
(see :pep:`3102` for explanation)::

def main(*, name: str = "unknown user") -> str:
...

Everything to the left of ``*`` becomes a positional CLI argument. Everything
to the right of ``*`` becomes a named one.

What about multiple commands? Easy::

argh.dispatch_commands([load, dump])
Expand Down Expand Up @@ -75,71 +88,102 @@ Declaring Commands
The Natural Way
...............

You've already learned the natural way of declaring commands before even
knowing about `argh`::
If you are comfortable with the basics of Python, you already knew the natural
way of declaring CLI commands with `Argh` before even learning about the
existence of `Argh`.

Please read the following snippet carefully. Is there any `Argh`-specific API?

::

def my_command(
alpha: str, beta: int = 1, *args, gamma: int, delta: bool = False
) -> list[str]:
return [alpha, beta, args, gamma, delta]

def my_command(alpha, beta=1, gamma=False, *delta):
return
The answer is: no. This is a completely generic Python function.

When executed as ``app.py my-command --help``, such application prints::
Let's make this function available as a CLI command::

usage: app.py my-command [-h] [-b BETA] [-g] alpha [delta ...]
import argh


def my_command(
alpha: str, beta: int = 1, *args, gamma: int, delta: bool = False
) -> list[str]:
return [alpha, beta, args, gamma, delta]


if __name__ == "__main__":
argh.dispatch_commands([my_command])

That's all. You don't need to do anything else.

When executed as ``./app.py my-command --help``, such application prints::

usage: app.py my-command [-h] -g GAMMA [-d] alpha [beta] [args ...]

positional arguments:
alpha
delta
alpha -
beta 1
args -

optional arguments:
options:
-h, --help show this help message and exit
-b BETA, --beta BETA
-g, --gamma
-g GAMMA, --gamma GAMMA
-
-d, --delta False

The same result can be achieved with this chunk of `argparse` code (with the
exception that in `argh` you don't immediately modify a parser but rather
declare what's to be added to it later)::
Now let's take a look at how we would do it without `Argh`::

parser.add_argument("alpha")
parser.add_argument("-b", "--beta", default=1, type=int)
parser.add_argument("-g", "--gamma", default=False, action="store_true")
parser.add_argument("delta", nargs="*")
import argparse

Verbose, hardly readable, requires learning another API.

`Argh` allows for more expressive and pythonic code because:
def my_command(
alpha: str, beta: int = 1, *args, gamma: int, delta: bool = False
) -> list[str]:
return [alpha, beta, args, gamma, delta]

* everything is inferred from the function signature;
* arguments without default values are interpreted as required positional
arguments;
* arguments with default values are interpreted as options;

* options with a `bool` as default value are considered flags and their
presence triggers the action `store_true` (or `store_false`);
* values of options that don't trigger actions are coerced to the same type
as the default value;
if __name__ == "__main__":
parser = argparse.ArgumentParser()

* the ``*args`` entry (function's positional arguments) is interpreted as
a single argument with 0..n values.
subparser = parser.add_subparsers().add_parser("my-command")

Hey, that's a lot for such a simple case! But then, that's why the API feels
natural: `argh` does a lot of work for you.
subparser.add_argument("alpha")
subparser.add_argument("beta", default=1, nargs="?", type=int)
subparser.add_argument("args", nargs="*")
subparser.add_argument("-g", "--gamma")
subparser.add_argument("-d", "--delta", default=False, action="store_true")

.. note::
ns = parser.parse_args()

The pattern described above is the "by name if has default" mapping policy.
It used to be *the* policy but starting with Argh v.0.30 there's a better
one, "by name if kwonly". Although the older one will remain the default
policy for a while, it may be eventually dropped in favour of the new one.
lines = my_command(ns.alpha, ns.beta, *ns.args, gamma=ns.gamma, delta=ns.delta)

Please check `~argh.assembling.NameMappingPolicy` for details.
for line in lines:
print(line)

Usage example::
Verbose, hardly readable, requires learning the API. With `Argh` it's just a
single line in addition to your function.

def my_command(alpha, beta=1, *, gamma, delta=False, **kwargs):
...
`Argh` allows for more expressive and pythonic code because:

* everything is inferred from the function signature;
* regular function arguments are represented as positional CLI arguments;
* varargs (``*args``) are represented as a "zero or more" positional CLI argument;
* kwonly (keyword-only arguments, see :pep:`3102`) are represented as named CLI
arguments;

* keyword-only arguments with a `bool` default value are considered flags
(AKA toggles) and their presence triggers the action `store_true` (or
`store_false`).

argh.dispatch_command(
my_command, name_mapping_policy=NameMappingPolicy.BY_NAME_IF_KWONLY
)
* you can ``print()`` but you don't have to — the return value will be printed
for you; it can even be an iterable (feel free to ``yield`` too), then each
element will be printed on its own line.

Hey, that's a lot for such a simple case! But then, that's why the API feels
natural: `argh` does a lot of work for you.

Well, there's nothing more elegant than a simple function. But simplicity
comes at a cost in terms of flexibility. Fortunately, `argh` doesn't stay in
Expand All @@ -158,6 +202,11 @@ Extended argument declaration can be helpful in that case.
Extended Argument Declaration
.............................

.. note::

This section will be out of date soon. Typing hints will be used for all
the cases described here including argument help.

When function signature isn't enough to fine-tune the argument declarations,
the :class:`~argh.decorators.arg` decorator comes in handy::

Expand Down

0 comments on commit c6549fc

Please sign in to comment.