Skip to content

How to use

It's important to note that ToolR relies on proper typing of the python functions that will become commands. If fact, it will complain and error out if typing information is missing or unable to parse.

It's also important to note that the function must also have a properly written docstring using google style docstrings.

from __future__ import annotations

from toolr import Context
from toolr import command_group

group = command_group("example", title="Example", description="Example commands")


@group.command
def echo(ctx: Context, what: str):
    """
    Command title line.

    This is the command description, it can span several lines.

    Args:
        what: What to echo.
    """
    ctx.print(what)

Let's see it!

toolr example -h
Usage: toolr example [-h] {echo} ...

Example commands

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

Example:
  Example commands

  {echo}
    echo      Command title line.

And now the command help:

toolr example echo -h
Usage: toolr example echo [-h] WHAT

This is the command description, it can span several lines.

Positional Arguments:
  WHAT        What to echo.

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

Roundup #1

So far you've seen a few important pieces:

Docstrings

Docstrings are really useful and can greatly improve the CLI UX:

"""
Complete example.

The purpose is to provide an extensive usage example, kind if like TDD

| Example | Description |
|---------|-------------|
| hello   | Say hello.  |
| goodbye | Say goodbye.|
| multiply| Multiply two numbers.|



"""

from __future__ import annotations

import shutil
from enum import StrEnum
from typing import Annotated
from typing import NoReturn

from toolr import Context
from toolr import arg
from toolr import command_group

group = command_group("example", title="Example", docstring=__doc__)


@group.command
def hello(ctx: Context) -> NoReturn:
    """
    Say hello.

    This is the long description about the hello command.
    """
    ctx.info("Hello, world!")


@group.command("goodbye")
def say_goodbye(ctx: Context, name: str | None = None) -> NoReturn:
    """
    Say goodbye.

    Args:
        name: Name to say goodbye to. If not provided, defaults to "world".
    """
    if name is None:
        name = "world"
    ctx.info(f"Goodbye, {name}!")


@group.command
def multiply(ctx: Context, a: int, b: int, verbose: bool = False) -> NoReturn:
    """
    Multiply two numbers.

    Args:
        a: First number.
        b: Second number.
        verbose: Whether to print the result calculation. Defaults to False, print only the result.
    """
    result = a * b
    if verbose:
        ctx.info(f"{a} * {b} = {result}")
    else:
        ctx.info(result)


class Operation(StrEnum):
    ADD = "add"
    SUBTRACT = "subtract"
    MULTIPLY = "multiply"
    DIVIDE = "divide"


@group.command
def math(
    ctx: Context,
    a: int,
    b: int,
    operation: Annotated[Operation, arg(aliases=["-o", "--op"])] = Operation.ADD,
    verbose: bool = False,
) -> NoReturn:
    """
    Perform a mathematical operation.

    Args:
        a: First number.
        b: Second number.
        operation: Operation to perform.
        verbose: Whether to print the result calculation. Defaults to False, print only the result.
    """
    match operation:
        case Operation.ADD:
            value = a + b
            log_msg = f"{a} + {b} = {value}"
        case Operation.SUBTRACT:
            value = a - b
            log_msg = f"{a} - {b} = {value}"
        case Operation.MULTIPLY:
            value = a * b
            log_msg = f"{a} * {b} = {value}"
        case Operation.DIVIDE:
            if b == 0:
                ctx.error("Division by zero!")
                return
            value = a / b
            log_msg = f"{a} / {b} = {value}"
        case _:
            raise ValueError(f"Invalid operation: {operation}")
    if verbose:
        ctx.info(log_msg)
    else:
        ctx.info(value)


@group.command
def py_version(ctx: Context) -> NoReturn:
    """
    Show Python version.

    This command demonstrates how to run subprocess commands and capture their output.
    """
    python = shutil.which("python")
    ret = ctx.run(python, "--version", capture_output=True, stream_output=False)
    ctx.info("Python version", ret.stdout.read().strip())

It can even render some markdown tables!

Module Help

toolr example -h
Usage: toolr example [-h] {hello,goodbye,multiply,math} ...

Complete example.

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

Example:
  The purpose is to provide an extensive usage example, kind if like TDD

    Example    Description
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    hello      Say hello.
    goodbye    Say goodbye.
    multiply   Multiply two numbers.

  {hello,goodbye,multiply,math}
    hello               Say hello.
    goodbye             Say goodbye.
    multiply            Multiply two numbers.
    math                Perform a mathematical operation.

math command help

toolr example math -h
Usage: toolr example math [-h] [--operation OPERATION] [--verbose] A B

Perform a mathematical operation.

Positional Arguments:
  A                     First number.
  B                     Second number.

Options:
  -h, --help            show this help message and exit
  --operation, -o, --op OPERATION
                        Operation to perform. Choices: 'add', 'subtract', 'multiply', 'divide'. (default: add)
  --verbose             Whether to print the result calculation. Defaults to False, print only the result. (default: False)

Advanced Topics

Mutually Exclusive Arguments

