candy-kingdom / mints

Clean and elegant CLI development kit.
MIT License
3 stars 1 forks source link
cli python

Mints

PyPI version Build

Clean and elegant CLI development kit.

Overview

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

Install

$ pip install mints

Getting started

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.

Parameters

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

Help page

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

Short name

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

Prefix

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

Types

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'>

Default types

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'>

User-defined types

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

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.

Commands

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

Learn more by looking at our carefully prepared examples.

License

The package is licensed under the MIT license.

Contributing

Before creating an issue or submitting a patch, check out our contribution guildelines.