Clean and elegant CLI development kit.
Mints is a microframework that allows building declarative and nice-looking CLI apps. Unlike Click or Plac, it utilizes function annotations more than decorators.
Here is a quick example:
# say.py
from mints import cli, Arg, Flag, Opt
@cli
def say(phrase: Arg('a phrase to print'),
caps: Flag('whether to print phrase in upper-case'),
times: Opt[int]('how many times to print') = 1):
"""Prints a phrase specified number of times."""
for _ in range(times):
print(phrase.upper() if caps else phrase)
if __name__ == '__main__':
cli()
And what we get in the command line:
$ python3 say.py "Hi!"
Hi!
$ python3 say.py "Hi!" --caps
HI!
$ python3 say.py "Hi!" --times 3
Hi!
Hi!
Hi!
$ python3 say.py --help
usage: say [-h] [--caps] [--times TIMES] phrase
Prints a phrase specified number of times.
positional arguments:
phrase a phrase to print
optional arguments:
-h, --help show this help message and exit
--caps whether to print phrase in upper-case
--times TIMES how many times to print
$ pip install mints
Note: the examples are not PEP 8 compatible: one blank line is used instead of two to separate top-level definitions.
In general, writing a CLI app is very similar to writing a regular function. This is also true for Mints.
Consider the following example:
# say.py
from mints import cli, Arg
@cli
def say(phrase: Arg):
print(phrase)
if __name__ == '__main__':
cli()
The script can be executed as a command-line app:
$ python3 say.py "Hello, world!"
Hello, world!
The main idea is very simple: you use the cli
decorator to wrap a function that acts as an entry point of the application (say
function from the example above), and then call the cli()
to make things running.
In the next sections you'll find out how to build more complex apps in Mints.
Arg
Arg
is an annotation for positional arguments.
Positional arguments in CLIs work in the same way as in programming languages.
Consider the following function:
# test.py
from mints import cli, Arg
@cli
def test(x: Arg, y: Arg):
print(x, y)
if __name__ == '__main__':
cli()
$ python test.py 1 2
1 2
Note: it's not possible to execute the script without an argument.
$ python test.py 1
usage: test [-h] x y
test: error: the following arguments are required: y
To address this issue, you could provide a default value to the argument:
# test.py
from mints import cli, Arg
@cli
def test(x: Arg, y: Arg = 2):
print(x, y)
if __name__ == '__main__':
cli()
$ python test.py 1
1 2
Flag
Flag
is an annotation for flags.
Flags are boolean arguments that represent an on/off behavior. Unlike positional arguments, they should be specified in the command line only with a special syntax.
Here is an example of a flag:
# test.py
from mints import cli, Flag
@cli
def test(some: Flag):
print(some)
if __name__ == '__main__':
cli()
$ python test.py --some
True
$ python test.py
False
Opt
Opt
is an annotation for options.
Options are simply flags with values (or arguments with names).
That's how you use the Opt
:
# test.py
from mints import cli, Opt
@cli
def test(some: Opt):
print(some)
if __name__ == '__main__':
cli()
$ python test.py --some 1
1
Note: it's not possible to not specify the option by default, as it was for flags.
$ python test.py --some
usage: test [-h] --some SOME
test: error: the following arguments are required: --some
You still could provide a default value:
# test.py
from mints import cli, Opt
@cli
def test(some: Opt = 1):
print(some)
if __name__ == '__main__':
cli()
$ python test.py
1
Each CLI in Mints has a built-in help page, which is automatically generated.
Consider the following example:
# test.py
from mints import cli, Arg
@cli
def test(some: Arg):
print(some)
if __name__ == '__main__':
cli()
$ python test.py --help
usage: test [-h] some
positional arguments:
some
optional arguments:
-h, --help show this help message and exit
Note the lack of the program description as well as the some
argument description.
To override the description of the program, put a simple doc-comment to a CLI function.
To assign a description to an argument, instantiate an annotation with the description
argument (it always comes first).
# test.py
from mints import cli, Arg
@cli
def test(some: Arg('some argument')):
"""A simple demonstration program."""
print(some)
if __name__ == '__main__':
cli()
$ python test.py --help
usage: test [-h] some
A simple demonstration program.
positional arguments:
some some argument
optional arguments:
-h, --help show this help message and exit
Usually, both flags and options come with a shortcut syntax. For example, instead of writing:
$ python test.py --some 1
One could write:
$ python test.py -s 1
To define a shortcut letter for a flag or an option, the short
parameter of either Flag
or Opt
should be used:
# test.py
from mints import cli, Flag
@cli
def test(some: Flag(short='s')):
print(some)
if __name__ == '__main__':
cli()
$ python test.py -s
True
Flags and options are usually called with the -
prefix (in short and long variations).
To override this behavior, the prefix
parameter of either Flag
or Opt
should be used.
# test.py
from mints import cli, Flag
@cli
def test(some: Flag(prefix='+')):
print(some)
if __name__ == '__main__':
cli()
$ python test.py ++some
True
By default, an argument that is passed from the CLI is of str
type if it's annotated with either Opt
or Arg
, and of bool
if it's annotated with Flag
.
# test.py
from mints import cli, Arg
@cli
def test(some: Arg):
print(type(some))
if __name__ == '__main__':
cli()
$ python test.py 1
<class 'str'>
To parse a primitive type that is supported by the argparse
, use the following syntax:
# test.py
from mints import cli, Arg
@cli
def test(some: Arg[int]):
print(type(some))
if __name__ == '__main__':
cli()
$ python test.py 1
<class 'int'>
To parse a custom type, register a parser function just for that.
You could use either the parse
decorator:
# test.py
from mints import cli, Arg
# User-defined type.
class Custom:
def __init__(self, x):
self.property = x
# A parser for user-defined type.
@cli.parse
def custom(x: str) -> Custom:
return Custom(x)
@cli
def test(some: Arg[Custom]):
print(some.property)
if __name__ == '__main__':
cli()
$ python test.py 1
1
Or the add_parser
function:
# test.py
from mints import cli, Arg
class Custom:
def __init__(self, x):
self.property = x
@cli
def test(some: Arg[Custom]):
print(some.property)
if __name__ == '__main__':
cli.add_parser(Custom)
cli()
$ python test.py 1
1
Variable arguments are also supported through the standard List
type:
# test.py
from typing import List
from mints import cli, Arg
@cli
def test(some: Arg[List[int]]):
print(some)
if __name__ == '__main__':
cli()
$ python test.py 1 2 3
[1, 2, 3]
Note that lists are non-greedy:
# test.py
from mints import cli, Arg
@cli
def test(x: Arg[int], y: Arg[List[int]], z: Arg[int]):
print(x, y, z)
if __name__ == '__main__':
cli()
$ python test.py 1 2 3 4
1 [2, 3] 4
Consider checking the rolling dices example with a more realistic use case.
Complex command line interfaces like git
have several subcommands, e.g., git status
, git pull
, git push
, etc.
These subcommands act as separate CLIs and, thus, should be defined as separate functions in Mints.
Consider the following example as a mock of git
CLI:
# git.py
from mints import cli, Flag
@cli
def git():
...
@git.command
def pull(rebase: Flag):
if rebase:
print('pulling with rebase')
else:
print('pulling')
@git.command
def push():
print('pushing')
if __name__ == '__main__':
cli()
$ python git.py pull
pulling
$ python git.py pull --rebase
pulling with rebase
$ python git.py push
pushing
Sometimes it's needed to have a deeper hierarchy of subcommands.
For example, the dotnet
CLI tool allows calling dotnet tool install ...
.
In Mints, this could be implemented in a natural way:
# dotnet.py
from mints import cli
@cli
def dotnet():
...
@dotnet.command
def tool():
...
@tool.command
def install():
...
if __name__ == '__main__':
cli()
Learn more by looking at our carefully prepared examples.
The package is licensed under the MIT license.
Before creating an issue or submitting a patch, check out our contribution guildelines.