ToolR supports mutually exclusive argument groups, which allow you to define sets of arguments where only one can be used at a time. This is useful for scenarios like verbosity levels, output formats, or alternative processing modes.

Basic Usage

Use the group parameter in the arg() function to specify which mutually exclusive group an argument belongs to:

from __future__ import annotations

from typing import Annotated

from toolr import Context
from toolr import arg


def process_file(
    ctx: Context,
    filename: str,
    *,
    verbose: Annotated[bool, arg(group="verbosity")] = False,
    quiet: Annotated[bool, arg(group="verbosity")] = False,
) -> None:
    """Process a file with configurable verbosity.

    Args:
        filename: The file to process.
        verbose: Enable verbose output.
        quiet: Suppress all output.
    """
    if verbose:
        ctx.info(f"Processing {filename} with verbose output...")
    elif quiet:
        # Process silently
        pass
    else:
        ctx.info(f"Processing {filename}...")

Verbose/Debug Example

Here's a more comprehensive example showing different verbosity and debug levels:

from __future__ import annotations

from typing import Annotated

from toolr import Context
from toolr import arg


def analyze_data(
    ctx: Context,
    input_file: str,
    *,
    # Verbosity group - only one can be used
    verbose: Annotated[bool, arg(group="verbosity")] = False,
    quiet: Annotated[bool, arg(group="verbosity")] = False,
    debug: Annotated[bool, arg(group="verbosity")] = False,
    # Output format group - only one can be used
    json: Annotated[bool, arg(group="format")] = False,
    yaml: Annotated[bool, arg(group="format")] = False,
    csv: Annotated[bool, arg(group="format")] = False,
) -> None:
    """Analyze data with multiple configuration options.

    Args:
        input_file: Input file to analyze.
        verbose: Enable verbose output.
        quiet: Suppress all output.
        debug: Enable debug output with detailed logging.
        json: Output results in JSON format.
        yaml: Output results in YAML format.
        csv: Output results in CSV format.
    """
    # Determine verbosity level
    if verbose:
        ctx.info("Verbose mode enabled")
    elif quiet:
        ctx.info("Quiet mode enabled")
    elif debug:
        ctx.info("Debug mode enabled with detailed logging")
    else:
        ctx.info("Normal mode")

    # Determine output format
    if json:
        ctx.info("Output will be in JSON format")
    elif yaml:
        ctx.info("Output will be in YAML format")
    elif csv:
        ctx.info("Output will be in CSV format")
    else:
        ctx.info("Output will be in default format")

    ctx.info(f"Analyzing {input_file}...")

Command Line Usage

When using the above function, you can only specify one argument from each group:

# Valid usage - one from each group
toolr analyze-data input.txt --verbose --json --fast

# Invalid usage - multiple from verbosity group
toolr analyze-data input.txt --verbose --quiet  # Error!

# Invalid usage - multiple from format group  
toolr analyze-data input.txt --json --yaml      # Error!

# Valid usage - using defaults for some groups
toolr analyze-data input.txt --debug --csv

Error Example

# This will raise an error
def invalid_function(
    ctx: Context,
    name: Annotated[str, arg(group="invalid")],  # Positional argument in group - ERROR!
) -> None:
    """This function will fail to parse.

    Args:
        name: The name parameter.
    """

This would raise: SignatureError: Positional parameter 'name' cannot be in a mutually exclusive group.

Third-Party Commands

ToolR supports 3rd-party commands from installable Python packages. This allows you to extend ToolR's functionality by installing additional packages that provide their own commands.

Creating a 3rd-Party Package

To create a package that contributes commands to ToolR, you need to:

  1. Define your commands using the standard ToolR API
  2. Register an entry point in your package's pyproject.toml

Here's an example of a 3rd-party package structure:

thirdparty/commands.py
from __future__ import annotations

from toolr import Context
from toolr import command_group

third_party_group = command_group("third-party", "Third Party Tools", "Tools from third-party packages")

@third_party_group.command("hello")
def hello_command(ctx: Context, name: str = "World") -> None:
    """Say hello to someone.

    Args:
        ctx: The execution context
        name: Name to greet (default: World)
    """
    ctx.print(f"Hello, {name} from 3rd-party package!")

@third_party_group.command("version")
def version_command(ctx: Context) -> None:
    """Show the version of the 3rd-party package.

    Args:
        ctx: The execution context
    """
    ctx.print("3rd-party package version 1.0.0")

Entry Point Configuration

In your package's pyproject.toml, define the entry point:

[project.entry-points."toolr.tools"]
<this name is not important> = "<package>.<module calling toolr.command_group()>"

For example:

[project.entry-points."toolr.tools"]
commands = "thirdparty.commands"

Installation and Discovery

Once installed alongside ToolR, the package will automatically contribute its commands. You can see a complete working example in the ToolR repository.

Command Resolution

When multiple packages provide commands with the same name:

  • Repository commands (commands defined in your local tools/ directory) override 3rd-party commands
  • If the parent command group is shared, 3rd-party commands augment the existing command group

This allows for flexible command composition while maintaining local control over command behavior.