Module bussilab.cli

Tools to implement the command line interface

This module is used internally to implement the command line interface. In addition, it can be used to run the commands that are available in the command line interface without the need to leave python:

from bussilab.cli import cli
cli("-h")
cli("wham -b bias") # provide the command line as a string
cli(["wham", "-b", "bias"]) # alternatively use a list

The documentation of all the commands can be found in the cli_documentation submodule.

Notice however that these commands typically have alternative python implementations that allow you to work directly on data structures and are thus more flexible. For instance, wham()(bias), where bias is a numpy array, is often more convenient than bussilab.cli.cli("wham -b bias"), where bias is a file.

Functions

def arg(*name, **kwargs)
Expand source code
def arg(*name, **kwargs):
    """ Decorator that adds an argument to a command line tool.

        Parameters are passed to the `parser.add_argument()` function.
        It should be written **after** the `bussilab.cli.command` decorator.
    """
    def call(p):
        p[-1].add_argument(*name, **kwargs)
        return p
    def add_argument(func):
        func = _Argument(func)
        func.calls.append(call)
        return func
    return add_argument

Decorator that adds an argument to a command line tool.

Parameters are passed to the parser.add_argument() function. It should be written after the command() decorator.

def cli(arguments: str | List[str] = '',
*,
prog: str | None = '',
use_argcomplete: bool = False,
throws_on_parser_errors: bool = True) ‑> int | None
Expand source code
def cli(arguments: Union[str, List[str]] = "",
        *,
        prog: Optional[str] = "",  # None is used to imply sys.argv[0]
        use_argcomplete: bool = False,
        throws_on_parser_errors: bool = True
       ) -> Optional[int]:
    """Executes a command line tool from python.

       This is the main function of this module and allows to launch all the subcommands available
       in the command line interface directly from python.

       Parameters
       ----------
       arguments : str or list
           Command line arguments. If a string is passed, it is first split using
           shlex.split()

       prog : str
           Name of the calling program. It is used to build help texts. **Mostly for internal use**.

       use_argcomplete : bool
           If True, the `autocomplete` function of `argcomplete` module is called on the parser,
           so as to allow autocompletion in the command line tool.
           If `argcomplete` module is not installed, nothing is done and no failure is reported.
           **Mostly for internal use**.

       throws_on_parser_errors : bool
           If True, in case of command line error it throws a TypeError
           exception. **Mostly for internal use**.

       Returns
       -------
       None or int
           If an error happens while parsing, it throws a TypeError exception,
           unless throws_on_parser_errors is set to false, in which ase it
           returns the corresponding error code.
           If an error happens while executing the requested command, an exception is thrown.
           If everything goes well, it returns None.
    """

    # allow passing a single string
    if isinstance(arguments, str):
        arguments = shlex.split(arguments)

    func = None

    if prog == "":
        eparser = _eparser
    else:
        eparser = _create_eparser(prog)

    if use_argcomplete:
        # optional feature:
        try:
            import argcomplete
            argcomplete.autocomplete(eparser.parser)
        except ImportError:
            pass

    # Parse options
    # In order to avoid python to crash when cli() is called from python
    # with wrong arguments, it is necessary to intercept the exception.
    try:
        args = vars(eparser.parser.parse_args(arguments))
    except SystemExit as e:
        if e.code != 0:
            if throws_on_parser_errors:
                raise TypeError("Error parsing command line arguments," +
                                " return code is " + str(e.code))
            else:
                return e.code
        return None

    if '_func' in args:
        func = args["_func"]
        del args["_func"]

    main_args = {}
    for i in args.keys():
        if i[0] == "_":
            main_args[i[1:]] = args[i]

    for i in main_args:
        del args["_"+i]

    if "version" in main_args and main_args["version"]:
        from . import __version__
        print(__version__)
        return None

    remove = []
    for i in args.keys():
        if args[i] is None:
            remove.append(i)

    for i in remove:
        del args[i]

    if func:
        func(**args)
    else:
        eparser.parser.print_usage()

    return None

Executes a command line tool from python.

This is the main function of this module and allows to launch all the subcommands available in the command line interface directly from python.

Parameters

arguments : str or list
Command line arguments. If a string is passed, it is first split using shlex.split()
prog : str
Name of the calling program. It is used to build help texts. Mostly for internal use.
use_argcomplete : bool
If True, the autocomplete function of argcomplete module is called on the parser, so as to allow autocompletion in the command line tool. If argcomplete module is not installed, nothing is done and no failure is reported. Mostly for internal use.
throws_on_parser_errors : bool
If True, in case of command line error it throws a TypeError exception. Mostly for internal use.

Returns

