Skip to main content

Command Line Interface Generator

Project description

clig

A single module, pure python, Command Line Interface Generator


Installation

pip install clig

License

clig is distributed under the terms of the MIT license.


User guide

clig is a single module, written in pure python, that wraps around the stdlib module argparse (using the stdlib module inspect) to generate command line interfaces through simple functions.

Basic usage

Create or import some function and call clig.run() with it:

# example01.py
import clig

def noundata(name, title="Mister"):
    print(f"Title: {title}")
    print(f"Name: {name}")

clig.run(noundata)

In general, the function arguments that have a "default" value are turned into optional flagged (--) command line arguments, while the "non default" will be positional arguments.

> python example01.py -h

    usage: noundata [-h] [--title TITLE] name

    positional arguments:
      name

    options:
      -h, --help     show this help message and exit
      --title TITLE

The script can then be used in the same way as used with argparse:

> python example01.py John

    Title: Mister
    Name: John

> python example01.py Maria --title Miss

    Title: Miss
    Name: Maria

Helps

Arguments and command Helps are taken from the docstring when possible:

# example02.py
import clig

def greetings(name, greet="Hello"):
    """Description of the command: A greeting prompt!

    Args:
        name: The name to greet
        greet: The greeting used. Defaults to "Hello".
    """
    print(f"Greetings: {greet} {name}!")

clig.run(greetings)
> python example02.py --help

    usage: greetings [-h] [--greet GREET] name

    Description of the command: A greeting prompt!

    positional arguments:
      name           The name to greet

    options:
      -h, --help     show this help message and exit
      --greet GREET  The greeting used. Defaults to "Hello".

There is an internal list of docstring templates from which you can choose if the inferred docstring is not correct. It is also possible to specify your own custom docstring template.

Argument inference

Based on type annotations, some arguments can be inferred from the function signature to pass to the argparse.ArgumentParser.add_argument() method:

# example03.py
import clig

def recordperson(name: str, age: int, height: float):
    print(locals())

clig.run(recordperson)

The types in the annotation may be passed to argparse.ArgumentParser.add_argument() method as type keyword argument:

> python example03.py John 37 1.70

    {'name': 'John', 'age': 37, 'height': 1.7}

And the type conversions are performed as usual

> python example03.py Mr John Doe

    usage: recordperson [-h] name age height
    recordperson: error: argument age: invalid int value: 'John'

Booleans

Booleans are transformed in arguments with action of kind "store_true" or "store_false" (depending on the default value).

# example04.py
import clig

def recordperson(name: str, age: int, title="Mister", graduate: bool = False):
    print(locals())

clig.run(recordperson)
> python example04.py -h

    usage: recordperson [-h] [--title TITLE] [--graduate] name age

    positional arguments:
      name
      age

    options:
      -h, --help     show this help message and exit
      --title TITLE
      --graduate

> python example04.py Leo 36 --title "Doctor" --graduate

    {'name': 'Leo', 'age': 36, 'title': 'Doctor', 'graduate': True}

If no default is given to the boolean, a required=True keyword argument is passed to add_argument() method in the flag boolean option and a BooleanOptionalAction (already available in argparse) is passed as action keyword argument, adding support for a boolean complement action in the form --no-option:

# example05.py
import clig

def recordperson(name: str, age: int, graduate: bool):
    print(locals())

clig.run(recordperson)
> python example05.py -h

    usage: recordperson [-h] --graduate | --no-graduate name age

    positional arguments:
      name
      age

    options:
      -h, --help            show this help message and exit
      --graduate, --no-graduate

> python example05.py Ana 23

    usage: recordperson [-h] --graduate | --no-graduate name age
    recordperson: error: the following arguments are required: --graduate/--no-graduate

Tuples, Sequences and Lists: nargs

If the type is a tuple of specified length N, the argument automatically uses nargs=N. If the type is a generic Sequence, a list or a tuple of any length (i.e., tuple[<type>, ...]), it uses nargs="*".

# example06.py
from typing import Sequence
import clig


def main(name: tuple[str, str], ages: list[int]):
    print(locals())


clig.run(main)
> python example06.py -h

    usage: main [-h] name name [ages ...]

    positional arguments:
      name
      ages

    options:
      -h, --help  show this help message and exit

