Tutorial
Contents
Tutorial#
We’ve already seen many of the concepts that power $λ
in the
Highlights section. This tutorial will address these
concepts one at a time and expose the reader to some nuances of usage.
An example from argparse#
Many of you are already familiar with argparse. You may even recognize this example from the argparse docs:
import argparse
parser = argparse.ArgumentParser(description="calculate X to the power of Y")
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
args = parser.parse_args()
Here is one way to express this logic in $λ
:
>>> from dollar_lambda import command, flag
>>> @command(
... parsers=dict(kwargs=(flag("verbose") | flag("quiet")).optional()),
... help=dict(x="the base", y="the exponent"),
... )
... def main(x: int, y: int, **kwargs):
... print(dict(x=x, y=y, **kwargs))
Here is the help text for this parser:
>>> main("-h")
usage: -x X -y Y [--verbose | --quiet]
x: the base
y: the exponent
As indicated, this succeeds given --verbose
>>> main("-x", "1", "-y", "2", "--verbose")
{'x': 1, 'y': 2, 'verbose': True}
or --quiet
>>> main("-x", "1", "-y", "2", "--quiet")
{'x': 1, 'y': 2, 'quiet': True}
or neither
>>> main("-x", "1", "-y", "2")
{'x': 1, 'y': 2}
Note
Ordinarily , we would not feed main
any arguments, and it would get
them from the command line:
>>> import sys
>>> sys.argv[1:] = ["-x", "1", "-y", "2"] # simulate command line input
>>> parsers.TESTING = False # unnecessary outside doctests
>>> main()
{'x': 1, 'y': 2}
>>> parsers.TESTING = True
Equivalent in lower-level syntax#
To better understand what is going on here, let’s remove the syntactic sugar:
>>> from dollar_lambda import nonpositional, option
>>> p = nonpositional(
... (flag("verbose") | flag("quiet")).optional(),
... option("x", type=int, help="the base"),
... option("y", type=int, help="the exponent"),
... )
...
>>> def main(x, y, **kwargs):
... return dict(x=x, y=y, **kwargs)
...
>>> main(**p.parse_args("-x", "1", "-y", "2", "--verbose"))
{'x': 1, 'y': 2, 'verbose': True}
>>> main(**p.parse_args("-x", "1", "-y", "2", "--quiet"))
{'x': 1, 'y': 2, 'quiet': True}
>>> main(**p.parse_args("-x", "1", "-y", "2"))
{'x': 1, 'y': 2}
Now let’s walk through this step by step.
High-Level Parsers#
In the de-sugared implementation there are two different parser
constructors: flag
, which binds a boolean value to a variable, and
option
, which binds an arbitrary value to a variable.
flag
#
>>> p = flag("verbose")
>>> p.parse_args("--verbose")
{'verbose': True}
By default flag
fails when it does not receive expected input:
>>> p.parse_args()
usage: --verbose
The following arguments are required: --verbose
Alternately, you can set a default value:
>>> flag("verbose", default=False).parse_args()
{'verbose': False}
option
#
option
is similar but takes an argument:
By default, option
, expects a single
-
for single-character variable names (as in
-x
), as opposed to --
for longer names (as in --xenophon
):
>>> option("x").parse_args("-x", "1")
{'x': '1'}
>>> option("xenophon").parse_args("-xenophon", "1")
{'xenophon': '1'}
Use the type
argument to convert the input to a different type:
>>> option("x", type=int).parse_args("-x", "1") # converts "1" to an int
{'x': 1}
Parser Combinators#
Parser combinators are functions that combine multiple parsers into new,
more complex parsers. Our example uses two such functions:
nonpositional
and
|
.
|
#
The |
operator is used for
alternatives. Specifically, it will try the first parser, and if that
fails, try the second:
>>> p = flag("verbose") | flag("quiet")
>>> p.parse_args("--quiet") # flag("verbose") fails
{'quiet': True}
>>> p.parse_args("--verbose") # flag("verbose") succeeds
{'verbose': True}
By default one of the two flags would be required to prevent failure:
>>> p.parse_args() # neither flag is provided so this fails usage:
usage: [--verbose | --quiet]
The following arguments are required: --verbose
We can permit the omission of both flags by using
optional
, as we
saw earlier, or we can supply a default value:
>>> (flag("verbose") | flag("quiet")).optional().parse_args() # flags fail, but that's ok
{}
>>> (flag("verbose") | flag("quiet", default=False)).parse_args()
{'quiet': False}
In the second example, flag("verbose")
fails but
flag("quiet", default=False)
succeeds.
Note
Unlike logical “or” but like Python or
, the
|
operator is not commutative:
>>> from dollar_lambda import argument
>>> (flag("verbose") | argument("x")).parse_args("--verbose")
{'verbose': True}
argument
binds to positional arguments. If it comes first, it will
think that "--verbose"
is the expression that we want to bind to
x
:
>>> from dollar_lambda import argument
>>> (argument("x") | flag("verbose")).parse_args("--verbose")
{'x': '--verbose'}
nonpositional
and +
#
nonpositional
takes a sequence of parsers as arguments and attempts
all permutations of them, returning the first permutations that is
successful:
>>> p = nonpositional(flag("verbose"), flag("quiet"))
>>> p.parse_args("--verbose", "--quiet")
{'verbose': True, 'quiet': True}
>>> p.parse_args("--quiet", "--verbose") # reverse order also works
{'quiet': True, 'verbose': True}
For just two parsers you can use
+
instead of nonpositional
:
>>> p = flag("verbose") + flag("quiet")
>>> p.parse_args("--verbose", "--quiet")
{'verbose': True, 'quiet': True}
>>> p.parse_args("--quiet", "--verbose") # reverse order also works
{'quiet': True, 'verbose': True}
This will not cover all permutations for more than two parsers:
>>> p = flag("verbose") + flag("quiet") + option("x")
>>> p.parse_args("--verbose", "-x", "1", "--quiet")
usage: --verbose --quiet -x X
Expected '--quiet'. Got '-x'
To see why note the implicit parentheses:
>>> p = (flag("verbose") + flag("quiet")) + option("x")
In order to cover the case where -x
comes between --verbose
and
--quiet
, use nonpositional
>>> p = nonpositional(flag("verbose"), flag("quiet"), option("x"))
>>> p.parse_args("--verbose", "-x", "1", "--quiet") # works
{'verbose': True, 'x': '1', 'quiet': True}
Putting it all together#
Let’s recall the original example without the syntactic sugar:
>>> p = nonpositional(
... (flag("verbose") | flag("quiet")).optional(),
... option("x", type=int, help="the base"),
... option("y", type=int, help="the exponent"),
... )
>>> def main(x, y, verbose=False, quiet=False):
... print(dict(x=x, y=y, verbose=verbose, quiet=quiet))
As we’ve seen, (flag("verbose") | flag("quiet")).optional()
succeeds
on either --verbose
or --quiet
or neither.
option("x", type=int)
succeeds on -x X
, where X
is some
integer, binding that integer to the variable "x"
. Similarly for
option("y", type=int)
.
nonpositional
takes the three parsers:
(flag("verbose") | flag("quiet")).optional()
option("x", type=int)
option("y", type=int)
and applies them in every order, until some order succeeds.
Applying the syntactic sugar:
>>> @command(
... parsers=dict(kwargs=(flag("verbose") | flag("quiet")).optional()),
... help=dict(x="the base", y="the exponent"),
... )
...
... def main(x: int, y: int, **kwargs):
... pass # do work
Here the parsers
argument reserves a function argument (in this
case, kwargs
) for a custom parser (in this case,
(flag("verbose") | flag("quiet")).optional()
) using our lower-level
syntax. The help
argument assigns help text to the arguments (in
this case x
and y
).