None or int
If an error happens while parsing, it throws a TypeError exception, unless throws_on_parser_errors is set to false, in which ase it returns the corresponding error code. If an error happens while executing the requested command, an exception is thrown. If everything goes well, it returns None.
def command(name: str, help: str | None = None, description: str | None = None, **kwargs)
Expand source code
def command(name: str, help: Optional[str] = None, description: Optional[str] = None, **kwargs):
    """Decorator that registers a function as a subcommand.

       This decorator should be written **before** the other decorators
       `bussilab.cli.arg`, `bussilab.cli.group`, and `bussilab.cli.endgroup`.

       Parameters
       ----------
       name : str
           Name of the subcommand (will be used on the command line)

       help : str
           Short help message for the subcommand (one line).

       description : str, optional
           Longer description. If not provided, it is set to a copy of `help`.

       kwargs
           Other parameters are passed as is to the `add_parser` function of `argparse`.

       Examples
       --------

       Simple command line tool that accepts a single `--out` argument followed by a string
       and call the function `do_something` with that string as an argument.

       ```python
       from bussilab.cli import command, arg

       @command("subcommand")
       @arg("--out")
       def myfunc(out):
           do_something(out)
       ```
    """
    if description is None:
        description = help
    def add_command(func):
        func = _Argument(func)
        _commands.append((name, help, description, func, kwargs))
        return func
    return add_command

Decorator that registers a function as a subcommand.

This decorator should be written before the other decorators arg(), group(), and endgroup().

Parameters

name : str
Name of the subcommand (will be used on the command line)
help : str
Short help message for the subcommand (one line).
description : str, optional
Longer description. If not provided, it is set to a copy of help.
kwargs
Other parameters are passed as is to the add_parser function of argparse.

Examples

Simple command line tool that accepts a single --out argument followed by a string and call the function do_something with that string as an argument.

from bussilab.cli import command, arg

@command("subcommand")
@arg("--out")
def myfunc(out):
    do_something(out)
def endgroup(f: Callable | None = None)
Expand source code
def endgroup(f: Optional[Callable] = None):
    """ Decorator that ends a group of arguments for a command line tool.

        See `bussilab.cli.group`.
    """
    def call(p):
        if len(p) < 2:
            msg = "Non matching group/endgroup"
            raise TypeError(msg)
        return p[:-1]
    def end_group(func):
        func = _Argument(func)
        func.calls.append(call)
        return func
    if f:
        return end_group(f)
    return end_group

Decorator that ends a group of arguments for a command line tool.

See group().

def group(title: str | Callable | None = None,
description: str | None = None,
exclusive: bool | None = None,
required: bool | None = None)
Expand source code
def group(title: Union[str, Callable, None] = None,
          description: Optional[str] = None,
          exclusive: Optional[bool] = None,
          required: Optional[bool] = None):
    """ Decorator that adds a group of arguments for a command line tool.

        It should be written **after** the `bussilab.cli.command` decorator.
        It should be followed by a number of `bussilab.cli.arg` decorators and by a closing
        `bussilab.cli.endgroup` decorator.

        Parameters
        ----------
        title : str
            The name of the group. Can only be used for non exclusive groups.

        description : str
            A description of the group. Can only be used for non exclusive groups.

        exclusive : bool
            If True, the arguments belonging to this group are mutually exclusive

        required : bool
            If True, one of the arguments at least should be passed.
            Can only be used for exclusive groups.

        Examples
        --------

        This is a simple command line tool that accepts three arguments (`-a`, `-b`, or `-c`),
        mutually exclusive. When ran, it will just print booleans showing if these arguments
        were passed.

        ```python
        from bussilab.cli import command, group, arg, endgroup

        @command("doit")
        @group(exclusive=True)
        @arg("-a", action='store_true')
        @arg("-b", action='store_true')
        @arg("-c", action='store_true')
        @endgroup
        def check(a, b, c):
           print(a, b, c)
        ```
    """
    noarg = None
    # note: _Argument is callable as well
    if description is None and exclusive is None and required is None and callable(title):
        noarg = title
        title = None
    if exclusive is None:
        exclusive = False
    if not exclusive and required is not None:
        msg = "required can only be used for exclusive groups"
        raise TypeError(msg)
    if exclusive and title is not None:
        msg = "title can only be used for non exclusive groups"
        raise TypeError(msg)
    if exclusive and description is not None:
        msg = "description can only be used for non exclusive groups"
        raise TypeError(msg)
    if exclusive and required is None:
        required = False  # default for add_mutually_exclusive_group
    def call(p):
        if exclusive:
            p.append(p[-1].add_mutually_exclusive_group(required=required))
        else:
            p.append(p[-1].add_argument_group(title=title, description=description))
        return p
    def begin_group(func: Union[Callable, _Argument]):
        func = _Argument(func)
        func.calls.append(call)
        return func
    if noarg:
        return begin_group(noarg)
    return begin_group

Decorator that adds a group of arguments for a command line tool.

It should be written after the command() decorator. It should be followed by a number of arg() decorators and by a closing endgroup() decorator.

Parameters

title : str
The name of the group. Can only be used for non exclusive groups.
description : str
A description of the group. Can only be used for non exclusive groups.
exclusive : bool
If True, the arguments belonging to this group are mutually exclusive
required : bool
If True, one of the arguments at least should be passed. Can only be used for exclusive groups.

Examples

This is a simple command line tool that accepts three arguments (-a, -b, or -c), mutually exclusive. When ran, it will just print booleans showing if these arguments were passed.

from bussilab.cli import command, group, arg, endgroup

@command("doit")
@group(exclusive=True)
@arg("-a", action='store_true')
@arg("-b", action='store_true')
@arg("-c", action='store_true')
@endgroup
def check(a, b, c):
   print(a, b, c)