> python example06.py John Mary 2 78 35

    {'name': ['John', 'Mary'], 'ages': [2, 78, 35]}

Literals and Enums: choices

If the type is a Literal or a Enum the argument automatically uses choices.

# example07.py
from typing import Literal
import clig

def main(name: str, move: Literal["rock", "paper", "scissors"]):
    print(locals())

clig.run(main)
> python example07.py -h

    usage: main [-h] name {rock,paper,scissors}

    positional arguments:
      name
      {rock,paper,scissors}

    options:
      -h, --help            show this help message and exit

As is expected in argparse, an error message will be displayed if the argument was not one of the acceptable values:

> python example07.py John knife

    usage: main [-h] name {rock,paper,scissors}
    main: error: argument move: invalid choice: 'knife' (choose from rock, paper, scissors)

> python example07.py Mary paper

    {'name': 'Mary', 'move': 'paper'}

Enums should be passed by name
# example08.py
from enum import Enum, StrEnum
import clig

class Color(Enum):
    red = 1
    blue = 2
    yellow = 3

class Statistic(StrEnum):
    minimun = "minimun"
    mean = "mean"
    maximum = "maximum"

def main(color: Color, statistic: Statistic):
    print(locals())

clig.run(main)
> python example08.py -h

    usage: main [-h] {red,blue,yellow} {minimun,mean,maximum}

    positional arguments:
      {red,blue,yellow}
      {minimun,mean,maximum}

    options:
      -h, --help            show this help message and exit

> python example08.py red mean

    {'color': <Color.red: 1>, 'statistic': <Statistic.mean: 'mean'>}

> python example08.py green

    usage: main [-h] {red,blue,yellow} {minimun,mean,maximum}
    main: error: argument color: invalid choice: 'green' (choose from red, blue, yellow)

You can even mix Enum and Literal
# example09.py
from typing import Literal
from enum import Enum
import clig

class Color(Enum):
    red = 1
    blue = 2
    yellow = 3

def main(color: Literal[Color.red, "green", "black"]):
    print(locals())

clig.run(main)
> python example09.py red

    {'color': <Color.red: 1>}

> python example09.py green

    {'color': 'green'}

Argument specification

TODO

Subcommands

Instead of using the function clig.run(), you can create an object instance of the type Command, passing your function to its constructor, and call the Command.run() method.

# example10.py
from clig import Command

def main(name:str, age: int, height):
    print(locals())

cmd = Command(main)
cmd.run()
> python example10.py "Carmem Miranda" 42 1.85

    {'name': 'Carmem Miranda', 'age': 42, 'height': '1.85'}

This makes possible to use some methods to add subcommands. All subcommands will also be instances of the same class Command. There are 4 methods available:

  • subcommand: Creates the subcommand and returns the input function unchanged. This is a proper method to be used as a function decorator.
  • new_subcommand: Creates a subcommand and returns the new created Command instance.
  • add_subcommand: Creates the subcommand and returns the caller object. This is useful to add multiple subcommands in one single line.
  • end_subcommand: Creates the subcommand and returns the parent of the caller object. If the caller doesn't have a parent, an error will be raised. This is useful when finishing to add subcommands in the object.

The functions will execute sequentially, from a Command to its subcommands.

# example11.py
from inspect import getframeinfo, currentframe
from clig import Command

def main(verbose: bool = False):
    """The main function

    Args:
        verbose: Verbose option
    """
    print(f"{getframeinfo(currentframe()).function} {locals()}")

# The main command could also not have a function
cmd = Command(main)

@cmd.subcommand
def foo(a, b):
    """The foo command

    Args:
        a: Help for a argument
        b: Help for b argument
    """
    print(f"{getframeinfo(currentframe()).function} {locals()}")

@cmd.subcommand
def bar(c, d):
    """The bar command

    Args:
        c: Help for c argument
        d: Help for d argument
    """
    print(f"{getframeinfo(currentframe()).function} {locals()}")

cmd.run()
> python example11.py -h

    usage: main [-h] [--verbose] {foo,bar} ...

    The main function

    options:
      -h, --help  show this help message and exit
      --verbose   Verbose option

    subcommands:
      {foo,bar}
        foo
        bar

> python example11.py foo -h

    usage: main foo [-h] a b

    The foo command

    positional arguments:
      a           Help for a argument
      b           Help for b argument

    options:
      -h, --help  show this help message and exit

> python example11.py bar -h

    usage: main bar [-h] c d

    The bar command

    positional arguments:
      c           Help for c argument
      d           Help for d argument

    options:
      -h, --help  show this help message and exit

> python example11.py bar baz ham

    main {'verbose': False}
    bar {'c': 'baz', 'd': 'ham'}

The next example tries to reproduce some of the Git interface, using methods after the function definitions.

# example12.py
from inspect import getframeinfo, currentframe
from pathlib import Path
from clig import Command

def git(exec_path: Path = Path("git"), work_tree: Path = Path("C:/Users")):
    print(f"{getframeinfo(currentframe()).function} {locals()}")

def status(branch: str):
    print(f"{getframeinfo(currentframe()).function} {locals()}")

def commit(message: str):
    print(f"{getframeinfo(currentframe()).function} {locals()}")

def remote(verbose: bool = False):
    print(f"{getframeinfo(currentframe()).function} {locals()}")

def add(name: str, url: str):
    print(f"{getframeinfo(currentframe()).function} {locals()}")

def rename(old: str, new: str):
    print(f"{getframeinfo(currentframe()).function} {locals()}")

def remove(name: str):
    print(f"{getframeinfo(currentframe()).function} {locals()}")

def submodule(quiet: bool):
    print(f"{getframeinfo(currentframe()).function} {locals()}")

def init(path: Path = Path(".").resolve()):
    print(f"{getframeinfo(currentframe()).function} {locals()}")

def update(init: bool, path: Path = Path(".").resolve()):
    print(f"{getframeinfo(currentframe()).function} {locals()}")

######################################################################################
# The interface is all built in the code below, which could also be in another file

(
    Command(git)
    .add_subcommand(status)
    .add_subcommand(commit)
    .new_subcommand(remote)
        .add_subcommand(add)
        .add_subcommand(rename)
        .end_subcommand(remove)
    .new_subcommand(submodule)
        .add_subcommand(init)
        .end_subcommand(update)
    .run()
)
> python example12.py -h

    usage: git [-h] [--exec-path EXEC_PATH] [--work-tree WORK_TREE]
               {status,commit,remote,submodule} ...

    options:
      -h, --help            show this help message and exit
      --exec-path EXEC_PATH
      --work-tree WORK_TREE

    subcommands:
      {status,commit,remote,submodule}
        status
        commit
        remote
        submodule

> python example12.py remote -h

    usage: git remote [-h] [--verbose] {add,rename,remove} ...

    options:
      -h, --help           show this help message and exit
      --verbose

    subcommands:
      {add,rename,remove}
        add
        rename
        remove

> python example12.py remote rename oldName newName

    git {'exec_path': WindowsPath('git'), 'work_tree': WindowsPath('C:/Users')}
    remote {'verbose': False}
    rename {'old': 'oldName', 'new': 'newName'}

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

clig-0.1.0.tar.gz (106.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

clig-0.1.0-py3-none-any.whl (13.3 kB view details)

Uploaded Python 3

File details

Details for the file clig-0.1.0.tar.gz.

File metadata

  • Download URL: clig-0.1.0.tar.gz
  • Upload date:
  • Size: 106.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.9

File hashes

Hashes for clig-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ab510505cd7a099661eae00de700f94d3f3ac66a35a2330380886629d863942e
MD5 2285a187d1f5b6bec826cc9e8a0d885a
BLAKE2b-256 9bdd93245b283606ef9c63522fc58d15c0566354861254b9cca40a660d850573

See more details on using hashes here.

File details

Details for the file clig-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: clig-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 13.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.9

File hashes

Hashes for clig-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7d516f0907a71f3293c19907b663afa52a26d5f3e9b8a56a47f91cf983f54088
MD5 886e97f7ca174943ee1ebb8519f16a1e
BLAKE2b-256 852686574c2a081cdb545c2811009405487e781a2ba77c9710f7356b673a50a2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page